perf(arrow-data): Format decimal values without intermediate allocs - #11002
Open
neilconway wants to merge 2 commits into
Open
perf(arrow-data): Format decimal values without intermediate allocs#11002neilconway wants to merge 2 commits into
neilconway wants to merge 2 commits into
Conversation
Formatting a decimal value allocated two heap strings: the unscaled
value was converted with to_string, and format_decimal_str then built a
second String with the decimal point inserted.
arrow-data gains three functions that share one implementation of the
formatting rules:
- write_decimal_str (private) takes the digits of an unscaled value,
with an optional sign, and writes them to a fmt::Write with the decimal
point inserted `scale` digits from the right, adding leading zeros as
needed.
- write_decimal is for callers that have an output to write to, such as
ArrayFormatter: it formats the native value into a stack buffer of
digits and passes them to write_decimal_str.
- format_decimal is for callers that need an owned String, such as
DecimalType::format_decimal: it formats the digits the same way, then
allocates a String of the required capacity and writes into it through
write_decimal_str.
write_decimal and format_decimal accept the native value of a decimal
type, i32, i64, i128 or i256, named by the new sealed DecimalNativeType
trait. DecimalType already names these types, but it lives in
arrow-array, which depends on arrow-data, so it cannot be used here.
The existing entry points are now implemented on top of write_decimal
and format_decimal. This reduces the number of heap allocations in
ArrayFormatter from 2 -> 0 (which improves performance writing CSV and
JSON output), and from 2 -> 1 for PrimitiveArray::value_as_string.
format_decimal benchmarks, M4 Max:
case before after change
decimal32 (9, 2) 9 digits 593.45 220.13 -62.9%
decimal64 (18, 6) 18 digits 726.44 243.84 -66.4%
decimal128 (10, 2) 1 digit 486.70 190.16 -60.9%
decimal128 (10, 2) 5 digits 480.75 209.84 -56.4%
decimal128 (38, 10) 38 digits 776.75 303.65 -60.9%
decimal256 (76, 10) 38 digits 1751.70 1070.30 -38.9%
decimal256 (76, 10) 76 digits 2097.40 1507.80 -28.1%
The relative improvement for decimal256 is smaller because that case had
additional overhead; that has been addressed in a concurrent
PR (apache#11000).
Contributor
|
run benchmark format_decimal |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing neilc/decimal-display-no-alloc (acca3ef) to 738e69c (merge-base) diff Run configurationrun benchmark format_decimalCPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
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.
Which issue does this PR close?
Rationale for this change
Formatting a decimal value allocated two heap strings: the unscaled value was converted with
to_string, andformat_decimal_strthen built a second String with the decimal point inserted.arrow-data gains three functions that share one implementation of the formatting rules:
write_decimal_str(private) takes the digits of an unscaled value, with an optional sign, and writes them to a fmt::Write with the decimal point insertedscaledigits from the right, adding leading zeros as needed.write_decimalis for callers that have an output to write to, such asArrayFormatter: it formats the native value into a stack buffer of digits and passes them to write_decimal_str.format_decimalis for callers that need an owned String, such asDecimalType::format_decimal: it formats the digits the same way, then allocates a String of the required capacity and writes into it throughwrite_decimal_str.write_decimalandformat_decimalaccept the native value of a decimal type, i32, i64, i128 or i256, named by the new sealedDecimalNativeTypetrait.DecimalTypealready names these types, but it lives in arrow-array, which depends on arrow-data, so it cannot be used here.The existing entry points are now implemented on top of write_decimal and format_decimal. This reduces the number of heap allocations in
ArrayFormatterfrom 2 -> 0 (which improves performance writing CSV and JSON output), and from 2 -> 1 forPrimitiveArray::value_as_string.format_decimal benchmarks, M4 Max:
The relative improvement for decimal256 is smaller because that case had additional overhead; that has been addressed in a concurrent PR (#11000).
What changes are included in this PR?
See above.
Are these changes tested?
Yes; existing tests pass, new test added.
Are there any user-facing changes?
No. Decimal output format is unchanged.
AI usage
Developed with Claude Code Fable 5.1; reviewed with Codex GPT-6 Astra. I reviewed, revised, and understand the resulting code.