Skip to content

Use JDK intrinsics for Binary equality/comparison/hashing - #3782

Open
abstractdog wants to merge 1 commit into
apache:masterfrom
abstractdog:binary-intrinsics
Open

Use JDK intrinsics for Binary equality/comparison/hashing#3782
abstractdog wants to merge 1 commit into
apache:masterfrom
abstractdog:binary-intrinsics

Conversation

@abstractdog

@abstractdogabstractdog commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

Binary#equals, Binary#lexicographicCompare, and Binary#hashCode sit on hot paths for statistics min/max maintenance, dictionary hash-map probing, predicate evaluation, and bloom-filter build — so any per-byte overhead is amortized across every reader and writer. The current implementations use hand-rolled scalar byte loops; the JDK 9+ range overloads of Arrays.equals / Arrays.compareUnsigned / Arrays.hashCode / ByteBuffer.mismatch route through ArraysSupport.vectorizedMismatch, an @IntrinsicCandidate helper HotSpot substitutes with a SIMD byte-scan (SSE / AVX2 / NEON).

What changes are included in this PR?

  • Replace the scalar loops in Binary.java for equals, lexicographicCompare, and hashCode with the intrinsic-backed Arrays.* / ByteBuffer.mismatch APIs across all four Binary shape pairs (byte[]/byte[], byte[]/ByteBuffer, ByteBuffer/ByteBuffer).
  • Add a JMH suite parquet-benchmarks/BinaryComparisonBenchmark covering equals / compareTo / hashCode at length regimes 8, 64, and 512, both worst-case (full match) and average-case (mid-length mismatch).

Follows the same technique already accepted for DeltaByteArrayWriter in #3465. Complementary to the Binary#hashCode caching in #3566 — caching reduces call frequency, intrinsics speed up the calls that remain plus equals / compareTo which caching does not address.

Are these changes tested?

Yes. Covered by the existing Binary unit tests (equality, comparison, hashing across all shape pairs) plus the new JMH suite. compareTo semantics are preserved — Arrays.compareUnsigned matches the contract of the previous loop (unsigned byte order, shorter-runs-first on prefix match).

Are there any user-facing changes?

No API changes. Behavior is identical; only the implementation is faster.

Benchmark — JDK 17, 1 fork, 3×1s warmup, 5×1s measurement, throughput (ops/s):

benchlenbeforeafterspeedup
equalsMatch_bytesBytes5129.1 M39.1 M4.30×
equalsMatch_bytesBytes6441.4 M160.9 M3.89×
equalsMismatch_bytesBytes51214.6 M55.5 M3.79×
compareTo_bytesBytes51215.1 M58.8 M3.91×
compareTo_bytesBytes6459.0 M199.4 M3.38×
equalsMismatch_bytesBuf6463.8 M190.2 M2.98×

Wins scale with length — at len=512 the SIMD lane count amortizes fully (≈4×); at len=64 it's ≈3–4×; len=8 sees only 1.1–1.7× because there's barely enough work for one SIMD lane. hashCode is unchanged in JDK 17 numbers — the 31*h+b polynomial has a serial cross-iteration dependency; HotSpot only gained a lane-split intrinsic (ArraysSupport.vectorizedHashCode) in JDK 21, so the new Arrays.hashCode call is equivalent on 17 and picks up the vectorization automatically on 21+.

For further reference on intrinsics, see:

https://github.com/openjdk/jdk17u/blob/1ed3717f5a226981a1ebeff70dcafadc74569190/src/java.base/share/classes/java/util/Arrays.java#L2582

https://github.com/openjdk/jdk17u/blob/1ed3717f5a226981a1ebeff70dcafadc74569190/src/hotspot/share/opto/library_call.cpp#L5393

Replace the hand-rolled scalar byte loops in Binary#equals,
Binary#lexicographicCompare, and Binary#hashCode with the JDK 9+
Arrays.equals(byte[],int,int,byte[],int,int),
Arrays.compareUnsigned(byte[],int,int,byte[],int,int), Arrays.hashCode,
and ByteBuffer.mismatch APIs. These range overloads route through
ArraysSupport.vectorizedMismatch, an @IntrinsicCandidate helper HotSpot
substitutes with a SIMD byte-scan (SSE / AVX2 / NEON on modern hardware).
Measured on this project's BinaryComparisonBenchmark (JDK 17, 1 fork,
3x1s warmup, 5x1s measurement, throughput):
equalsMatch_bytesBytes len=512 9.1M -> 39.1M ops/s (4.30x)
equalsMatch_bytesBytes len= 64 41.4M -> 160.9M ops/s (3.89x)
equalsMismatch_bytesBytes len=512 14.6M -> 55.5M ops/s (3.79x)
compareTo_bytesBytes len=512 15.1M -> 58.8M ops/s (3.91x)
compareTo_bytesBytes len= 64 59.0M -> 199.4M ops/s (3.38x)
equalsMismatch_bytesBuf len= 64 63.8M -> 190.2M ops/s (2.98x)
Short values (len=8) see only 1.1-1.7x because there's barely enough
work for one SIMD lane.
hashCode is separate. The 31*h+b polynomial has a serial dependency
across iterations, so SIMD needs a lane-split algebraic trick that
HotSpot only gained in JDK 21 (via ArraysSupport.vectorizedHashCode,
which is @IntrinsicCandidate). On JDK 17 -- this project's target --
Arrays.hashCode is a plain scalar loop and shows no measured speedup
here. The call is kept for forward compatibility: JDK 21+ runtimes pick
up the vectorized intrinsic silently; a bespoke loop would stay stuck
at scalar.
These primitives sit on Binary equality, comparison and hash-map
probing, which is the hot code path for statistics min/max maintenance,
dictionary probing, predicate evaluation, and bloom-filter build --
i.e. broadly amortized across reader and writer.
Semantics are preserved bit-for-bit:
- hashCode(byte[]) uses the same 31*h + b polynomial (Arrays.hashCode
on full arrays; the polynomial expanded for slices).
- equals is bytewise identity.
- lexicographicCompare is unsigned bytewise with shorter-first tie-break
on prefix match, matching Arrays.compareUnsigned's contract.
The change is JIT-friendly across the four Binary/Binary shape pairs
(byte[]/byte[], byte[]/ByteBuffer, ByteBuffer/ByteBuffer) and for
heap-backed ByteBuffers unwraps to the intrinsic byte[] path via
buffer.array().
Follows the same technique already accepted for DeltaByteArrayWriter in
PR apache#3465. Complementary to the Binary hashCode
caching in the open dictionary-optimization PR
apache#3566: caching reduces call frequency; intrinsics
speed up the calls that remain plus equals/compareTo which caching does
not address.
Includes a JMH micro-benchmark
(parquet-benchmarks/BinaryComparisonBenchmark) covering equals /
compareTo / hashCode across length regimes 8, 64 and 512 for the three
Binary shape combinations, with both worst-case (full match) and
average-case (mid-length mismatch) inputs.
@Fokko

Fokko commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for raising this PR @abstractdog, very interesting. Any way to verify using tests that we don't change hashes?

Sign up for freeto 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

@abstractdog@Fokko