Skip to content

fix: Consolidate string-to-decimal parsing into a single code path - #10850

Merged
Jefffrey merged 2 commits into
apache:mainfrom
neilconway:neilc/consolidate-decimal-parsing
Sep 1, 2026
Merged

fix: Consolidate string-to-decimal parsing into a single code path#10850
Jefffrey merged 2 commits into
apache:mainfrom
neilconway:neilc/consolidate-decimal-parsing

Conversation

@neilconway

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

arrow-cast had two different string-to-decimal parsers: parse_string_to_decimal_native, used by cast, and parse_decimal, used by the CSV and JSON readers. Aside from redundancy, these code paths behaved differently (e.g., truncating vs rounding for digits beyond the target type's scale, whitespace trimming, support for e-notation, etc.), so decimal conversion behaved differently depending on how the decimal value arrived into arrow-rs.

This PR replaces these parsers with a single unified parser; parse_decimal(s, precision_scale) is now the public entry point and parse_string_to_decimal_native is a thin, deprecated wrapper over it. The new parser is based on the one-pass, u64-chunked parser in #10668, extended with support for e-notation and negative scales. This fixes a lot of bugs and ensures consistent behavior, but it does result in some behavior changes and a small performance regression for the CSV/JSON path; more details below.

Bugs fixed: (all in the JSON/CSV path)

Behaviour changes:

Performance:

  • cast string-to-decimal: ~unchanged. The cast path already used the fast single-pass parser from perf(arrow-cast): optimize parsing of decimals from strings #10668; the unified parser benchmarks within Criterion noise (~4%) of it.

  • CSV/JSON reader path: short inputs cost 1-2 ns more per value, while long Decimal256 inputs are ~35% faster. End-to-end, CSV reads of decimal columns are ~8% slower. I suspect there is room for further optimization here (which will now benefit both code paths!) to reach or exceed the previous performance, but I'd like to land the unified parser first before we tackle further optimizations.

What changes are included in this PR?

See above.

Are these changes tested?

New tests added to cover rounding, exponents, negative scale, whitespace, long inputs and the four widths, a seeded differential test checks 20k random inputs against a BigInt reference (num-bigint was added as a dev-dependency), and the CSV/JSON readers gain end-to-end tests for the new behavior and bugfixes listed above.

Are there any user-facing changes?

Yes; a deprecated public API, and user-visible behavioral changes in decimal parsing.

AI usage

Iterated primarily with CC Fable 5; code reviewed by Codex GPT 5.6. I read, understand, and revised the resulting code.

arrow-cast previously contained two different string-to-decimal parsers:
`parse_string_to_decimal_native`, used by `cast`, and `parse_decimal`,
used by the CSV and JSON readers.  Aside from redundancy, these code
paths behaved differently (e.g., truncating vs rounding for digits
beyond the target type's scale, whitespace trimming, support for
e-notation, etc.), so decimal conversion behaved differently depending
on how the decimal value arrived into arrow-rs.

This branch replaces these two parsers with a single unified parser;
`parse_decimal(s, precision_scale)` is now the public entry point and
`parse_string_to_decimal_native` is a thin, deprecated wrapper over it.

The new parser is based on the one-pass, u64-chunked parser in apache#10668,
extended with support for e-notation and negative scales. This fixes a
lot of bugs and ensures consistent behavior, but it does result in some
behavior changes and a small performance regression for the CSV/JSON
path; more details below.

Bugs fixed: (all in the JSON/CSV path)

- divide-by-zero panic or wrong values for some inputs in exponent
  notation (apache#10788, apache#5762)
- overflow on inputs with >= 256 digits or long exponents (apache#10787)
- negative scales were ignored (apache#10791)
- `0e0`/`-0e0` were rejected (apache#10789) while `e5` and `-.` parsed as 0
  (apache#10790)

Behaviour changes:

- CSV and JSON readers now round half away from zero instead of
  truncating digits beyond the scale (apache#9410, apache#9422, apache#7355)
- `cast` from strings accepts exponent notation (apache#5068) and negative
  scales, which `can_cast_types` already advertised (apache#10792), and
  validates the target precision and scale before parsing any values
- CSV and JSON readers now trim whitespace (apache#10793). Only ASCII
  whitespace characters are trimmed, which matches the behavior of the
  CSV float/int parsers; previously, the `cast` path trimmed Unicode
  whitespace as well, but it will no longer do so.
- parse errors use `ArrowError::ParseError` with unified messages
- `variant_get` validates the target precision for string inputs
  (apache#10794)

Tests: new tests cover rounding, exponents, negative scale, whitespace,
long inputs and the four widths, a seeded differential test checks 20k
random inputs against a BigInt reference (num-bigint was added as a
dev-dependency), and the CSV/JSON readers gain end-to-end tests for the
new behavior and bugfixes listed above.

Performance:

- `cast` string-to-decimal: ~unchanged. The cast path already used the
  fast single-pass parser from apache#10668; the unified parser benchmarks
  within Criterion noise (~4%) of it.

- CSV/JSON reader path: short inputs cost 1-2 ns more per value, while
  long Decimal256 inputs are ~35% faster. End-to-end, CSV reads of
  decimal columns are ~8% slower. I suspect there is room for further
  optimization here (which will now benefit both code paths!) to reach
  or exceed the previous performance, but I'd like to land the unified
  parser first before we tackle further optimizations.

Closes apache#10787, closes apache#10788, closes apache#10789, closes apache#10790,
closes apache#10791, closes apache#10792, closes apache#10793, closes apache#10794
@Jefffrey

Copy link
Copy Markdown
Contributor

run benchmark cast_kernels
env:
BENCH_FILTER: string to decimal

@adriangbot

This comment was marked as duplicate.

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing neilc/consolidate-decimal-parsing (3960a18) to 2a82e59 (merge-base) diff

Run configuration
run benchmark cast_kernels
env:
  BENCH_FILTER: "string to decimal"
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                 main                                   neilc_consolidate-decimal-parsing
-----                                 ----                                   ---------------------------------
"cast string to decimal128(38, 3)"    1.25    119.0±0.37µs        ? ?/sec    1.00     95.5±0.44µs        ? ?/sec
"cast string to decimal256(76, 3)"    1.00    147.3±0.39µs        ? ?/sec    1.04    153.5±0.78µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 25.0s
Peak memory 11.2 MiB
Avg memory 8.9 MiB
CPU user 19.1s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 20.0s
Peak memory 11.0 MiB
Avg memory 9.9 MiB
CPU user 18.0s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

Comment thread arrow-cast/src/parse.rs
@Jefffrey Jefffrey added the bug label Aug 30, 2026
@Jefffrey

Copy link
Copy Markdown
Contributor

CSV/JSON reader path: short inputs cost 1-2 ns more per value, while long Decimal256 inputs are ~35% faster. End-to-end, CSV reads of decimal columns are ~8% slower. I suspect there is room for further optimization here (which will now benefit both code paths!) to reach or exceed the previous performance, but I'd like to land the unified parser first before we tackle further optimizations.

were these based on new benchmarks? i cant seem to spot any decimal read paths in our existing csv/json benchmarks

@neilconway

Copy link
Copy Markdown
Contributor Author

were these based on new benchmarks? i cant seem to spot any decimal read paths in our existing csv/json benchmarks

Yeah -- I had Claude write a quick end-to-end test harness because it wasn't covered by the existing benchmarks. I'm happy to add that (as a separate PR?) if you think it would be useful.

@Jefffrey
Jefffrey merged commit 70219af into apache:main Sep 1, 2026
44 checks passed
@Jefffrey

Jefffrey commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

no big deal, was just curious

thanks for this @neilconway

Jefffrey pushed a commit that referenced this pull request Sep 2, 2026
# Which issue does this PR close?

- N/A; motivated by the performance regression in #10850

# Rationale for this change

`decimal` did not have benchmark coverage for end-to-end CSV or JSON
parsing; also, the `parse_decimal` microbenchmark had unrepresentative
branch predictor behavior.

<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

# What changes are included in this PR?

* Add benchmark for parsing CSV with a decimal field
* Add benchmark for parsing JSON with a decimal field
* Improve the `parse_decimal` microbenchmark to generate a set of random
strings to parse, rather than repeatedly parsing the same string.
Repeatedly parsing the same string is not representative of real-world
workloads; in particular, it gives the branch predictor an artificial
boost, which can hide constructs that will poorly poorly in more
realistic scenarios due to poor branch prediction.

# Are these changes tested?

Yes.

# Are there any user-facing changes?

No.

# AI usage

Developed with Claude Code, Fable 5.1. I revised and understand the
resulting code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment