Decimal casting - #8562
Conversation
Signed-off-by: "Nicholas Gates" <nick@nickgates.com>
Signed-off-by: "Nicholas Gates" <nick@nickgates.com>
Use array_session in decimal cast tests so the PR merge ref matches develop's test-session import pattern. I, Nicholas Gates <nick@nickgates.com>, hereby add my Signed-off-by to this commit: 25713a5 I, Nicholas Gates <nick@nickgates.com>, hereby add my Signed-off-by to this commit: 8948181 Signed-off-by: Nicholas Gates <nick@nickgates.com>
Merging this PR will not alter performance
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | chunked_varbinview_canonical_into[(1000, 10)] |
154.8 µs | 191 µs | -18.99% |
| ❌ | Simulation | slice_empty_vortex |
310 ns | 368.3 ns | -15.84% |
| ⚡ | Simulation | chunked_bool_canonical_into[(1000, 10)] |
26.7 µs | 16.2 µs | +64.55% |
| ⚡ | Simulation | chunked_varbinview_canonical_into[(100, 100)] |
259.1 µs | 224.4 µs | +15.49% |
| ⚡ | Simulation | chunked_varbinview_into_canonical[(100, 100)] |
305.7 µs | 271.4 µs | +12.63% |
| ⚡ | Simulation | bitwise_not_vortex_buffer_mut[128] |
273.6 ns | 244.4 ns | +11.93% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ngates/fix-decimal-casting (e2178ef) with develop (4668b6e)
Footnotes
-
4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Generalize the decimal CastKernel buffer-reuse fast path: when the scale is unchanged, the target precision only widens, and the current physical value type already fits the target precision, reuse the values buffer instead of allocating and re-scanning every lane. This restores the zero-copy behavior the vectorized cast regressed for the common widening case (e.g. casting a Decimal(10,2) i64 array to Decimal(18,2)). Also assert (debug-only) that the vectorized fast path and the scalar slow path agree: decimal_cast_error's success arm is unreachable unless they drift. Add a regression test asserting the widening cast shares the source buffer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Nicholas Gates <nick@nickgates.com>
|
|
||
| assert_eq!(casted.dtype(), &target_dtype); | ||
| let scalar = casted | ||
| .execute_scalar(0, &mut LEGACY_SESSION.create_execution_ctx()) |
There was a problem hiding this comment.
NOOOO, I have just fixed all the tests to not use it.
| fn cast_decimal_values<F, T>( | ||
| array: ArrayView<'_, Decimal>, | ||
| from_decimal_dtype: DecimalDType, | ||
| to_decimal_dtype: DecimalDType, | ||
| validity: Validity, | ||
| valid_values: &Mask, | ||
| ) -> VortexResult<ArrayRef> | ||
| where | ||
| F: NativeDecimalType, | ||
| T: NativeDecimalType + CheckedMul, | ||
| DecimalValue: From<F>, | ||
| { | ||
| let values = array.buffer::<F>(); | ||
| let values = values.as_slice(); | ||
| let cast_plan = DecimalCastPlan::<T>::new(from_decimal_dtype, to_decimal_dtype); | ||
|
|
||
| let buffer = match valid_values { | ||
| Mask::AllTrue(_) => { | ||
| let mut buffer = BufferMut::<T>::with_capacity(values.len()); | ||
| values | ||
| .try_map_into(&mut buffer.spare_capacity_mut()[..values.len()], |value| { | ||
| cast_plan.cast(value) | ||
| }) | ||
| .map_err(|idx| { | ||
| decimal_cast_error::<F, T>(values[idx], from_decimal_dtype, to_decimal_dtype) | ||
| })?; | ||
| // SAFETY: try_map_into initializes every lane before returning Ok. | ||
| unsafe { buffer.set_len(values.len()) }; | ||
| buffer.freeze() | ||
| } | ||
| Mask::AllFalse(_) => BufferMut::<T>::zeroed(values.len()).freeze(), | ||
| Mask::Values(mask) => { | ||
| let mut buffer = BufferMut::<T>::with_capacity(values.len()); | ||
| values | ||
| .try_map_masked_into( | ||
| mask.bit_buffer(), | ||
| &mut buffer.spare_capacity_mut()[..values.len()], | ||
| |value| cast_plan.cast(value), | ||
| ) | ||
| .map_err(|idx| { | ||
| decimal_cast_error::<F, T>(values[idx], from_decimal_dtype, to_decimal_dtype) | ||
| })?; | ||
| // SAFETY: try_map_masked_into initializes every lane before returning Ok. | ||
| unsafe { buffer.set_len(values.len()) }; | ||
| buffer.freeze() | ||
| } | ||
| }; | ||
|
|
||
| Ok(DecimalArray::new(buffer, to_decimal_dtype, validity).into_array()) | ||
| } |
There was a problem hiding this comment.
This should use the same logic that PrimitiveArray cast uses
| let rescaled = Self::rescale_i256( | ||
| self.as_i256(), | ||
| from_decimal_dtype.scale(), | ||
| to_decimal_dtype.scale(), | ||
| )?; | ||
| Self::try_from_i256(rescaled, to_decimal_dtype) |
There was a problem hiding this comment.
Why would this need to widen to i256?
There was a problem hiding this comment.
I see, it's a bit less code to do it in i256
|
|
||
| let scaled = DecimalValue::rescale_i256(value, 0, decimal_dtype.scale())?; | ||
| DecimalValue::try_from_i256(scaled, decimal_dtype) | ||
| } |
There was a problem hiding this comment.
This seems lazy? There's 3 conversions here which seems excessive
No description provided.