Skip to content

Reuse UTF-8 scratch for DataTable dictionary decoding - #19535

Open
xiangfu0 wants to merge 1 commit into
apache:masterfrom
xiangfu0:xiangfu0/java-datatable-dictionary-decode
Open

Reuse UTF-8 scratch for DataTable dictionary decoding#19535
xiangfu0 wants to merge 1 commit into
apache:masterfrom
xiangfu0:xiangfu0/java-datatable-dictionary-decode

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Decoding a V4 DataTable allocates a temporary UTF-8 byte array for each string dictionary entry. Reuse one method-local scratch array, growing to the required length only when necessary. Each returned String owns its contents. Empty entries, wire encoding, the standalone string decoder and malformed-input behavior remain unchanged. The change includes regression tests for buffer state, String ownership and complete V4 messages.

Reproduction and benchmark

Measure DataTableFactory.getDataTable on already encoded V4 messages, including source-buffer duplication and complete decoding, using heap, direct and read-only heap buffers. Input preparation/encoding, SQL parsing, scans, aggregation, JNI and transport are excluded. Each fork validates all decoded rows, schema, metadata and source-buffer position before timing.

Workloads: NUMERIC_5000 has 5,000 INT/LONG rows and no string dictionary; STRING_ONE has one entry; ASCII_5000 has 5,000 unique 64-byte strings; UTF8_5000 uses Japanese, supplementary-plane and accented characters; OUTLIER_5000 starts with one 262,152-byte string followed by 4,999 64-byte strings; GROWING_512 has 512 distinct strings increasing from 32 to 4,120 bytes in 8-byte steps, requiring a larger scratch array for every entry.

Measured against upstream 5771d6acea60cd72738116835111cf7c49965373 on an Apple M2 Max (12 cores, 32 GiB), macOS ARM64 and Temurin JDK 25. Three alternating AB/BA/AB pairs, one fresh JMH fork per case/arm, one thread, 512 MiB heap, three 500 ms warmups and five 500 ms measurements, with GC profiling. Ordinary shared-host activity remained; no task builds or other task benchmarks ran during timing.

Both arms used identical measurement code and a pinned runtime; only the DataTableImplV4 classes differed. Filename-sorted classpaths, runtime manifests and source hashes were checked before/after each arm. This is a controlled class comparison, not a packaged deployment comparison.

Results

Times and allocations are medians of three arm means; ratios are medians of paired baseline/candidate times, not ratios of displayed medians. Values above 1 favor the candidate; ranges retain all three pairs.

Scenario Buffer Baseline µs/op Candidate µs/op Paired ratio Pair range Allocation B/op before → after Reduction
ASCII_5000 DIRECT 88.281 86.486 1.033× 0.988–1.110 980953 → 581033 40.8%
ASCII_5000 HEAP 87.289 85.614 1.028× 1.016–1.040 980945 → 581025 40.8%
ASCII_5000 READ_ONLY 88.503 85.481 1.029× 1.029–1.035 980945 → 581025 40.8%
GROWING_512 DIRECT 98.151 98.561 0.996× 0.975–1.007 2161593 → 2161593 0.0%
GROWING_512 HEAP 97.623 97.544 1.001× 0.992–1.013 2161585 → 2161585 0.0%
GROWING_512 READ_ONLY 98.533 98.186 0.991× 0.988–1.016 2161585 → 2161585 0.0%
NUMERIC_5000 DIRECT 2.309 2.297 1.004× 0.999–1.034 60952 → 60952 0.0%
NUMERIC_5000 HEAP 2.339 2.365 1.003× 0.989–1.013 60944 → 60944 0.0%
NUMERIC_5000 READ_ONLY 2.379 2.295 1.006× 1.005–1.048 60944 → 60944 0.0%
OUTLIER_5000 DIRECT 112.614 109.849 1.025× 1.003–1.103 1505130 → 1105210 26.6%
OUTLIER_5000 HEAP 111.067 113.448 0.968× 0.959–1.006 1505122 → 1105202 26.6%
OUTLIER_5000 READ_ONLY 111.548 114.548 0.974× 0.951–0.979 1505122 → 1105202 26.6%
STRING_ONE DIRECT 0.154 0.150 1.021× 1.001–1.036 1152 → 1152 0.0%
STRING_ONE HEAP 0.146 0.146 1.000× 0.984–1.001 1144 → 1144 0.0%
STRING_ONE READ_ONLY 0.153 0.150 0.999× 0.968–1.021 1144 → 1144 0.0%
UTF8_5000 DIRECT 388.269 376.446 1.033× 1.022–1.046 2540957 → 2061053 18.9%
UTF8_5000 HEAP 388.805 372.515 1.047× 1.039–1.053 2540949 → 2061045 18.9%
UTF8_5000 READ_ONLY 389.744 374.906 1.040× 1.025–1.043 2540949 → 2061045 18.9%

ASCII/UTF-8 dictionary medians show modest latency gains and 40.8%/18.9% less allocation. Outlier-first dictionaries allocate 26.6% less, but the original heap/read-only median paired slowdowns were about 3.3%/2.7%. Growing-length and single-entry controls also include latency non-wins; numeric, single-entry and growing-length allocation is unchanged. Allocation here means bytes allocated per operation, not peak heap or RSS.

Separate longer outlier follow-up

The original read-only pair 2 was 109.718 → 115.326 µs/op, a +5.111% slowdown, flagged by benchmark review. Its original three-pair median paired slowdown was 2.689%. These findings remain part of the assessment.

Three fresh AB/BA/AB pairs used unchanged source/runtime, five 1-second warmups and ten 1-second measurements across all three outlier buffer types; other settings were unchanged. Source/runtime checks passed before/after each arm. These results are separate from the original protocol and do not replace it.

OUTLIER_5000 buffer Baseline µs/op Candidate µs/op Median paired ratio All pair ratios Allocation reduction
DIRECT 116.079 108.123 1.062× 1.090, 1.062, 1.031 26.6%
HEAP 109.871 108.674 1.004× 1.004, 0.996, 1.011 26.6%
READ_ONLY 110.133 108.883 1.009× 1.006, 1.012, 1.009 26.6%

The longer protocol did not reproduce the >5% regression or explain its cause; one longer heap pair remained slower. The allocation reduction repeated. The original tradeoff remains: these shared-host component measurements establish neither universal latency improvement, query throughput, production capacity nor 10× performance.

Validation

114 tests passed: DataTableStringDictionaryTest (48), DataTableDictionarySerDeTest (8), DataTableSerDeTest (9), SelectionOperatorServiceTest (5) and InterSegmentGroupBySingleValueQueriesTest (44). Coverage includes empty entries/dictionaries, Unicode, malformed UTF-8 replacement, negative counts/lengths, truncation, buffer position/limit/mark, independence of earlier Strings and complete V4 scalar/MV/null/metadata/exception messages across heap/direct, read-only and sliced buffers. Independent correctness and testing source reviews found no actionable issues; the benchmark review's original slowdown remains disclosed above.

Recorded local checks passed: normal compilation/tests, packaging, Spotless, Checkstyle, license format/check and Apache RAT. Scoped lint retained the baseline’s two constructor this-escape warnings and four dependency-manifest path warnings, with no new source warnings; it excluded the ASF-header dangling-doc category. A prior same-base full-reactor Xlint run failed in unchanged Zstandard code on a missing JetBrains annotation and was not repeated.

@xiangfu0 xiangfu0 added the performance Related to performance optimization label Sep 12, 2026
@codecov-commenter

codecov-commenter commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.69%. Comparing base (5771d6a) to head (a42ffd9).

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19535      +/-   ##
============================================
- Coverage     67.72%   67.69%   -0.03%     
  Complexity     1450     1450              
============================================
  Files          3490     3490              
  Lines        225032   225042      +10     
  Branches      35527    35530       +3     
============================================
- Hits         152393   152346      -47     
- Misses        60612    60664      +52     
- Partials      12027    12032       +5     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.69% <100.00%> (-0.03%) ⬇️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.69% <100.00%> (-0.03%) ⬇️
unittests 67.69% <100.00%> (-0.03%) ⬇️
unittests1 57.81% <100.00%> (+<0.01%) ⬆️
unittests2 39.46% <0.00%> (-0.03%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Decoding a string dictionary allocates temporary UTF-8 storage for every entry. Reuse method-local scratch while preserving each returned String's ownership and existing buffer and error behavior.
@xiangfu0
xiangfu0 force-pushed the xiangfu0/java-datatable-dictionary-decode branch from 562411d to a42ffd9 Compare September 12, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Related to performance optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants