Skip to content

perf: vectorize float/decimal to narrow-integer casts#4941

Open
andygrove wants to merge 4 commits into
apache:mainfrom
andygrove:perf-optimize-narrowing-casts
Open

perf: vectorize float/decimal to narrow-integer casts#4941
andygrove wants to merge 4 commits into
apache:mainfrom
andygrove:perf-optimize-narrowing-casts

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Part of #4936.

Rationale for this change

The float-to-int and decimal-to-int narrowing casts built their output with a per-element iter().map(...).collect::<Result<...>>() over Option/Result, the same slow pattern that spark_cast_int_to_int moved off of (that change was up to 100x faster).

What changes are included in this PR?

Rewrites the four macros (cast_float_to_int16_down, cast_float_to_int32_up, cast_decimal_to_int16_down, cast_decimal_to_int32_up) to use Arrow's unary (legacy/try) and try_unary (ANSI) kernels, which map the values buffer in one pass and carry the null buffer over, following the existing cast_int_to_int_macro. Each macro gains a destination ArrowPrimitiveType parameter, threaded through the 12 call sites. The decimal macros also hoist the constant 10^scale divisor out of the per-element loop.

Overflow, NaN, saturation, and the cast-through-Int wrap semantics are preserved exactly; only the iteration mechanism changes.

How are these changes tested?

Added Rust unit tests (these paths previously had only Scala coverage): float64-to-Byte legacy wrap, float64-to-Int ANSI ok + overflow error, decimal-to-Int legacy, decimal-to-Byte legacy wrap, and decimal-to-Int ANSI overflow error. Output is bit-identical to main.

Benchmark (criterion), baseline main vs this branch, 8192-row columns:

cast_narrowing: f64 -> i8:        21.7 µs -> 1.9 µs   (-91%)
cast_narrowing: f64 -> i32:       21.4 µs -> 1.8 µs   (-92%)
cast_narrowing: f64 -> i32 ansi:  23.4 µs -> 11.9 µs  (-49%)
cast_narrowing: dec -> i8:        41.6 µs -> 17.1 µs  (-59%)
cast_narrowing: dec -> i32:       43.7 µs -> 16.6 µs  (-62%)

The four macros for float-to-int and decimal-to-int narrowing casts
(cast_float_to_int16_down, cast_float_to_int32_up, cast_decimal_to_int16_down,
cast_decimal_to_int32_up) built the output with a per-element iterator-collect
over Option/Result. Replace that with Arrow's unary (legacy) and try_unary
(ANSI) kernels, which map the values buffer in one pass and carry the null
buffer over, following the same pattern used by cast_int_to_int_macro. The
decimal macros also hoist the constant scale divisor out of the per-element loop.

Overflow, NaN, saturation, and wrap-through-Int semantics are preserved
unchanged; only the iteration mechanism changes. Add Rust unit tests for the
float-to-Byte and decimal-to-Int/Byte legacy wrap paths and the ANSI overflow
error paths (previously covered only by Scala tests), plus a benchmark.

Non-overflow casts are 49-91% faster with no regression.

Part of apache#4936.

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

First pass, thanks @andygrove!

Comment thread native/spark-expr/src/conversion_funcs/numeric.rs
Comment thread native/spark-expr/src/conversion_funcs/numeric.rs Outdated
…ulls invariant

Extends test_cast_float64_to_int8_legacy_wraps with 3e9, +inf, and -inf so the saturate-i32-then-truncate-to-i8 chain is asserted end to end; a regression that lost the double-narrowing would fail loudly. Adds a one-line comment on each of the four legacy PrimitiveArray::unary arms noting that the closure runs on null slots too and stating why the as-cast / positive-divisor division is infallible for any bit pattern.

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

This one LGTM after revision, thanks @andygrove!

@andygrove

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts on this branch.

…ng-casts

# Conflicts:
#	docs/source/contributor-guide/expression-audits/conversion_funcs.md
#	native/spark-expr/Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants