Skip to content

Commit 295fae6

Browse files
committed
Support wide decimals in DecimalBytePartsArray and kernels (#9809)
`DecimalBytePartsArray` previously stored the entire unscaled decimal value in one signed integer child, limiting it to values that fit in 64 bits. It now supports wide decimals by representing each value as integer parts that can be compressed independently, while preserving the decimal's logical precision, scale, and nullability. The array has a signed most significant part (MSP) and up to three unsigned 64-bit lower parts, ordered most significant first. Splitting canonical decimal storage produces: | Decimal storage | Children | | --- | --- | | `i8` / `i16` / `i32` / `i64` | Signed MSP only; shares the original value buffer | | `i128` | `i64` MSP + one `u64` lower part | | `i256` | `i64` MSP + three `u64` lower parts | Only the MSP carries validity. Every lower part must be a non-nullable `u64` array with the same length, and splitting wide decimals zeroes the parts at null positions. All children remain `ArrayRef`s, so their individual encodings are independent of the decimal representation. `execute::<DecimalArray>` reassembles a `DecimalArray` from the MSP and lower parts children. `take` with nullable indices is not yet supported by the DecimalByteParts kernel for arrays with lower parts; it falls back to canonical execution. Taking each part directly would make the lower parts nullable, violating the representation's invariant. The frozen serializer also continues to reject arrays with lower parts. This PR also refines the splitting and assembly modules. * Assembly takes `ArrayRef`s instead of `PrimitiveArray`s so that in the future, we can add special fast paths for constant arrays * Assembly casts lower parts to `u64`s, allowing for assembly of narrowed lower parts. * Assembly loop is optimized such that it vectorizes for `i256` assembly on local runs. --------- Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent bad88ce commit 295fae6

28 files changed

Lines changed: 2626 additions & 1106 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ coverage.xml
5252
*.cover
5353
*.py,cover
5454
.hypothesis/
55+
# hegeltest's example database, the Rust equivalent of .hypothesis/
56+
.hegel/
5557
.pytest_cache/
5658
cover/
5759

‎Cargo.lock‎

Lines changed: 83 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ glob = "0.3.2"
176176
goldenfile = "1"
177177
half = { version = "2.7.1", features = ["std", "num-traits"] }
178178
hashbrown = "0.17.1"
179+
hegeltest = "0.28.7"
179180
http = "1.5.0"
180181
humansize = "2.1.3"
181182
indicatif = "0.18.0"

‎encodings/decimal-byte-parts/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ vortex-session = { workspace = true }
2727

2828
[dev-dependencies]
2929
divan = { workspace = true }
30+
hegeltest = { workspace = true }
3031
rand = { workspace = true }
3132
rstest = { workspace = true }
3233
vortex-array = { path = "../../vortex-array", features = ["_test-harness"] }
34+
vortex-bench-support = { workspace = true }
3335

3436
[[bench]]
3537
name = "dbp_assemble"
Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,77 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! Shared decimal inputs for splitting and assembly benchmarks.
4+
//! Shared inputs for splitting and assembly benchmarks.
55
66
use rand::RngExt;
77
use rand::SeedableRng;
88
use rand::rngs::StdRng;
9-
use vortex_array::arrays::DecimalArray;
10-
use vortex_array::dtype::DecimalDType;
119
use vortex_array::dtype::DecimalType;
1210
use vortex_array::dtype::i256;
13-
use vortex_array::validity::Validity;
1411
use vortex_buffer::Buffer;
15-
use vortex_error::vortex_panic;
1612

1713
pub(super) fn cases() -> Vec<(DecimalType, usize)> {
18-
[DecimalType::I64, DecimalType::I128, DecimalType::I256]
14+
[DecimalType::I128, DecimalType::I256]
1915
.into_iter()
2016
.flat_map(|values_type| [1_024, 8_192].map(|len| (values_type, len)))
2117
.collect()
2218
}
2319

24-
pub(super) fn decimal_array(
25-
values_type: DecimalType,
26-
len: usize,
27-
validity: Validity,
28-
) -> DecimalArray {
20+
pub(super) fn i128_values(len: usize) -> Buffer<i128> {
2921
let mut rng = StdRng::seed_from_u64(42);
22+
let max = 10i128.pow(38) - 1;
23+
(0..len).map(|_| rng.random_range(-max..=max)).collect()
24+
}
25+
26+
pub(super) fn i256_values(len: usize) -> Buffer<i256> {
27+
let mut rng = StdRng::seed_from_u64(42);
28+
// Keep the magnitude below 10^76 while exercising all four signed/unsigned words.
29+
(0..len)
30+
.map(|_| i256::from_parts(rng.random(), rng.random::<i128>() >> 4))
31+
.collect()
32+
}
33+
34+
#[cfg(not(codspeed))]
35+
pub(super) mod arrays {
36+
use rand::RngExt;
37+
use rand::SeedableRng;
38+
use rand::rngs::StdRng;
39+
use vortex_array::arrays::DecimalArray;
40+
use vortex_array::dtype::DecimalDType;
41+
use vortex_array::dtype::DecimalType;
42+
use vortex_array::validity::Validity;
43+
use vortex_buffer::Buffer;
44+
use vortex_error::vortex_panic;
45+
46+
use super::i128_values;
47+
use super::i256_values;
3048

31-
macro_rules! decimal {
32-
($T:ty, $precision:literal) => {{
33-
let max = <$T>::pow(10, $precision) - 1;
34-
let values: Buffer<$T> = (0..len).map(|_| rng.random_range(-max..=max)).collect();
35-
DecimalArray::new(values, DecimalDType::new($precision, 2), validity)
36-
}};
49+
pub(crate) fn cases() -> Vec<(DecimalType, usize)> {
50+
[DecimalType::I64, DecimalType::I128, DecimalType::I256]
51+
.into_iter()
52+
.flat_map(|values_type| [1_024, 8_192].map(|len| (values_type, len)))
53+
.collect()
3754
}
3855

39-
match values_type {
40-
DecimalType::I64 => decimal!(i64, 18),
41-
DecimalType::I128 => decimal!(i128, 38),
42-
DecimalType::I256 => {
43-
// Keep the magnitude below 10^76 while exercising all four signed/unsigned words.
44-
let values: Buffer<i256> = (0..len)
45-
.map(|_| i256::from_parts(rng.random(), rng.random::<i128>() >> 4))
46-
.collect();
47-
DecimalArray::new(values, DecimalDType::new(76, 2), validity)
56+
pub(crate) fn decimal_array(
57+
values_type: DecimalType,
58+
len: usize,
59+
validity: Validity,
60+
) -> DecimalArray {
61+
match values_type {
62+
DecimalType::I64 => {
63+
let mut rng = StdRng::seed_from_u64(42);
64+
let max = 10i64.pow(18) - 1;
65+
let values: Buffer<i64> = (0..len).map(|_| rng.random_range(-max..=max)).collect();
66+
DecimalArray::new(values, DecimalDType::new(18, 2), validity)
67+
}
68+
DecimalType::I128 => {
69+
DecimalArray::new(i128_values(len), DecimalDType::new(38, 2), validity)
70+
}
71+
DecimalType::I256 => {
72+
DecimalArray::new(i256_values(len), DecimalDType::new(76, 2), validity)
73+
}
74+
_ => vortex_panic!("unsupported benchmark storage type: {values_type}"),
4875
}
49-
_ => vortex_panic!("unsupported benchmark storage type: {values_type}"),
5076
}
5177
}

0 commit comments

Comments
 (0)