Skip to content

feat: reduce allocations in Spark Decimal access - #9842

Merged
robert3005 merged 5 commits into
vortex-data:developfrom
xiaoh1024:exp/decimal-accessor-pr
Sep 24, 2026
Merged

robert3005 merged 5 commits into
vortex-data:developfrom
xiaoh1024:exp/decimal-accessor-pr

Conversation

@xiaoh1024

Copy link
Copy Markdown
Contributor

Summary

Closes #9837. See the issue for the motivation, design, benchmark results, and decimal-specific validation.

Changes

  • Add SmallDecimalAccessor for source precision 1–18, preserving full-width fallback and Spark's decimal conversion semantics.
  • Add regression tests for decimal decoding, conversions, and returned-value independence.

AI assistance: AI assistance was used for implementation, tests, validation tooling, and this description, including translation from Chinese.

Read small-precision Decimal128 values from native long words, retaining
the full-width Arrow conversion when the integer cannot fit in a long.
Handle both native word orders and use the source scale when constructing
BigDecimal, leaving requested rescaling and overflow checks to Spark.
Preserve Spark's expanded Decimal representation for checked integer casts.

Add tests for decimal values, rescaling, malformed buffers, both word
orders, slices, object independence, negative scales and checked casts.

Validated on Spark 3.5.9/Scala 2.12 and Spark 4.1.2/Scala 2.13, with 26
targeted tests per version plus Javadoc and test formatting checks.
AI-assisted implementation and tests.

Signed-off-by: Peifeng Li <lipeifeng@xiaohongshu.com>
@codspeed

codspeed Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 78.89%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ 5 benchmarks measured no execution time

Nothing ran under measurement, usually because the compiler removed the code under test. These results are not comparable, so they count as unchanged.

Preventing compiler optimizations

⚡ 1 improved benchmark
✅ 2219 untouched benchmarks
⏩ 329 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
⚡ Simulation take_fsl_random[128, 10] 59.1 µs 33 µs +78.89%
⚠️ Simulation take_fsl_u32_random[256, 10] < 1 ns < 1 ns N/A
⚠️ Simulation fixed_16_advancing_ptr_safe[100] < 1 ns < 1 ns N/A
⚠️ Simulation preverify_advancing_ptr_unchecked[1000] < 1 ns < 1 ns N/A
⚠️ Simulation preverify_advancing_ptr_unchecked[10000] < 1 ns < 1 ns N/A
⚠️ Simulation bench_compare_sliced_dict_primitive[(3333, 10000)] 77.5 µs < 1 ns N/A

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing xiaoh1024:exp/decimal-accessor-pr (614ad61) with develop (1eb5b43)

Open in CodSpeed

Footnotes

  1. 329 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@robert3005 robert3005 added the changelog/performance A performance improvement label Sep 14, 2026
@xiaoh1024

Copy link
Copy Markdown
Contributor Author

Hi @robert3005, would you have a chance to review this when you have time?

@robert3005 robert3005 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.

Some nits, I would like to understand why we need BigDecimal.valueOf

if (high == (unscaled >> 63)) {
// Decode with the source scale; Spark performs the requested rescaling and overflow checks.
// Keep the expanded Decimal representation for Spark's checked integer casts.
return Decimal.apply(BigDecimal.valueOf(unscaled, accessor.getScale()), precision, scale);

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.

can we use Decimal.apply(unscaled, p, s) ? What does BigDecimal.valueOf change here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I initially tried Decimal.apply(unscaled, p, s), but it changes the internal representation and some downstream conversion behavior.

In Spark 3.5.9 and 4.1.2, for example, 127.999999999999999 throws an overflow exception on roundToByte() through the existing BigDecimal-based path, while the compact Decimal created by the long overload returns 127. The checked integer conversion test covers this difference.

Using BigDecimal.valueOf(unscaled, accessor.getScale()) preserves that behavior. It also reconstructs the value with the source scale, before Spark applies the requested precision and scale. Directly using the requested scale with the unscaled integer would change the value when the scales differ.

We still avoid the temporary byte array and BigInteger created by Arrow’s getObject(), while retaining the existing Spark Decimal behavior.

Initialize both UnsafeRowWriter instances before writing and verify that each row reads back the expected Decimal. Add braces around the small Decimal null check.

Signed-off-by: Peifeng Li <lipeifeng@xiaohongshu.com>
@robert3005
robert3005 enabled auto-merge (squash) September 23, 2026 21:53
@robert3005
robert3005 merged commit 2ab66d7 into vortex-data:develop Sep 24, 2026
122 of 123 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/performance A performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce allocations when reading small-precision decimals in the Spark connector

2 participants