Skip to content

Skip tautological compiled programs in RuleCache#460

Open
snuderl wants to merge 2 commits intobufbuild:mainfrom
snuderl:reduce-residuals-rulecache
Open

Skip tautological compiled programs in RuleCache#460
snuderl wants to merge 2 commits intobufbuild:mainfrom
snuderl:reduce-residuals-rulecache

Conversation

@snuderl
Copy link
Copy Markdown
Contributor

@snuderl snuderl commented Apr 26, 2026

Summary

  • Mirrors protovalidate-go's ReduceResiduals optimization in RuleCache: after compiling a rule's CEL program, we eagerly evaluate it with only the rules / rule variables bound.
  • If the program returns true or "" (the success values), it is tautological for this rule configuration and is dropped before being added to the per-field program list, so it never runs during validation.
  • Programs that depend on the unbound this (or now) variables raise CelEvaluationException during the probe; we swallow that and keep the program as before.

This is a follow-up to #454, which performed a similar tautology check at the MessageEvaluator level.

Test plan

  • ./gradlew test — full unit + conformance suite passes locally.

snuderl added 2 commits April 26, 2026 12:41
Mirrors protovalidate-go's ReduceResiduals: when a rule program evaluates
to true (or empty string) using only the bound rules/rule variables, the
expression is tautological and can be eliminated at compile time so it
never runs during validation. Programs that reference unbound variables
(this, now) raise CelEvaluationException and are kept as before.
Adds a 20-field float message with only 'gt' set on each field. The
standard float.gt rule expands into five CEL programs; four of them
short-circuit to '' using only the bound rules variable when neither
'lt' nor 'lte' is set. This benchmark targets the residual-reduction
skip in RuleCache.
@snuderl
Copy link
Copy Markdown
Contributor Author

snuderl commented Apr 26, 2026

Benchmark results

Added a NumericRangeMessage benchmark targeting this optimization: 20 float fields with only gt set, so 4/5 of the float.gt rule expressions short-circuit to '' using only rules and become tautological.

A/B run on the same machine, JDK 21, JMH defaults (3×2s warmup, 5×2s measurement, 2 forks, AverageTime mode). Baseline is bufbuild/main + benchmark commit only; "after" adds the RuleCache change.

benchmark             metric  before          after           delta
validateNumericRange  time    28429.09 ns/op  17028.83 ns/op  -40.1%
validateNumericRange  alloc   97920.01 B/op   58560 B/op      -40.2%
validateManyUnruled   time      469.29 ns/op    453.31 ns/op   -3.4%
validateManyUnruled   alloc     1992    B/op   2044    B/op    +2.6%
validateRegexPattern  time     1504.76 ns/op   1545.29 ns/op   +2.7%
validateRegexPattern  alloc     3944    B/op   4072    B/op    +3.2%
validateRepeatedRule  time     9878.88 ns/op  10196.05 ns/op   +3.2%
validateRepeatedRule  alloc    40289.13 B/op   41600    B/op   +3.3%
validateSimple        time     1519.79 ns/op   1544.66 ns/op   +1.6%
validateSimple        alloc     3520    B/op   3640    B/op    +3.4%

The targeted benchmark drops ~40% in both time and per-op allocation (the four eliminated CEL programs are no longer evaluated and don't allocate intermediate result objects). Other benchmarks move within the typical run-to-run noise band for the default iteration budget — none of them have rule expressions that residualize to a constant, so no real change is expected there.

@jonbodner-buf
Copy link
Copy Markdown
Contributor

Are there other rules that expand in the same way as gt/lt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants