Use JDK intrinsics for Binary equality/comparison/hashing - #3782
Open
abstractdog wants to merge 1 commit into
Open
Use JDK intrinsics for Binary equality/comparison/hashing#3782abstractdog wants to merge 1 commit into
abstractdog wants to merge 1 commit into
Conversation
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.
abstractdog
force-pushed
the
binary-intrinsics
branch
from
September 7, 2026 13:12
7abceee to
7a77f94
Compare
Contributor
|
Thanks for raising this PR @abstractdog, very interesting. Any way to verify using tests that we don't change hashes? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rationale for this change
Binary#equals,Binary#lexicographicCompare, andBinary#hashCodesit 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 ofArrays.equals/Arrays.compareUnsigned/Arrays.hashCode/ByteBuffer.mismatchroute throughArraysSupport.vectorizedMismatch, an@IntrinsicCandidatehelper HotSpot substitutes with a SIMD byte-scan (SSE / AVX2 / NEON).What changes are included in this PR?
Binary.javaforequals,lexicographicCompare, andhashCodewith the intrinsic-backedArrays.*/ByteBuffer.mismatchAPIs across all fourBinaryshape pairs (byte[]/byte[],byte[]/ByteBuffer,ByteBuffer/ByteBuffer).parquet-benchmarks/BinaryComparisonBenchmarkcoveringequals/compareTo/hashCodeat length regimes 8, 64, and 512, both worst-case (full match) and average-case (mid-length mismatch).Follows the same technique already accepted for
DeltaByteArrayWriterin #3465. Complementary to theBinary#hashCodecaching in #3566 — caching reduces call frequency, intrinsics speed up the calls that remain plusequals/compareTowhich caching does not address.Are these changes tested?
Yes. Covered by the existing
Binaryunit tests (equality, comparison, hashing across all shape pairs) plus the new JMH suite.compareTosemantics are preserved —Arrays.compareUnsignedmatches 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):
equalsMatch_bytesBytesequalsMatch_bytesBytesequalsMismatch_bytesBytescompareTo_bytesBytescompareTo_bytesBytesequalsMismatch_bytesBufWins 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.
hashCodeis unchanged in JDK 17 numbers — the31*h+bpolynomial has a serial cross-iteration dependency; HotSpot only gained a lane-split intrinsic (ArraysSupport.vectorizedHashCode) in JDK 21, so the newArrays.hashCodecall 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