Skip to content

Optimize decimal scale power calculation - #10567

Open
jaideeppyne wants to merge 2 commits into
apache:mainfrom
jaideeppyne:agent/10523-decimal-power-lookup
Open

Optimize decimal scale power calculation#10567
jaideeppyne wants to merge 2 commits into
apache:mainfrom
jaideeppyne:agent/10523-decimal-power-lookup

Conversation

@jaideeppyne

@jaideeppyne jaideeppyne commented Aug 6, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

Decimal↔float casts recompute 10^scale with f64::powi. Valid Arrow decimal scales are bounded by Decimal256Type::MAX_SCALE (76), so these powers can be looked up instead of recomputed.

What changes are included in this PR?

  • Precompute IEEE-754 bit patterns of 10_f64.powi(0..=76) in one shared table.
  • Expose decimal_f64_power(scale) and use it in:
    • arrow-cast decimal→float (single_decimal_to_float_lossy)
    • arrow-cast float→decimal (cast_floating_point_to_decimal)
    • parquet-variant-compute Variant float→decimal (variant_to_unscaled_decimal)
  • Fall back to powi for negative or out-of-range scales (preserves prior behavior).
  • Exhaustive regression test: bit-identical to 10_f64.powi for every signed scale in ±MAX_SCALE.

Scope note (issue file list)

Path from #10523 Action
arrow-cast/src/cast/mod.rs Replaced 10_f64.powi with lookup
arrow-cast/src/cast/decimal.rs Replaced 10_f64.powi with lookup
arrow-arith/src/numeric.rs No 10_f64.powi; uses integer pow_checked/pow_wrapping on decimal natives (different path)
arrow-cast/src/parse.rs No decimal-scale f64::powi; uses integer powers for interval/decimal string parse
parquet-variant-compute/.../type_conversion.rs Extra call site of the same 10_f64.powi(scale) pattern — also converted

Are these changes tested?

Evidence from local validation on branch agent/10523-decimal-power-lookup:

  1. Table bit-identity (standalone rustc check)
    Compared all 77 table entries to 10_f64.powi(i).to_bits() for i in 0..=76bad=0 total=77.

  2. Unit test
    cargo test -p arrow-cast test_decimal_f64_power_matches_powi --lib1 passed
    Compares .to_bits() for every scale in -(MAX_SCALE)..=MAX_SCALE.

  3. Cast suite
    cargo test -p arrow-cast cast:: --lib326 passed.

  4. Clippy
    cargo clippy -p arrow-cast --all-targets --all-features -- -D warningspassed.

  5. Fmt
    cargo fmt --all -- --checkpassed.

  6. Downstream compile
    cargo check -p parquet-variant-computepassed (uses public decimal_f64_power via arrow::compute).

Are there any user-facing changes?

  • Behavior: no intended numerical change for valid scales (bit-identical to previous powi results).
  • API: adds public arrow_cast::cast::decimal_f64_power (also reachable via arrow::compute re-export). Pure function; no breaking change.

AI assistance disclosure

Assisted generation/investigation was used; Jaideep Pyne is the human operator responsible for the submission. Changes were validated with the commands above.

@github-actions github-actions Bot added arrow Changes to the arrow crate arrow-cast labels Aug 6, 2026
Replace 10_f64.powi(scale) with a bit-identical lookup table for
valid decimal scales (0..=Decimal256Type::MAX_SCALE) in arrow-cast
and parquet-variant-compute.

- Add public decimal_f64_power helper with exhaustive bit-equality test
- Use helper in decimal/float casts and Variant float-to-decimal conversion
- Fall back to powi for negative/out-of-range scales

Closes apache#10523
@jaideeppyne

Copy link
Copy Markdown
Author

Follow-up verification (evidence)

Pushed a tightened revision:

  1. Regenerated the DECIMAL_F64_POWERS table from 10_f64.powi and verified all 77 entries bit-identical (bad=0 total=77).
  2. Strengthened the unit test to compare .to_bits() across ±Decimal256Type::MAX_SCALE.
  3. Extended the same helper to the remaining production 10_f64.powi(scale) call site in parquet-variant-compute.
  4. Confirmed arrow-arith / parse.rs do not use decimal-scale f64::powi (they use integer powers).

Local: cast tests 326 passed, clippy -D warnings clean, fmt clean, parquet-variant-compute checks clean.

@jaideeppyne
jaideeppyne marked this pull request as ready for review August 6, 2026 11:20
@jhorstmann

Copy link
Copy Markdown
Contributor

Does this make a measurable difference in any benchmarks? The powi function should only be called once per array to calculate a multiplier, inside the loop there should be a simple multiplication.

@Jefffrey

Jefffrey commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

run benchmark cast_kernels

@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 agent/10523-decimal-power-lookup (0aa845f) to 6b7d6b3 (merge-base) diff

Run configuration
run benchmark cast_kernels
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                                                              agent_10523-decimal-power-lookup       main
-----                                                              --------------------------------       ----
"cast decimal128 to float64"                                       1.00     27.1±0.02µs        ? ?/sec    1.00     27.1±0.02µs        ? ?/sec
"cast decimal128 to int64"                                         1.00     48.0±0.47µs        ? ?/sec    1.01     48.3±0.82µs        ? ?/sec
"cast decimal128 to int8"                                          1.00     60.3±0.54µs        ? ?/sec    1.01     60.7±0.75µs        ? ?/sec
"cast decimal256 to float64"                                       1.00     60.0±0.05µs        ? ?/sec    1.14     68.5±0.04µs        ? ?/sec
"cast decimal256 to int64"                                         1.00    151.8±1.15µs        ? ?/sec    1.00    151.5±0.96µs        ? ?/sec
"cast float64 to decimal128(32, 3)"                                1.00     34.4±0.05µs        ? ?/sec    1.00     34.4±0.05µs        ? ?/sec
"cast invalid float64 to to decimal128(32, 3)"                     1.06     19.5±0.05µs        ? ?/sec    1.00     18.4±0.45µs        ? ?/sec
"cast string to decimal128(38, 3)"                                 1.00    646.4±0.42µs        ? ?/sec    1.01    654.8±0.85µs        ? ?/sec
cast binary view to string                                         1.00     69.1±0.80µs        ? ?/sec    1.01     69.6±1.41µs        ? ?/sec
cast binary view to string view                                    1.00     65.3±0.32µs        ? ?/sec    1.00     65.3±0.25µs        ? ?/sec
cast binary view to wide string                                    1.00     70.0±0.86µs        ? ?/sec    1.00     69.9±1.01µs        ? ?/sec
cast date32 to date64 512                                          1.02    329.0±0.61ns        ? ?/sec    1.00    321.4±0.85ns        ? ?/sec
cast date64 to date32 512                                          1.00   1415.3±2.18ns        ? ?/sec    1.00   1414.7±4.21ns        ? ?/sec
cast decimal128 to decimal128 512                                  1.00      6.9±0.01µs        ? ?/sec    1.00      6.9±0.01µs        ? ?/sec
cast decimal128 to decimal128 512 lower precision                  1.00     19.9±0.01µs        ? ?/sec    1.01     20.2±0.06µs        ? ?/sec
cast decimal128 to decimal128 512 with lower scale (infallible)    1.00     45.8±0.08µs        ? ?/sec    1.00     45.9±0.08µs        ? ?/sec
cast decimal128 to decimal128 512 with same scale                  1.00     75.1±0.30ns        ? ?/sec    1.02     76.4±2.11ns        ? ?/sec
cast decimal128 to decimal256 512                                  1.00     26.2±0.03µs        ? ?/sec    1.00     26.3±0.06µs        ? ?/sec
cast decimal256 to decimal128 512                                  1.00    318.4±0.20µs        ? ?/sec    1.00    317.1±0.18µs        ? ?/sec
cast decimal256 to decimal256 512                                  1.00     81.9±0.10µs        ? ?/sec    1.00     82.0±0.11µs        ? ?/sec
cast decimal256 to decimal256 512 with same scale                  1.00     76.0±1.24ns        ? ?/sec    1.00     76.0±1.21ns        ? ?/sec
cast dict to string view                                           1.01     46.7±0.60µs        ? ?/sec    1.00     46.4±0.59µs        ? ?/sec
cast f32 to string 512                                             1.02     12.0±0.08µs        ? ?/sec    1.00     11.9±0.05µs        ? ?/sec
cast f64 to string 512                                             1.00     15.3±0.05µs        ? ?/sec    1.00     15.2±0.04µs        ? ?/sec
cast float32 to int32 512                                          1.00   1313.0±1.28ns        ? ?/sec    1.03   1348.9±3.20ns        ? ?/sec
cast float64 to float32 512                                        1.00    678.1±1.62ns        ? ?/sec    1.06    717.1±1.61ns        ? ?/sec
cast float64 to uint64 512                                         1.00   1401.8±3.65ns        ? ?/sec    1.02   1424.6±4.29ns        ? ?/sec
cast i64 to string 512                                             1.00      8.7±0.03µs        ? ?/sec    1.00      8.7±0.04µs        ? ?/sec
cast int32 to float32 512                                          1.06    733.7±2.73ns        ? ?/sec    1.00    689.1±4.17ns        ? ?/sec
cast int32 to float64 512                                          1.04    743.7±3.16ns        ? ?/sec    1.00    712.4±2.61ns        ? ?/sec
cast int32 to int32 512                                            1.00    170.1±0.72ns        ? ?/sec    1.01    172.7±0.75ns        ? ?/sec
cast int32 to int64 512                                            1.00    681.2±2.91ns        ? ?/sec    1.03    704.1±1.96ns        ? ?/sec
cast int32 to uint32 512                                           1.01   1378.4±1.66ns        ? ?/sec    1.00   1364.4±1.28ns        ? ?/sec
cast int64 to int32 512                                            1.00   1434.9±2.29ns        ? ?/sec    1.00   1438.1±4.85ns        ? ?/sec
cast nested dict to dict                                           1.00      4.6±0.02µs        ? ?/sec    1.11      5.1±0.28µs        ? ?/sec
cast no runs of int32s to ree<int32>                               1.02     59.2±2.65µs        ? ?/sec    1.00     57.9±1.30µs        ? ?/sec
cast runs of 10 string to ree<int32>                               1.00      8.8±0.06µs        ? ?/sec    1.00      8.7±0.06µs        ? ?/sec
cast runs of 1000 int32s to ree<int32>                             1.00      3.4±0.01µs        ? ?/sec    1.00      3.4±0.01µs        ? ?/sec
cast string single run to ree<int32>                               1.00     27.4±0.03µs        ? ?/sec    1.00     27.4±0.03µs        ? ?/sec
cast string to binary view 512                                     1.01      2.3±0.02µs        ? ?/sec    1.00      2.3±0.01µs        ? ?/sec
cast string view to binary view                                    1.00     82.2±0.95ns        ? ?/sec    1.07     88.3±0.72ns        ? ?/sec
cast string view to dict                                           1.00    155.1±0.42µs        ? ?/sec    1.00    154.9±0.49µs        ? ?/sec
cast string view to string                                         1.00     43.2±0.74µs        ? ?/sec    1.00     43.4±0.75µs        ? ?/sec
cast string view to wide string                                    1.00     43.1±0.76µs        ? ?/sec    1.00     43.1±0.76µs        ? ?/sec
cast time32s to time32ms 512                                       1.00   1425.3±2.69ns        ? ?/sec    1.00   1422.7±2.43ns        ? ?/sec
cast time32s to time64us 512                                       1.02    329.8±0.59ns        ? ?/sec    1.00    322.4±0.71ns        ? ?/sec
cast time64ns to time32s 512                                       1.00    402.0±0.37ns        ? ?/sec    1.03    412.7±0.59ns        ? ?/sec
cast timestamp_ms to i64 512                                       1.00    246.6±5.14ns        ? ?/sec    1.01    248.7±3.85ns        ? ?/sec
cast timestamp_ms to timestamp_ns 512                              1.03   1886.8±7.02ns        ? ?/sec    1.00   1828.9±1.86ns        ? ?/sec
cast timestamp_ns to timestamp_s 512                               1.00    170.8±1.06ns        ? ?/sec    1.00    171.5±1.14ns        ? ?/sec
cast utf8 to date32 512                                            1.02      7.0±0.05µs        ? ?/sec    1.00      6.8±0.03µs        ? ?/sec
cast utf8 to date64 512                                            1.00     32.1±0.12µs        ? ?/sec    1.00     32.3±0.80µs        ? ?/sec
cast utf8 to f32                                                   1.00      5.6±0.02µs        ? ?/sec    1.00      5.6±0.03µs        ? ?/sec
cast utf8 to i32                                                   1.00      5.3±0.05µs        ? ?/sec    1.00      5.3±0.05µs        ? ?/sec
cast wide string to binary view 512                                1.00      4.0±0.07µs        ? ?/sec    1.00      4.0±0.08µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 560.1s
Peak memory 15.8 MiB
Avg memory 14.4 MiB
CPU user 554.4s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 550.1s
Peak memory 16.6 MiB
Avg memory 15.0 MiB
CPU user 546.6s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@Jefffrey

Jefffrey commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Does this make a measurable difference in any benchmarks? The powi function should only be called once per array to calculate a multiplier, inside the loop there should be a simple multiplication.

this is a good point, i think we could achieve the same speedup via fixing the cast kernel to not have the power inside in the hot loop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate arrow-cast

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace decimal power calls with lookup table

4 participants