perf(arrow-cast): optimize parsing of decimals from strings - #10668
perf(arrow-cast): optimize parsing of decimals from strings#10668neilconway wants to merge 2 commits into
Conversation
Rewrite parse_string_to_decimal_native to accumulate digits in u64 chunks that are folded into the target native type with checked arithmetic (one wide multiply per 19 digits), instead of splitting the string and round-tripping through i256 and intermediate allocations. String-to-decimal128 casts are ~8x faster and decimal256 casts ~6.5x faster; the parser microbenchmarks improve 64-92% across all cases. The checked arithmetic also fixes two bugs in the previous implementation: Decimal256 values whose unscaled magnitude exceeds the i256 range could silently wrap to an arbitrary in-range value instead of reporting overflow, and inputs with more than 76 fractional digits were rejected even when the scaled value fits the target type. Also use PrimitiveBuilder instead of an intermediate Vec and unsafe from_trusted_len_iter in the strict (safe=false) cast path. The safe path keeps the unsafe trusted-len construction: it measures 15-19% faster than a PrimitiveBuilder equivalent, and its justification comment now records that. Also document the accepted syntax and rounding behavior, and add benchmark coverage for string-to-decimal parsing.
|
i wonder if this will fix/affect some of the other issues we have open for decimal parsing/casting, see: edit: seems like none, those other issues are for a different decimal parsing function |
Yeah, I believe those issues are distinct. Although I think it might make sense to unify some of the code here, both to simplify the implementation and to address some inconsistencies (e.g., rounding vs. truncating when given more decimal digits than required for the target type's scale). I can take a look at some of that once this PR lands. |
8012d45 to
7f9a781
Compare
|
run benchmark parse_decimal |
Which issue does this PR close?
Decimal256#10665Rationale for this change
The previous implementation of
parse_string_to_decimal_nativeused a string-manipulation approach: it trimmed the input string, split it based on".", parsed both halves withi256::from_string, converted the result back to a string withformat!, and parsed that again withi256::from_string.Instead, we use a single pass over the input bytes. A simple state machine walks over the input digits, accumulating a running sum. This avoids all of the string manipulation and heap allocation of the previous approach.
We further optimize this by accumulating the running sum in a
u64, and then periodically folding that partial value into the runningdecimalvalue (we do this often enough that there is no risk of overflowing theu64). That trades a bit of redundant computation for doing more work inu64and less work indecimal; based on benchmarking, this is a clear win.Finally, we don't need to accumulate digits from the suffix of the string. Values beyond the target type's scale don't contribute to the result value; only the first such digit influences rounding behavior.
This new approach also fixes two correctness bugs (#10664 and #10665) in the previous implementation.
Benchmarks (M4 Max)
Parser microbenchmarks (arrow-cast/benches/parse_decimal.rs):
End-to-end cast kernel (arrow/benches/cast_kernels.rs, 512-row string array, safe mode):
What changes are included in this PR?
parse_string_to_decimal_nativeas described aboveVec+unsafewithPrimitiveBuilder, which saves an allocationAre these changes tested?
Yes; existing tests pass, and new tests have been added. I also checked the new implementation against a naive oracle built using the num-bigint crate; the new implementation was consistent with
num-bigintfor 120M randomly generated inputs.Are there any user-facing changes?
No, aside from fixed bugs.
AI usage
Iterated with the help of Claude Fable; I reviewed and understand the resulting code.