🌱 crd/markers: Emit cheaper CEL rules for oneOf markers - #1408
Conversation
Followup to kubernetes-sigs#1212 `AtMostOneOf`, `ExactlyOneOf`, and `AtLeastOneOf` currently emit a CEL rule of the form: ```cel [has(self.a), has(self.b), ...].filter(x, x == true).size() <op> N ``` This pays for list construction and a comprehension over the list. Two equivalent but cheaper forms work just as well: * `AtMostOneOf`/`ExactlyOneOf`: ternary-sum: ```cel (has(self.a)?1:0) + (has(self.b)?1:0) + ... <op> 1 ``` Constant-cost scalar arithmetic, no list, no iteration. * `AtLeastOneOf`: short-circuit OR: ```cel has(self.a) || has(self.b) || ... ``` Already a boolean, no count/compare, short-circuits as soon as any field is set. User-visible behavior is unchanged: violation messages are emitted from the marker's static `Message` field, not from the rule expression itself. I did some benchmarking with `github.com/google/cel-go/checker` to measure the estimated cost of both the old and the new CEL expressions with a couple number of items: * estimated cost is from `checker.EstimateCost`: worst-case, what the apiserver budgets against the per-CRD limit at CRD-creation time * runtime cost is from interpreter `ActualCost`: what counts against the per-call budget at admission Runtime input states: * `none`: no field set * `one`: only first field set * `all`: every field set * `last`: only last field set .### `ExactlyOneOf` / `AtMostOneOf` (filter -> sum) | N | est.min | est.max | rt.none | rt.one | rt.all | |----|----------|-------------------|----------|----------|----------| | 2 | 33 -> 6 | 57 -> 6 (−89%) | 31 -> 6 | 44 -> 6 | 44 -> 6 | | 3 | 38 -> 9 | 74 -> 9 (−88%) | 35 -> 9 | 48 -> 9 | 48 -> 9 | | 5 | 48 -> 15 | 108 -> 15 (−86%) | 43 -> 15 | 56 -> 15 | 56 -> 15 | | 10 | 73 -> 30 | 193 -> 30 (−84%) | 63 -> 30 | 76 -> 30 | 76 -> 30 | .### `AtLeastOneOf` (filter -> `||`) | N | est.min | est.max | rt.none | rt.one | rt.last | |----|----------|-------------------|----------|---------|----------| | 2 | 33 -> 2 | 57 -> 4 (−93%) | 31 -> 4 | 44 -> 2 | 44 -> 4 | | 3 | 38 -> 2 | 74 -> 6 (−92%) | 35 -> 6 | 48 -> 2 | 48 -> 6 | | 5 | 48 -> 2 | 108 -> 10 (−91%) | 43 -> 10 | 56 -> 2 | 56 -> 10 | | 10 | 73 -> 2 | 193 -> 20 (−90%) | 63 -> 20 | 76 -> 2 | 76 -> 20 | Note the `rt.one` column for `AtLeastOneOf` (cost `2` across all N): the `||` form short-circuits as soon as any field is found, so runtime cost is independent of how many fields the marker covers when any earlier one is set. So these numbers show that: * both estimated and runtime cost are always lower with the new expressions * the maximum estimated cost, which as raised in kubernetes-sigs#1212 can quickly balloon when nested deep in layers of unbounded arrays is significantly reduced by about 90% in average
|
Hi @HadrienPatte. Thanks for your PR. I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
JoelSpeed
left a comment
There was a problem hiding this comment.
Thanks for the analysis of the cost and the optimisation suggestions, LGTM
/lgtm
/approve
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: HadrienPatte, JoelSpeed The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
LGTM label has been added. DetailsGit tree hash: 985e3d39ae4341373ea6b344e4e1c8c0306b5dec |
|
/ok-to-test |
Followup to #1212
AtMostOneOf,ExactlyOneOf, andAtLeastOneOfcurrently emit a CEL rule of the form:This pays for list construction and a comprehension over the list. Two equivalent but cheaper forms work just as well:
AtMostOneOf/ExactlyOneOf: ternary-sum:Constant-cost scalar arithmetic, no list, no iteration.
AtLeastOneOf: short-circuit OR:Already a boolean, no count/compare, short-circuits as soon as any field is set.
User-visible behavior is unchanged: violation messages are emitted from the marker's static
Messagefield, not from the rule expression itself.I did some benchmarking with
github.com/google/cel-go/checkerto measure the estimated cost of both the old and the new CEL expressions with a couple number of items:checker.EstimateCost: worst-case, what the apiserver budgets against the per-CRD limit at CRD-creation timeActualCost: what counts against the per-call budget at admissionRuntime input states:
none: no field setone: only first field setall: every field setlast: only last field setExactlyOneOf/AtMostOneOf(filter -> sum)AtLeastOneOf(filter ->||)Note the
rt.onecolumn forAtLeastOneOf(cost2across all N): the||form short-circuits as soon as any field is found, so runtime cost is independent of how many fields the marker covers when any earlier one is set.So these numbers show that: