Skip to content

perf(common): avoid UTF-8 allocations in string comparator - #19414

Merged
voonhous merged 2 commits into
apache:masterfrom
cshuo:fix/utf8-string-comparator-allocation
Jul 31, 2026
Merged

perf(common): avoid UTF-8 allocations in string comparator#19414
voonhous merged 2 commits into
apache:masterfrom
cshuo:fix/utf8-string-comparator-allocation

Conversation

@cshuo

@cshuo cshuo commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

Closes #19409.

The UTF-8 string comparator introduced in #18941 encodes both operands on every comparison, allocating two temporary byte[] arrays before scanning them in unsigned byte order. Sorting invokes the comparator O(N log N) times, so large metadata record-index operations generate substantial short-lived allocations and GC pressure.

The same UTF-8 ordering can be derived directly from well-formed UTF-16 code units. This avoids encoding and allocation while preserving the HFile-compatible ordering required for non-ASCII and supplementary record keys.

Summary and Changelog

  • Port the allocation-free compareUtf8Strings algorithm from Google Firebase Firestore, with the source recorded next to the implementation.
  • Compare UTF-16 code units directly and handle supplementary characters specially instead of materializing UTF-8 byte arrays.
  • Add exhaustive tests over 2,380 valid Unicode strings, comparing every pair against the unsigned order of their encoded UTF-8 bytes.
  • Cover one-, two-, three-, and four-byte UTF-8 boundaries, BMP characters, supplementary characters, common prefixes, and two supplementary code points sharing a high surrogate.

Local benchmark

Environment: Java 11.0.27, macOS ARM64, G1 GC. Each microbenchmark result is the median of five measured trials and was repeated across three independent JVM forks. The benchmark harness is local-only and not included in this PR.

Comparator scenario Existing implementation This PR Speedup Existing allocation This PR allocation
32-character UUID-like ASCII 19.50 ns/op 2.69 ns/op 7.25x 96 B/op 0 B/op
BMP Unicode 134.95 ns/op 2.90 ns/op 46.5x 348.3 B/op 0 B/op
Supplementary Unicode 132.95 ns/op 3.52 ns/op 37.8x 348.5 B/op 0 B/op
ASCII with 64-character common prefix 60.62 ns/op 33.38 ns/op 1.82x 224 B/op 0 B/op

End-to-end Arrays.sort results:

Sort scenario Existing implementation This PR Speedup Existing allocation This PR allocation
250K UUID-like ASCII keys 159.2 ms 59.7 ms 2.67x 381.8 MiB 1.0 MiB
250K ASCII keys with 64-character common prefix 310.5 ms 219.7 ms 1.41x 889.7 MiB 1.0 MiB

The approximately 1 MiB remaining in the sort benchmark comes from the sorting implementation's temporary storage; the new comparator itself allocates 0 B/op.

Validation:

mvn -pl hudi-io -am \
  -Dtest=TestStringUtils \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -DskipITs -DskipSparkTests -DskipScalaTests \
  test

Result: 24 tests run, 0 failures, 0 errors, 0 skipped; Checkstyle reported 0 violations.

Impact

  • Functional impact: No ordering change for well-formed strings. UTF-8 byte ordering remains compatible with HFile ordering, including supplementary characters.
  • Performance impact: Removes two UTF-8 byte-array allocations per comparator invocation and improves both comparator throughput and end-to-end sorting performance.
  • Public API and storage format: No public API, configuration, or storage-format change.

Risk Level

Low. The implementation is ported from Firebase Firestore and is validated exhaustively against encoded unsigned UTF-8 byte order for valid Unicode strings. Like the Firestore implementation, it assumes well-formed UTF-16 input; malformed/unpaired surrogates are outside the expected Spark/Avro ingestion path.

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

@cshuo
cshuo marked this pull request as ready for review July 30, 2026 07:12
@cshuo
cshuo requested a review from voonhous July 30, 2026 07:12
@cshuo
cshuo force-pushed the fix/utf8-string-comparator-allocation branch from d9e0157 to 769684d Compare July 30, 2026 07:14
@cshuo cshuo changed the title perf: avoid UTF-8 allocations in string comparator perf(common): avoid UTF-8 allocations in string comparator Jul 30, 2026
@github-actions github-actions Bot added the size:S PR with lines of changes in (10, 100] label Jul 30, 2026

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR replaces the allocating UTF-8 string comparator with an allocation-free algorithm ported from Firestore that compares UTF-16 code units directly while preserving HFile-compatible unsigned UTF-8 byte ordering. I traced the surrogate handling (BMP vs supplementary, surrogate-vs-surrogate ordering, and the length tiebreak) against the encoded-byte order and it holds for well-formed UTF-16, and the exhaustive pairwise test is solid validation. No issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.43%. Comparing base (a284d6e) to head (59f4e8d).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19414      +/-   ##
============================================
+ Coverage     74.83%   75.43%   +0.60%     
- Complexity    32332    32600     +268     
============================================
  Files          2574     2574              
  Lines        142978   142998      +20     
  Branches      17527    17576      +49     
============================================
+ Hits         106992   107874     +882     
+ Misses        27921    27085     -836     
+ Partials       8065     8039      -26     
Components Coverage Δ
hudi-common 82.24% <ø> (-0.01%) ⬇️
hudi-client 81.72% <ø> (-0.08%) ⬇️
hudi-flink 84.03% <ø> (+5.39%) ⬆️
hudi-spark-datasource 67.82% <ø> (-0.51%) ⬇️
hudi-utilities 71.22% <ø> (+0.03%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.49% <ø> (ø)
hudi-sync 70.72% <ø> (+0.05%) ⬆️
hudi-io 79.60% <100.00%> (+0.02%) ⬆️
hudi-timeline-service 83.44% <ø> (-0.79%) ⬇️
hudi-cloud 64.00% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 49.22% <100.00%> (+1.44%) ⬆️
flink-integration-tests 48.83% <72.72%> (+1.19%) ⬆️
hadoop-mr-java-client 43.38% <90.90%> (-0.02%) ⬇️
integration-tests 13.63% <54.54%> (-0.01%) ⬇️
spark-client-hadoop-common 48.71% <90.90%> (+<0.01%) ⬆️
spark-java-tests 51.00% <90.90%> (-0.39%) ⬇️
spark-scala-tests 45.92% <72.72%> (-0.17%) ⬇️
utilities 36.65% <72.72%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../java/org/apache/hudi/common/util/StringUtils.java 77.57% <100.00%> (+0.41%) ⬆️

... and 76 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@voonhous voonhous left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a deeper pass on this since I reviewed #18941. The algorithm itself checks out: I brute forced it against the encoded byte order for well-formed input, mutation tested the new sweep, and confirmed no call site depends on the return magnitude. The comments below are about attribution, the malformed-input contract this rewrite quietly changes, and a few ways to sharpen the tests.

Comment thread hudi-io/src/main/java/org/apache/hudi/common/util/StringUtils.java
Comment thread hudi-io/src/main/java/org/apache/hudi/common/util/StringUtils.java
Comment thread hudi-io/src/main/java/org/apache/hudi/common/util/StringUtils.java Outdated
Comment thread hudi-io/src/main/java/org/apache/hudi/common/util/StringUtils.java
Comment thread hudi-io/src/test/java/org/apache/hudi/common/util/TestStringUtils.java Outdated
Comment thread hudi-io/src/test/java/org/apache/hudi/common/util/TestStringUtils.java Outdated
Comment thread hudi-io/src/test/java/org/apache/hudi/common/util/TestStringUtils.java Outdated
Comment thread hudi-io/src/main/java/org/apache/hudi/common/util/StringUtils.java Outdated
Preserve the null-rejection contract, document malformed surrogate behavior, add Firebase attribution, and strengthen the HFile-ordering tests.
@github-actions github-actions Bot added size:M PR with lines of changes in (100, 300] and removed size:S PR with lines of changes in (10, 100] labels Jul 30, 2026
@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR replaces the allocation-heavy UTF-8 string comparator with an allocation-free port of Firestore's compareUtf8Strings that derives the same ordering directly from UTF-16 code units, with exhaustive tests validating against the production HFile key comparator. I traced the supplementary-character special case and boundary interactions and they preserve the HFile-compatible unsigned UTF-8 byte order; the null contract is also preserved. The main points (unpaired-surrogate divergence, (null,null) handling, LICENSE attribution, test-oracle independence) were already raised and addressed in prior rounds. No new issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@voonhous voonhous left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@voonhous
voonhous merged commit 377fc04 into apache:master Jul 31, 2026
75 checks passed
voonhous pushed a commit that referenced this pull request Aug 6, 2026
* perf(common): avoid UTF-8 allocations in string comparator

* fix(common): address UTF-8 comparator review feedback

Preserve the null-rejection contract, document malformed surrogate behavior, add Firebase attribution, and strengthen the HFile-ordering tests.

(cherry picked from commit 377fc04)
voonhous pushed a commit that referenced this pull request Aug 7, 2026
* perf(common): avoid UTF-8 allocations in string comparator

* fix(common): address UTF-8 comparator review feedback

Preserve the null-rejection contract, document malformed surrogate behavior, add Firebase attribution, and strengthen the HFile-ordering tests.

(cherry picked from commit 377fc04)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M PR with lines of changes in (100, 300]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize UTF-8 string comparison to avoid per-comparison byte array allocations

6 participants