Skip to content

Trim allocations on the per-constraint and per-violation paths - #274

Open
jungm wants to merge 4 commits into
perf-2-lookupsfrom
perf-3-allocations
Open

jungm wants to merge 4 commits into
perf-2-lookupsfrom
perf-3-allocations

Conversation

@jungm

@jungm jungm commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Trims allocations on the per-constraint and per-violation paths.

 Frame.validate(constraint)
   ConstraintValidatorContextImpl.violations
-    Lazy<HashSet>
+    ArrayList(2) with a containment check
   createViolation
-    context path materialized, then copied again
+    derived once from the parent context's path
-    Arrays.deepHashCode(new Object[] {...})
+    field-by-field hash
 composing constraints of a @ReportAsSingleViolation constraint
-  build violations (path, message, hash) and discard them
+  record only whether they failed
 BeanFrame property frames, cascading
-  traversableResolver.isReachable/isCascadable (+ path copies)
+  skipped when exactly DefaultTraversableResolver without JPA
 PathImpl
-  LinkedList<NodeImpl>
+  ArrayList<NodeImpl>

Evidence

  • Before / After: benchmarks

Numbers are JMH throughput (-p provider=bval -f 2 -wi 2 -i 3, 1 s iterations, -prof gc, JDK 25), measured against the PR below this one. The machine was busy, so throughput carries wide error bars; the bytes allocated per operation (B/op) are deterministic.

Benchmark Before After Change B/op before → after
composedInvalid (ops/ms) 669 ± 5% 1,192 ± 9% 1.78× 4,712 → 2,532
wideBeanInvalid (ops/ms) 68.1 ± 44% 80.6 ± 150% 1.18× 28,157 → 20,600
deepGraph (ops/ms) 87.9 ± 68% 97.0 ± 46% 1.10× 31,289 → 23,154
mixedDrivers (ops/ms) 536 ± 27% 507 ± 62% 0.95× 5,422 → 4,332
All benchmarks
Benchmark Before After Change B/op before → after
cascadedCycles (ops/ms) 2.42 ± 14% 0.95 ± 66% 0.39× 1,202,427 → 1,098,127
cascadedList (ops/ms) 0.32 ± 33% 0.29 ± 82% 0.92× 5,778,430 → 5,269,994
classLevelInvalid (ops/ms) 516 ± 61% 729 ± 96% 1.41× 3,896 → 3,256
classLevelValid (ops/ms) 978 ± 68% 1,332 ± 11% 1.36× 3,040 → 2,592
composedInvalid (ops/ms) 669 ± 5% 1,192 ± 9% 1.78× 4,712 → 2,532
composedValid (ops/ms) 1,042 ± 5% 951 ± 38% 0.91× 2,976 → 2,152
containerElements (ops/ms) 1.84 ± 19% 1.92 ± 9% 1.04× 1,623,248 → 1,371,279
deepGraph (ops/ms) 87.9 ± 68% 97.0 ± 46% 1.10× 31,289 → 23,154
elMessage (ops/ms) 657 ± 99% 836 ± 45% 1.27× 3,840 → 3,412
executableParameters (ops/ms) 532 ± 10% 422 ± 87% 0.79× 5,761 → 5,423
executableReturnValue (ops/ms) 496 ± 12% 338 ± 46% 0.68× 6,255 → 5,460
explicitGroup (ops/ms) 734 ± 11% 703 ± 32% 0.96× 4,930 → 4,084
mixedDrivers (ops/ms) 536 ± 27% 507 ± 62% 0.95× 5,422 → 4,332
validateProperty (ops/ms) 2,026 ± 4% 1,615 ± 27% 0.80× 1,795 → 1,764
validateValue (ops/ms) 2,479 ± 45% 3,477 ± 10% 1.40× 1,070 → 1,088
wideBeanInvalid (ops/ms) 68.1 ± 44% 80.6 ± 150% 1.18× 28,157 → 20,600
wideBeanValid (ops/ms) 72.0 ± 92% 120 ± 68% 1.67× 17,308 → 13,737
Jsr303Benchmark.bvalBootstrapAndValidate (ops/s) 1,652 ± 61% 1,523 ± 43% 0.92× 250,395 → 251,542
Jsr303Benchmark.bvalBuildFactory (ops/s) 3,164 ± 43% 7,148 ± 34% 2.26× 89,026 → 88,164
Jsr303Benchmark.bvalConstraintsFailure (ops/s) 471,733 ± 46% 412,829 ± 89% 0.88× 5,000 → 4,236
Jsr303Benchmark.bvalConstraintsFailureReuseValidator (ops/s) 514,364 ± 60% 458,567 ± 38% 0.89× 5,048 → 4,248
Jsr303Benchmark.bvalConstraintsSuccess (ops/s) 826,895 ± 91% 930,692 ± 50% 1.13× 3,068 → 3,032
Jsr303Benchmark.bvalConstraintsSuccessReuseValidator (ops/s) 1,133,176 ± 70% 575,500 ± 101% 0.51× 3,080 → 2,900
Jsr303Benchmark.bvalNoConstraints (ops/s) 159,318,160 ± 10% 132,245,313 ± 93% 0.83× 0 → 0
Jsr303Benchmark.bvalNoConstraintsReuseValidator (ops/s) 147,841,169 ± 67% 160,105,673 ± 54% 1.08× 0 → 0

Several throughput rows (e.g. cascadedCycles ±66%) are within noise even though their allocation went down.

  • Tests: bval-jsr unit tests: 255 run, 0 failures · TCK: 1070 run, 0 failures, 1 skipped

Merge Danger

Door: two-way

Blast Radius: violation paths

  • Violation paths and hash codes are built differently; their equality is unchanged, and the TCK checks paths extensively.
  • TraversableResolver: custom resolvers, subclasses of DefaultTraversableResolver and JPA environments are still consulted. Only the exact default class without JPA, which always answers yes, is skipped.

jungm added 4 commits October 5, 2026 13:32
…rargs

- Back PathImpl with an ArrayList, which copies and indexes faster than a
  LinkedList for the short paths validation builds.
- Compute violation and node hash codes without allocating a varargs
  array.
Build the private path copy handed to a violation from the parent's path
instead of first materializing the context's own path and copying it
again.
Without JPA, DefaultTraversableResolver reports every property as
reachable and cascadable, so calling it only costs the paths built for
it. The exact-class check keeps subclasses honored.
The violations of constraints composing one annotated with
@ReportAsSingleViolation are discarded, so only whether they failed
matters. Recognize the discarding sink and skip building their paths,
messages and hash codes, and evaluate composing constraints without a
stream.
@jungm
jungm force-pushed the perf-3-allocations branch from 4f9103a to 780244e Compare October 5, 2026 11:43
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.

1 participant