From 1db27f22053a5aa421d1cfd9b2bb571b21eb83d9 Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Fri, 18 Sep 2026 16:55:32 -0400 Subject: [PATCH 1/5] perf: pack primitive comparison results in fixed-size chunks Reuse collect_bool_word for primitive comparison bitmaps while preserving execution-context allocation and comparison semantics. Cover bitmap boundaries, all primitive types, nulls, and constant operand orientation. Signed-off-by: "Matt Katz" Signed-off-by: Matt Katz --- .../scalar_fn/fns/binary/compare/primitive.rs | 44 ++++++++- .../src/scalar_fn/fns/binary/compare/tests.rs | 99 +++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs index 97e248a1a7d..1a4000b6a9e 100644 --- a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs +++ b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs @@ -1,10 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -//! Native comparison of primitive arrays via bit-packing lane kernels. +//! Native comparison of primitive arrays with byte-oriented bitmap packing. use vortex_buffer::BitBuffer; use vortex_buffer::BufferAllocatorRef; +use vortex_buffer::BufferMut; +use vortex_buffer::collect_bool_word; use vortex_error::VortexResult; use vortex_error::vortex_bail; @@ -19,8 +21,7 @@ use crate::dtype::Nullability; use crate::dtype::PType; use crate::match_each_native_ptype; use crate::scalar::Scalar; -use crate::scalar_fn::fns::binary::compare::collect_bits; -use crate::scalar_fn::fns::binary::compare::collect_zip_bits; +use crate::scalar_fn::fns::binary::compare::bit_buffer_from_words; use crate::scalar_fn::fns::binary::compare::compare_validity; use crate::scalar_fn::fns::binary::primitive_operand::PrimitiveOperand; use crate::scalar_fn::fns::operators::CompareOperator; @@ -140,3 +141,40 @@ fn compare_slice_constant( CompareOperator::Lte => collect_bits(lhs, |a: T| a.is_le(rhs), allocator), } } + +fn collect_bits( + values: &[T], + f: impl Fn(T) -> bool, + allocator: &BufferAllocatorRef, +) -> BitBuffer { + let (chunks, tail) = values.as_chunks::<64>(); + let mut words = BufferMut::::zeroed_in(values.len().div_ceil(64), allocator.clone()); + // Fixed-size chunks let the compiler prove the predicate's indexing stays in bounds. + for (word, chunk) in words.iter_mut().zip(chunks) { + *word = collect_bool_word(64, |i| f(chunk[i])); + } + if !tail.is_empty() { + words[chunks.len()] = collect_bool_word(tail.len(), |i| f(tail[i])); + } + bit_buffer_from_words(words, values.len()) +} + +fn collect_zip_bits( + lhs: &[T], + rhs: &[T], + f: impl Fn(T, T) -> bool, + allocator: &BufferAllocatorRef, +) -> BitBuffer { + assert_eq!(lhs.len(), rhs.len()); + let (left_chunks, left_tail) = lhs.as_chunks::<64>(); + let (right_chunks, right_tail) = rhs.as_chunks::<64>(); + let mut words = BufferMut::::zeroed_in(lhs.len().div_ceil(64), allocator.clone()); + for ((word, left), right) in words.iter_mut().zip(left_chunks).zip(right_chunks) { + *word = collect_bool_word(64, |i| f(left[i], right[i])); + } + if !left_tail.is_empty() { + words[left_chunks.len()] = + collect_bool_word(left_tail.len(), |i| f(left_tail[i], right_tail[i])); + } + bit_buffer_from_words(words, lhs.len()) +} diff --git a/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs b/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs index 85260afbd99..14a80ef5b07 100644 --- a/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs +++ b/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs @@ -883,3 +883,102 @@ fn struct_of_map_compare() -> VortexResult<()> { Ok(()) } + +#[rstest] +fn primitive_comparisons_across_bitmap_words( + #[values( + PType::I8, PType::I16, PType::I32, PType::I64, PType::U8, PType::U16, PType::U32, + PType::U64, PType::F16, PType::F32, PType::F64 + )] + ptype: PType, + #[values(0, 1, 63, 64, 65, 129)] len: usize, + #[values( + CompareOperator::Eq, + CompareOperator::NotEq, + CompareOperator::Lt, + CompareOperator::Lte, + CompareOperator::Gt, + CompareOperator::Gte + )] + op: CompareOperator, +) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let value_for_type = |value: Option| { + value.map(|value| { + if ptype.is_unsigned_int() { + value.abs() + } else { + value + } + }) + }; + let left: Vec<_> = [Some(-128i16), Some(-1), None, Some(127), Some(42), Some(2)] + .into_iter() + .cycle() + .take(len) + .map(value_for_type) + .collect(); + let right: Vec<_> = [ + Some(-1i16), + Some(-128), + Some(127), + None, + Some(42), + Some(3), + Some(100), + ] + .into_iter() + .cycle() + .take(len) + .map(value_for_type) + .collect(); + let dtype = DType::Primitive(ptype, Nullability::Nullable); + let lhs = PrimitiveArray::from_option_iter(left.iter().copied()) + .into_array() + .cast(dtype.clone())? + .execute::(&mut ctx)? + .into_array(); + let rhs = PrimitiveArray::from_option_iter(right.iter().copied()) + .into_array() + .cast(dtype.clone())? + .execute::(&mut ctx)? + .into_array(); + let predicate = |a: i16, b: i16| match op { + CompareOperator::Eq => a == b, + CompareOperator::NotEq => a != b, + CompareOperator::Lt => a < b, + CompareOperator::Lte => a <= b, + CompareOperator::Gt => a > b, + CompareOperator::Gte => a >= b, + }; + let expected = BoolArray::from_iter( + left.iter() + .zip(&right) + .map(|(a, b)| a.zip(*b).map(|(a, b)| predicate(a, b))), + ); + assert_arrays_eq!(lhs.binary(rhs, op.into())?, expected, &mut ctx); + + let constant = ConstantArray::new( + Scalar::primitive(42u8, Nullability::Nullable).cast(&dtype)?, + len, + ) + .into_array(); + for swapped in [false, true] { + let actual = if swapped { + constant.binary(lhs.clone(), op.into())? + } else { + lhs.binary(constant.clone(), op.into())? + }; + let expected = BoolArray::from_iter(left.iter().map(|value| { + value.map(|value| { + if swapped { + predicate(42, value) + } else { + predicate(value, 42) + } + }) + })); + assert_arrays_eq!(actual, expected, &mut ctx); + } + Ok(()) +} From 95b15506fd31d10503b84825289be95b8e6871d9 Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Tue, 22 Sep 2026 10:34:36 -0400 Subject: [PATCH 2/5] perf: specialize comparison bitmap packing for 8-bit inputs Use byte packing only for i8/u8 and preserve the existing lane collectors for wider primitive types. The type selection folds away during monomorphization and avoids the wider AVX2 comparison regressions. Signed-off-by: "Matt Katz" Signed-off-by: Matt Katz --- .../src/scalar_fn/fns/binary/compare/primitive.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs index 1a4000b6a9e..c0d3c85a68c 100644 --- a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs +++ b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -//! Native comparison of primitive arrays with byte-oriented bitmap packing. +//! Native comparison of primitive arrays with specialized bitmap packing for 8-bit inputs. use vortex_buffer::BitBuffer; use vortex_buffer::BufferAllocatorRef; @@ -142,11 +142,16 @@ fn compare_slice_constant( } } -fn collect_bits( +fn collect_bits( values: &[T], f: impl Fn(T) -> bool, allocator: &BufferAllocatorRef, ) -> BitBuffer { + // This type check folds away during monomorphization. Wider masks keep the lane kernel: + // byte packing regresses 64-bit comparisons on AVX2. + if !matches!(T::PTYPE, PType::I8 | PType::U8) { + return super::collect_bits(values, f, allocator); + } let (chunks, tail) = values.as_chunks::<64>(); let mut words = BufferMut::::zeroed_in(values.len().div_ceil(64), allocator.clone()); // Fixed-size chunks let the compiler prove the predicate's indexing stays in bounds. @@ -159,12 +164,15 @@ fn collect_bits( bit_buffer_from_words(words, values.len()) } -fn collect_zip_bits( +fn collect_zip_bits( lhs: &[T], rhs: &[T], f: impl Fn(T, T) -> bool, allocator: &BufferAllocatorRef, ) -> BitBuffer { + if !matches!(T::PTYPE, PType::I8 | PType::U8) { + return super::collect_zip_bits(lhs, rhs, f, allocator); + } assert_eq!(lhs.len(), rhs.len()); let (left_chunks, left_tail) = lhs.as_chunks::<64>(); let (right_chunks, right_tail) = rhs.as_chunks::<64>(); From 4483ef94e7f6968a527e544883dcb33ac72217f2 Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Tue, 22 Sep 2026 10:55:03 -0400 Subject: [PATCH 3/5] refactor: separate primitive comparison bitmap dispatch Keep type selection in collect_bits_dispatch and collect_zip_bits_dispatch, with independent narrow collectors and the existing wider collectors. Signed-off-by: "Matt Katz" Signed-off-by: Matt Katz --- .../scalar_fn/fns/binary/compare/primitive.rs | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs index c0d3c85a68c..d7607953651 100644 --- a/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs +++ b/vortex-array/src/scalar_fn/fns/binary/compare/primitive.rs @@ -22,6 +22,8 @@ use crate::dtype::PType; use crate::match_each_native_ptype; use crate::scalar::Scalar; use crate::scalar_fn::fns::binary::compare::bit_buffer_from_words; +use crate::scalar_fn::fns::binary::compare::collect_bits; +use crate::scalar_fn::fns::binary::compare::collect_zip_bits; use crate::scalar_fn::fns::binary::compare::compare_validity; use crate::scalar_fn::fns::binary::primitive_operand::PrimitiveOperand; use crate::scalar_fn::fns::operators::CompareOperator; @@ -117,12 +119,16 @@ fn compare_slices( // Dispatch the operator outside the lane loop so each instantiation vectorizes a single // branch-free predicate. match op { - CompareOperator::Eq => collect_zip_bits(lhs, rhs, |a: T, b: T| a.is_eq(b), allocator), - CompareOperator::NotEq => collect_zip_bits(lhs, rhs, |a: T, b: T| !a.is_eq(b), allocator), - CompareOperator::Gt => collect_zip_bits(lhs, rhs, T::is_gt, allocator), - CompareOperator::Gte => collect_zip_bits(lhs, rhs, T::is_ge, allocator), - CompareOperator::Lt => collect_zip_bits(lhs, rhs, T::is_lt, allocator), - CompareOperator::Lte => collect_zip_bits(lhs, rhs, T::is_le, allocator), + CompareOperator::Eq => { + collect_zip_bits_dispatch(lhs, rhs, |a: T, b: T| a.is_eq(b), allocator) + } + CompareOperator::NotEq => { + collect_zip_bits_dispatch(lhs, rhs, |a: T, b: T| !a.is_eq(b), allocator) + } + CompareOperator::Gt => collect_zip_bits_dispatch(lhs, rhs, T::is_gt, allocator), + CompareOperator::Gte => collect_zip_bits_dispatch(lhs, rhs, T::is_ge, allocator), + CompareOperator::Lt => collect_zip_bits_dispatch(lhs, rhs, T::is_lt, allocator), + CompareOperator::Lte => collect_zip_bits_dispatch(lhs, rhs, T::is_le, allocator), } } @@ -133,25 +139,47 @@ fn compare_slice_constant( allocator: &BufferAllocatorRef, ) -> BitBuffer { match op { - CompareOperator::Eq => collect_bits(lhs, |a: T| a.is_eq(rhs), allocator), - CompareOperator::NotEq => collect_bits(lhs, |a: T| !a.is_eq(rhs), allocator), - CompareOperator::Gt => collect_bits(lhs, |a: T| a.is_gt(rhs), allocator), - CompareOperator::Gte => collect_bits(lhs, |a: T| a.is_ge(rhs), allocator), - CompareOperator::Lt => collect_bits(lhs, |a: T| a.is_lt(rhs), allocator), - CompareOperator::Lte => collect_bits(lhs, |a: T| a.is_le(rhs), allocator), + CompareOperator::Eq => collect_bits_dispatch(lhs, |a: T| a.is_eq(rhs), allocator), + CompareOperator::NotEq => collect_bits_dispatch(lhs, |a: T| !a.is_eq(rhs), allocator), + CompareOperator::Gt => collect_bits_dispatch(lhs, |a: T| a.is_gt(rhs), allocator), + CompareOperator::Gte => collect_bits_dispatch(lhs, |a: T| a.is_ge(rhs), allocator), + CompareOperator::Lt => collect_bits_dispatch(lhs, |a: T| a.is_lt(rhs), allocator), + CompareOperator::Lte => collect_bits_dispatch(lhs, |a: T| a.is_le(rhs), allocator), } } -fn collect_bits( +fn collect_bits_dispatch( values: &[T], f: impl Fn(T) -> bool, allocator: &BufferAllocatorRef, ) -> BitBuffer { // This type check folds away during monomorphization. Wider masks keep the lane kernel: // byte packing regresses 64-bit comparisons on AVX2. - if !matches!(T::PTYPE, PType::I8 | PType::U8) { - return super::collect_bits(values, f, allocator); + if matches!(T::PTYPE, PType::I8 | PType::U8) { + collect_bits_narrow(values, f, allocator) + } else { + collect_bits(values, f, allocator) } +} + +fn collect_zip_bits_dispatch( + lhs: &[T], + rhs: &[T], + f: impl Fn(T, T) -> bool, + allocator: &BufferAllocatorRef, +) -> BitBuffer { + if matches!(T::PTYPE, PType::I8 | PType::U8) { + collect_zip_bits_narrow(lhs, rhs, f, allocator) + } else { + collect_zip_bits(lhs, rhs, f, allocator) + } +} + +fn collect_bits_narrow( + values: &[T], + f: impl Fn(T) -> bool, + allocator: &BufferAllocatorRef, +) -> BitBuffer { let (chunks, tail) = values.as_chunks::<64>(); let mut words = BufferMut::::zeroed_in(values.len().div_ceil(64), allocator.clone()); // Fixed-size chunks let the compiler prove the predicate's indexing stays in bounds. @@ -164,15 +192,12 @@ fn collect_bits( bit_buffer_from_words(words, values.len()) } -fn collect_zip_bits( +fn collect_zip_bits_narrow( lhs: &[T], rhs: &[T], f: impl Fn(T, T) -> bool, allocator: &BufferAllocatorRef, ) -> BitBuffer { - if !matches!(T::PTYPE, PType::I8 | PType::U8) { - return super::collect_zip_bits(lhs, rhs, f, allocator); - } assert_eq!(lhs.len(), rhs.len()); let (left_chunks, left_tail) = lhs.as_chunks::<64>(); let (right_chunks, right_tail) = rhs.as_chunks::<64>(); From c215b0e423f558ef83ac0f99841160d70237a9b7 Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Tue, 22 Sep 2026 10:59:48 -0400 Subject: [PATCH 4/5] test: focus bitmap comparison coverage on 8-bit inputs Limit the new boundary matrix to the i8/u8 specialization. Existing tests cover the unchanged wider primitive comparison paths. Signed-off-by: "Matt Katz" Signed-off-by: Matt Katz --- vortex-array/src/scalar_fn/fns/binary/compare/tests.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs b/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs index 14a80ef5b07..4688d323c90 100644 --- a/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs +++ b/vortex-array/src/scalar_fn/fns/binary/compare/tests.rs @@ -885,12 +885,8 @@ fn struct_of_map_compare() -> VortexResult<()> { } #[rstest] -fn primitive_comparisons_across_bitmap_words( - #[values( - PType::I8, PType::I16, PType::I32, PType::I64, PType::U8, PType::U16, PType::U32, - PType::U64, PType::F16, PType::F32, PType::F64 - )] - ptype: PType, +fn byte_comparisons_across_bitmap_words( + #[values(PType::I8, PType::U8)] ptype: PType, #[values(0, 1, 63, 64, 65, 129)] len: usize, #[values( CompareOperator::Eq, From d7d9a989dca798ad8b7fc49eed3f50e7fabaf11c Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Wed, 23 Sep 2026 15:50:28 -0400 Subject: [PATCH 5/5] feat: prototype NarrowArray for narrow integer storage Signed-off-by: Matt Katz --- encodings/fastlanes/tests/narrow.rs | 86 +++++ narrow-array-benchmark-results.json | 173 ++++++++++ narrow-array-benchmarks.md | 206 ++++++++++++ narrow-array-design.md | 73 +++++ vortex-array/Cargo.toml | 8 + vortex-array/benches/narrow.rs | 282 ++++++++++++++++ vortex-array/benches/narrow_compare.rs | 195 ++++++++++++ vortex-array/src/arrays/mod.rs | 5 + vortex-array/src/arrays/narrow/compute.rs | 166 ++++++++++ vortex-array/src/arrays/narrow/mod.rs | 122 +++++++ vortex-array/src/arrays/narrow/tests.rs | 372 ++++++++++++++++++++++ vortex-array/src/arrays/narrow/vtable.rs | 153 +++++++++ vortex-array/src/session/mod.rs | 2 + 13 files changed, 1843 insertions(+) create mode 100644 encodings/fastlanes/tests/narrow.rs create mode 100644 narrow-array-benchmark-results.json create mode 100644 narrow-array-benchmarks.md create mode 100644 narrow-array-design.md create mode 100644 vortex-array/benches/narrow.rs create mode 100644 vortex-array/benches/narrow_compare.rs create mode 100644 vortex-array/src/arrays/narrow/compute.rs create mode 100644 vortex-array/src/arrays/narrow/mod.rs create mode 100644 vortex-array/src/arrays/narrow/tests.rs create mode 100644 vortex-array/src/arrays/narrow/vtable.rs diff --git a/encodings/fastlanes/tests/narrow.rs b/encodings/fastlanes/tests/narrow.rs new file mode 100644 index 00000000000..8f482681d6e --- /dev/null +++ b/encodings/fastlanes/tests/narrow.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![cfg(test)] + +use vortex_array::ArrayContext; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::array_session; +use vortex_array::arrays::BoolArray; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::Narrow; +use vortex_array::arrays::NarrowArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::arrays::ScalarFn; +use vortex_array::arrays::narrow::NarrowArraySlotsExt; +use vortex_array::arrays::scalar_fn::ScalarFnArrayExt; +use vortex_array::assert_arrays_eq; +use vortex_array::builtins::ArrayBuiltins; +use vortex_array::dtype::PType; +use vortex_array::scalar_fn::fns::binary::CompareKernel; +use vortex_array::scalar_fn::fns::operators::CompareOperator; +use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::serde::SerializeOptions; +use vortex_array::serde::SerializedArray; +use vortex_buffer::ByteBufferMut; +use vortex_buffer::buffer; +use vortex_error::VortexResult; +use vortex_error::vortex_err; +use vortex_fastlanes::BitPacked; +use vortex_fastlanes::BitPackedData; +use vortex_session::registry::ReadContext; + +#[test] +fn narrow_bitpacked_comparison_selection_and_roundtrip() -> VortexResult<()> { + let session = array_session(); + vortex_fastlanes::initialize(&session); + let mut ctx = session.create_execution_ctx(); + let primitive = PrimitiveArray::from_iter((0u8..32).cycle().take(2300)).into_array(); + let packed = BitPackedData::encode(&primitive, 5, &mut ctx)?.into_array(); + let array = NarrowArray::try_new(packed, PType::U64.into())?; + + let constant = ConstantArray::new(16u64, array.len()).into_array(); + let comparison = Narrow::compare(array.as_view(), &constant, CompareOperator::Lt, &mut ctx)? + .ok_or_else(|| vortex_err!("expected narrow comparison kernel"))?; + let comparison_view = comparison.as_::(); + assert!(comparison_view.get_child(0).is::()); + assert_eq!(comparison_view.get_child(1).dtype(), &PType::U8.into()); + assert_arrays_eq!( + comparison, + BoolArray::from_iter((0..2300).map(|i| i % 32 < 16)), + &mut ctx + ); + + let sliced = array.slice(13..2200)?; + assert!(sliced.as_::().values().is::()); + let taken = sliced.take(buffer![0u32, 1020, 2186].into_array())?; + assert!(taken.is::()); + assert_arrays_eq!(taken, buffer![13u64, 9, 23].into_array(), &mut ctx); + + let array = array.into_array(); + let array_ctx = ArrayContext::empty(); + let mut bytes = ByteBufferMut::empty(); + for buffer in array.serialize(&array_ctx, &session, &SerializeOptions::default())? { + bytes.extend_from_slice(&buffer); + } + let decoded = SerializedArray::try_from(bytes.freeze())?.decode( + array.dtype(), + array.len(), + &ReadContext::new(array_ctx.to_ids()), + &session, + )?; + assert!(decoded.as_::().values().is::()); + assert_eq!(decoded.as_::().values().dtype(), &PType::U8.into()); + assert_arrays_eq!( + decoded, + PrimitiveArray::from_iter((0u64..32).cycle().take(2300)), + &mut ctx + ); + assert_arrays_eq!( + decoded.binary(ConstantArray::new(300u64, 2300).into_array(), Operator::Lt)?, + BoolArray::from_iter([true; 2300]), + &mut ctx + ); + Ok(()) +} diff --git a/narrow-array-benchmark-results.json b/narrow-array-benchmark-results.json new file mode 100644 index 00000000000..d8b8ff4228d --- /dev/null +++ b/narrow-array-benchmark-results.json @@ -0,0 +1,173 @@ +{ + "environment": {"date": "2026-09-23", "cpu": "Apple M5 Max", "os": "macOS 26.6.1 (25G76), aarch64", "rustc": "1.98.0 (88d9e12ae 2026-08-18)", "base": "c215b0e423f558ef83ac0f99841160d70237a9b7", "passes": 3, "samples_per_case_per_pass": 140, "timing_unit": "microseconds", "aggregation": "pooled median, Q1/Q3, and individual pass medians; descriptive, not confidence intervals", "input": "non-null, uncompressed primitive buffers; NarrowArray logical i64; reused inputs, no forced cache flush"}, + "results": [ + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "narrow_i16", "samples": 420, "median_us": 2.625, "q1_us": 2.209, "q3_us": 3.25, "run1_median_us": 2.625, "run2_median_us": 3.292, "run3_median_us": 2.167}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "narrow_i32", "samples": 420, "median_us": 2.5415, "q1_us": 2.125, "q3_us": 3.167, "run1_median_us": 2.541, "run2_median_us": 3.209, "run3_median_us": 2.125}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "narrow_i8", "samples": 420, "median_us": 1.292, "q1_us": 1.125, "q3_us": 1.666, "run1_median_us": 1.292, "run2_median_us": 1.708, "run3_median_us": 1.083}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "primitive_i16", "samples": 420, "median_us": 2.458, "q1_us": 2.042, "q3_us": 3.0, "run1_median_us": 2.458, "run2_median_us": 3.041, "run3_median_us": 2.0}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "primitive_i32", "samples": 420, "median_us": 2.334, "q1_us": 1.958, "q3_us": 2.875, "run1_median_us": 2.334, "run2_median_us": 2.917, "run3_median_us": 1.917}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "primitive_i64", "samples": 420, "median_us": 2.208, "q1_us": 1.791, "q3_us": 2.834, "run1_median_us": 2.208, "run2_median_us": 2.916, "run3_median_us": 1.75}, + {"suite": "compare", "rows": 8192, "operation": "constant_<", "representation": "primitive_i8", "samples": 420, "median_us": 1.084, "q1_us": 0.917, "q3_us": 1.375, "run1_median_us": 1.084, "run2_median_us": 1.416, "run3_median_us": 0.916}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "narrow_i16", "samples": 420, "median_us": 2.75, "q1_us": 2.208, "q3_us": 3.208, "run1_median_us": 2.75, "run2_median_us": 3.209, "run3_median_us": 2.167}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "narrow_i32", "samples": 420, "median_us": 2.625, "q1_us": 2.084, "q3_us": 3.083, "run1_median_us": 2.625, "run2_median_us": 3.125, "run3_median_us": 2.083}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "narrow_i8", "samples": 420, "median_us": 1.3335, "q1_us": 1.083, "q3_us": 1.584, "run1_median_us": 1.3335, "run2_median_us": 1.625, "run3_median_us": 1.042}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "primitive_i16", "samples": 420, "median_us": 2.541, "q1_us": 2.0, "q3_us": 2.917, "run1_median_us": 2.541, "run2_median_us": 2.958, "run3_median_us": 2.0}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "primitive_i32", "samples": 420, "median_us": 2.417, "q1_us": 1.917, "q3_us": 2.833, "run1_median_us": 2.417, "run2_median_us": 2.875, "run3_median_us": 1.917}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "primitive_i64", "samples": 420, "median_us": 2.208, "q1_us": 1.75, "q3_us": 2.708, "run1_median_us": 2.208, "run2_median_us": 2.75, "run3_median_us": 1.75}, + {"suite": "compare", "rows": 8192, "operation": "constant_=", "representation": "primitive_i8", "samples": 420, "median_us": 1.125, "q1_us": 0.917, "q3_us": 1.333, "run1_median_us": 1.125, "run2_median_us": 1.334, "run3_median_us": 0.875}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "narrow_i16", "samples": 420, "median_us": 2.417, "q1_us": 2.042, "q3_us": 2.833, "run1_median_us": 2.417, "run2_median_us": 2.875, "run3_median_us": 2.041}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "narrow_i32", "samples": 420, "median_us": 2.417, "q1_us": 1.959, "q3_us": 2.7397, "run1_median_us": 2.417, "run2_median_us": 2.75, "run3_median_us": 1.958}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "narrow_i8", "samples": 420, "median_us": 1.084, "q1_us": 0.958, "q3_us": 1.25, "run1_median_us": 1.084, "run2_median_us": 1.2705, "run3_median_us": 0.917}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "primitive_i16", "samples": 420, "median_us": 2.375, "q1_us": 2.0, "q3_us": 2.833, "run1_median_us": 2.375, "run2_median_us": 2.833, "run3_median_us": 2.0}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "primitive_i32", "samples": 420, "median_us": 2.375, "q1_us": 1.917, "q3_us": 2.708, "run1_median_us": 2.375, "run2_median_us": 2.75, "run3_median_us": 1.917}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "primitive_i64", "samples": 420, "median_us": 2.709, "q1_us": 2.208, "q3_us": 3.083, "run1_median_us": 2.709, "run2_median_us": 3.084, "run3_median_us": 2.167}, + {"suite": "compare", "rows": 8192, "operation": "pair_<", "representation": "primitive_i8", "samples": 420, "median_us": 1.042, "q1_us": 0.875, "q3_us": 1.2087, "run1_median_us": 1.042, "run2_median_us": 1.25, "run3_median_us": 0.875}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "narrow_i16", "samples": 420, "median_us": 2.417, "q1_us": 2.0412, "q3_us": 2.917, "run1_median_us": 2.417, "run2_median_us": 3.0, "run3_median_us": 2.0}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "narrow_i32", "samples": 420, "median_us": 2.4165, "q1_us": 1.959, "q3_us": 2.958, "run1_median_us": 2.416, "run2_median_us": 3.042, "run3_median_us": 1.958}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "narrow_i8", "samples": 420, "median_us": 1.083, "q1_us": 0.917, "q3_us": 1.292, "run1_median_us": 1.083, "run2_median_us": 1.375, "run3_median_us": 0.9165}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "primitive_i16", "samples": 420, "median_us": 2.375, "q1_us": 2.0, "q3_us": 2.875, "run1_median_us": 2.375, "run2_median_us": 2.917, "run3_median_us": 1.959}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "primitive_i32", "samples": 420, "median_us": 2.375, "q1_us": 1.917, "q3_us": 2.916, "run1_median_us": 2.375, "run2_median_us": 2.959, "run3_median_us": 1.916}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "primitive_i64", "samples": 420, "median_us": 2.708, "q1_us": 2.167, "q3_us": 3.5, "run1_median_us": 2.708, "run2_median_us": 3.6455, "run3_median_us": 2.167}, + {"suite": "compare", "rows": 8192, "operation": "pair_=", "representation": "primitive_i8", "samples": 420, "median_us": 1.042, "q1_us": 0.875, "q3_us": 1.25, "run1_median_us": 1.042, "run2_median_us": 1.292, "run3_median_us": 0.875}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "narrow_i16", "samples": 420, "median_us": 208.604, "q1_us": 176.208, "q3_us": 257.5315, "run1_median_us": 208.583, "run2_median_us": 261.375, "run3_median_us": 175.041}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "narrow_i32", "samples": 420, "median_us": 196.4795, "q1_us": 165.146, "q3_us": 243.0938, "run1_median_us": 196.4165, "run2_median_us": 248.167, "run3_median_us": 163.625}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "narrow_i8", "samples": 420, "median_us": 38.9795, "q1_us": 32.625, "q3_us": 49.6148, "run1_median_us": 38.8955, "run2_median_us": 51.604, "run3_median_us": 32.417}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "primitive_i16", "samples": 420, "median_us": 208.3955, "q1_us": 176.833, "q3_us": 257.6975, "run1_median_us": 208.375, "run2_median_us": 259.958, "run3_median_us": 174.708}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "primitive_i32", "samples": 420, "median_us": 196.042, "q1_us": 164.0523, "q3_us": 242.8745, "run1_median_us": 196.042, "run2_median_us": 246.688, "run3_median_us": 163.292}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "primitive_i64", "samples": 420, "median_us": 171.5, "q1_us": 143.3957, "q3_us": 216.5103, "run1_median_us": 171.375, "run2_median_us": 223.4585, "run3_median_us": 141.3545}, + {"suite": "compare", "rows": 1048576, "operation": "constant_<", "representation": "primitive_i8", "samples": 420, "median_us": 38.5, "q1_us": 32.333, "q3_us": 48.6145, "run1_median_us": 38.5, "run2_median_us": 50.083, "run3_median_us": 32.167}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "narrow_i16", "samples": 420, "median_us": 220.1875, "q1_us": 177.458, "q3_us": 256.375, "run1_median_us": 220.1875, "run2_median_us": 257.4585, "run3_median_us": 175.0}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "narrow_i32", "samples": 420, "median_us": 206.438, "q1_us": 166.1772, "q3_us": 241.5837, "run1_median_us": 206.3335, "run2_median_us": 243.1875, "run3_median_us": 163.583}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "narrow_i8", "samples": 420, "median_us": 41.667, "q1_us": 32.542, "q3_us": 49.302, "run1_median_us": 41.5, "run2_median_us": 50.4375, "run3_median_us": 32.334}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "primitive_i16", "samples": 420, "median_us": 219.7295, "q1_us": 177.2185, "q3_us": 255.8228, "run1_median_us": 219.709, "run2_median_us": 256.4585, "run3_median_us": 174.708}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "primitive_i32", "samples": 420, "median_us": 205.375, "q1_us": 165.625, "q3_us": 240.1978, "run1_median_us": 205.375, "run2_median_us": 242.0835, "run3_median_us": 163.333}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "primitive_i64", "samples": 420, "median_us": 179.354, "q1_us": 143.812, "q3_us": 210.0417, "run1_median_us": 179.187, "run2_median_us": 215.5625, "run3_median_us": 141.125}, + {"suite": "compare", "rows": 1048576, "operation": "constant_=", "representation": "primitive_i8", "samples": 420, "median_us": 40.625, "q1_us": 32.2912, "q3_us": 47.9588, "run1_median_us": 40.625, "run2_median_us": 48.813, "run3_median_us": 32.125}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "narrow_i16", "samples": 420, "median_us": 256.9375, "q1_us": 178.1772, "q3_us": 262.2815, "run1_median_us": 258.4795, "run2_median_us": 261.7295, "run3_median_us": 175.75}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "narrow_i32", "samples": 420, "median_us": 243.25, "q1_us": 166.3022, "q3_us": 257.3645, "run1_median_us": 244.104, "run2_median_us": 251.708, "run3_median_us": 165.3955}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "narrow_i8", "samples": 420, "median_us": 49.1665, "q1_us": 35.0102, "q3_us": 53.969, "run1_median_us": 50.3545, "run2_median_us": 53.3125, "run3_median_us": 34.7085}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "primitive_i16", "samples": 420, "median_us": 257.2705, "q1_us": 176.2605, "q3_us": 263.6875, "run1_median_us": 258.7715, "run2_median_us": 262.604, "run3_median_us": 175.625}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "primitive_i32", "samples": 420, "median_us": 242.75, "q1_us": 167.2912, "q3_us": 254.771, "run1_median_us": 243.9375, "run2_median_us": 248.979, "run3_median_us": 165.3955}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "primitive_i64", "samples": 420, "median_us": 296.021, "q1_us": 245.6355, "q3_us": 372.885, "run1_median_us": 303.625, "run2_median_us": 372.854, "run3_median_us": 243.729}, + {"suite": "compare", "rows": 1048576, "operation": "pair_<", "representation": "primitive_i8", "samples": 420, "median_us": 48.375, "q1_us": 34.75, "q3_us": 52.385, "run1_median_us": 48.75, "run2_median_us": 52.75, "run3_median_us": 34.458}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "narrow_i16", "samples": 420, "median_us": 257.896, "q1_us": 176.0935, "q3_us": 262.875, "run1_median_us": 258.7085, "run2_median_us": 263.146, "run3_median_us": 175.708}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "narrow_i32", "samples": 420, "median_us": 244.584, "q1_us": 166.5215, "q3_us": 251.1978, "run1_median_us": 245.354, "run2_median_us": 252.6665, "run3_median_us": 165.2915}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "narrow_i8", "samples": 420, "median_us": 49.6875, "q1_us": 34.9162, "q3_us": 54.4057, "run1_median_us": 49.9165, "run2_median_us": 55.0415, "run3_median_us": 34.4795}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "primitive_i16", "samples": 420, "median_us": 257.875, "q1_us": 176.2602, "q3_us": 263.834, "run1_median_us": 258.313, "run2_median_us": 264.813, "run3_median_us": 175.6665}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "primitive_i32", "samples": 420, "median_us": 244.792, "q1_us": 165.8025, "q3_us": 252.6145, "run1_median_us": 245.417, "run2_median_us": 253.417, "run3_median_us": 165.167}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "primitive_i64", "samples": 420, "median_us": 305.4375, "q1_us": 245.1668, "q3_us": 388.5525, "run1_median_us": 305.4375, "run2_median_us": 394.187, "run3_median_us": 243.3335}, + {"suite": "compare", "rows": 1048576, "operation": "pair_=", "representation": "primitive_i8", "samples": 420, "median_us": 48.771, "q1_us": 34.6773, "q3_us": 53.5315, "run1_median_us": 48.9165, "run2_median_us": 54.271, "run3_median_us": 34.3335}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "narrow_i16", "samples": 420, "median_us": 4217.9375, "q1_us": 2807.25, "q3_us": 4744.125, "run1_median_us": 4482.521, "run2_median_us": 4505.9375, "run3_median_us": 2794.4375}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "narrow_i32", "samples": 420, "median_us": 3942.4585, "q1_us": 2628.4167, "q3_us": 4409.2288, "run1_median_us": 4235.9165, "run2_median_us": 4199.896, "run3_median_us": 2615.4375}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "narrow_i8", "samples": 420, "median_us": 738.854, "q1_us": 505.7917, "q3_us": 794.0102, "run1_median_us": 751.4165, "run2_median_us": 777.7915, "run3_median_us": 499.3125}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "primitive_i16", "samples": 420, "median_us": 4235.1875, "q1_us": 2807.2185, "q3_us": 4808.2182, "run1_median_us": 4472.2295, "run2_median_us": 4620.75, "run3_median_us": 2794.292}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "primitive_i32", "samples": 420, "median_us": 3990.1455, "q1_us": 2626.385, "q3_us": 4442.1355, "run1_median_us": 4220.3125, "run2_median_us": 4245.771, "run3_median_us": 2612.396}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "primitive_i64", "samples": 420, "median_us": 3568.771, "q1_us": 2299.3958, "q3_us": 4102.656, "run1_median_us": 3754.896, "run2_median_us": 4011.625, "run3_median_us": 2282.479}, + {"suite": "compare", "rows": 16777216, "operation": "constant_<", "representation": "primitive_i8", "samples": 420, "median_us": 734.7915, "q1_us": 502.24, "q3_us": 802.7395, "run1_median_us": 765.771, "run2_median_us": 776.854, "run3_median_us": 497.9165}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "narrow_i16", "samples": 420, "median_us": 3173.813, "q1_us": 2797.7082, "q3_us": 4094.4275, "run1_median_us": 3173.813, "run2_median_us": 4083.104, "run3_median_us": 2791.0005}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "narrow_i32", "samples": 420, "median_us": 2981.5205, "q1_us": 2622.4378, "q3_us": 3838.6142, "run1_median_us": 2981.5205, "run2_median_us": 3826.75, "run3_median_us": 2612.4375}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "narrow_i8", "samples": 420, "median_us": 566.854, "q1_us": 500.8018, "q3_us": 731.3338, "run1_median_us": 566.854, "run2_median_us": 732.0415, "run3_median_us": 499.1455}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "primitive_i16", "samples": 420, "median_us": 3175.146, "q1_us": 2795.1355, "q3_us": 4081.7497, "run1_median_us": 3175.146, "run2_median_us": 4077.4165, "run3_median_us": 2787.292}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "primitive_i32", "samples": 420, "median_us": 2953.396, "q1_us": 2617.0727, "q3_us": 3823.583, "run1_median_us": 2951.6875, "run2_median_us": 3814.042, "run3_median_us": 2611.3335}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "primitive_i64", "samples": 420, "median_us": 2605.375, "q1_us": 2289.021, "q3_us": 3379.9585, "run1_median_us": 2605.375, "run2_median_us": 3359.854, "run3_median_us": 2279.6875}, + {"suite": "compare", "rows": 16777216, "operation": "constant_=", "representation": "primitive_i8", "samples": 420, "median_us": 571.7915, "q1_us": 499.3023, "q3_us": 732.0935, "run1_median_us": 571.7915, "run2_median_us": 731.0, "run3_median_us": 497.396}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "narrow_i16", "samples": 420, "median_us": 2815.9375, "q1_us": 2797.3643, "q3_us": 3575.6985, "run1_median_us": 2798.146, "run2_median_us": 4070.6665, "run3_median_us": 2805.6665}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "narrow_i32", "samples": 420, "median_us": 2655.8745, "q1_us": 2636.6352, "q3_us": 3441.5727, "run1_median_us": 2639.0625, "run2_median_us": 3864.146, "run3_median_us": 2645.1665}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "narrow_i8", "samples": 420, "median_us": 544.5205, "q1_us": 537.3853, "q3_us": 715.6037, "run1_median_us": 539.479, "run2_median_us": 754.0005, "run3_median_us": 538.917}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "primitive_i16", "samples": 420, "median_us": 2818.25, "q1_us": 2798.2605, "q3_us": 3656.302, "run1_median_us": 2798.271, "run2_median_us": 4076.1665, "run3_median_us": 2809.6875}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "primitive_i32", "samples": 420, "median_us": 2657.4165, "q1_us": 2637.125, "q3_us": 3435.0932, "run1_median_us": 2637.688, "run2_median_us": 3857.604, "run3_median_us": 2650.813}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "primitive_i64", "samples": 420, "median_us": 3905.5205, "q1_us": 3877.0205, "q3_us": 4888.0938, "run1_median_us": 3886.1875, "run2_median_us": 5125.271, "run3_median_us": 3884.5}, + {"suite": "compare", "rows": 16777216, "operation": "pair_<", "representation": "primitive_i8", "samples": 420, "median_us": 544.5625, "q1_us": 536.9685, "q3_us": 712.646, "run1_median_us": 538.4165, "run2_median_us": 772.583, "run3_median_us": 538.75}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "narrow_i16", "samples": 420, "median_us": 2858.5205, "q1_us": 2802.1357, "q3_us": 3540.6672, "run1_median_us": 2825.2505, "run2_median_us": 3570.4165, "run3_median_us": 2805.0625}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "narrow_i32", "samples": 420, "median_us": 2726.3955, "q1_us": 2644.2915, "q3_us": 3361.552, "run1_median_us": 2682.354, "run2_median_us": 3388.875, "run3_median_us": 2646.5}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "narrow_i8", "samples": 420, "median_us": 569.854, "q1_us": 538.948, "q3_us": 690.2082, "run1_median_us": 556.229, "run2_median_us": 696.7705, "run3_median_us": 538.5415}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "primitive_i16", "samples": 420, "median_us": 2895.146, "q1_us": 2804.9275, "q3_us": 3547.7395, "run1_median_us": 2831.75, "run2_median_us": 3563.542, "run3_median_us": 2809.3335}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "primitive_i32", "samples": 420, "median_us": 2770.875, "q1_us": 2645.3755, "q3_us": 3361.833, "run1_median_us": 2681.8335, "run2_median_us": 3384.9165, "run3_median_us": 2648.396}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "primitive_i64", "samples": 420, "median_us": 4040.042, "q1_us": 3879.7915, "q3_us": 4965.4163, "run1_median_us": 3971.792, "run2_median_us": 5014.3955, "run3_median_us": 3876.563}, + {"suite": "compare", "rows": 16777216, "operation": "pair_=", "representation": "primitive_i8", "samples": 420, "median_us": 567.021, "q1_us": 537.9692, "q3_us": 690.7703, "run1_median_us": 550.8335, "run2_median_us": 700.042, "run3_median_us": 537.9375}, + {"suite": "operations", "rows": 8192, "operation": "CompareFilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 5.292, "q1_us": 5.167, "q3_us": 6.416, "run1_median_us": 5.25, "run2_median_us": 6.4585, "run3_median_us": 5.167}, + {"suite": "operations", "rows": 8192, "operation": "CompareFilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 5.8335, "q1_us": 5.6353, "q3_us": 7.0, "run1_median_us": 5.75, "run2_median_us": 7.084, "run3_median_us": 5.625}, + {"suite": "operations", "rows": 8192, "operation": "CompareFilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 5.875, "q1_us": 5.75, "q3_us": 7.1147, "run1_median_us": 5.834, "run2_median_us": 7.167, "run3_median_us": 5.75}, + {"suite": "operations", "rows": 8192, "operation": "CompareFilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 7.459, "q1_us": 7.0105, "q3_us": 8.0, "run1_median_us": 7.354, "run2_median_us": 7.459, "run3_median_us": 7.583}, + {"suite": "operations", "rows": 8192, "operation": "Encode", "representation": "narrow_i16", "samples": 420, "median_us": 3.959, "q1_us": 3.916, "q3_us": 4.916, "run1_median_us": 3.917, "run2_median_us": 4.959, "run3_median_us": 3.917}, + {"suite": "operations", "rows": 8192, "operation": "Encode", "representation": "narrow_i32", "samples": 420, "median_us": 4.3955, "q1_us": 4.333, "q3_us": 5.417, "run1_median_us": 4.334, "run2_median_us": 5.459, "run3_median_us": 4.333}, + {"suite": "operations", "rows": 8192, "operation": "Encode", "representation": "narrow_i8", "samples": 420, "median_us": 3.792, "q1_us": 3.709, "q3_us": 4.667, "run1_median_us": 3.75, "run2_median_us": 4.709, "run3_median_us": 3.75}, + {"suite": "operations", "rows": 8192, "operation": "Encode", "representation": "primitive_i64", "samples": 420, "median_us": 2.875, "q1_us": 2.792, "q3_us": 3.542, "run1_median_us": 2.833, "run2_median_us": 3.583, "run3_median_us": 2.792}, + {"suite": "operations", "rows": 8192, "operation": "FilterCompare", "representation": "narrow_i16", "samples": 420, "median_us": 3.417, "q1_us": 3.375, "q3_us": 4.125, "run1_median_us": 3.416, "run2_median_us": 4.167, "run3_median_us": 3.375}, + {"suite": "operations", "rows": 8192, "operation": "FilterCompare", "representation": "narrow_i32", "samples": 420, "median_us": 3.875, "q1_us": 3.791, "q3_us": 4.666, "run1_median_us": 3.833, "run2_median_us": 4.708, "run3_median_us": 3.792}, + {"suite": "operations", "rows": 8192, "operation": "FilterCompare", "representation": "narrow_i8", "samples": 420, "median_us": 4.333, "q1_us": 4.25, "q3_us": 5.208, "run1_median_us": 4.291, "run2_median_us": 5.25, "run3_median_us": 4.25}, + {"suite": "operations", "rows": 8192, "operation": "FilterCompare", "representation": "primitive_i64", "samples": 420, "median_us": 6.625, "q1_us": 5.333, "q3_us": 8.5838, "run1_median_us": 5.604, "run2_median_us": 6.792, "run3_median_us": 5.4375}, + {"suite": "operations", "rows": 8192, "operation": "FilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 2.875, "q1_us": 2.792, "q3_us": 3.5, "run1_median_us": 2.833, "run2_median_us": 3.542, "run3_median_us": 2.792}, + {"suite": "operations", "rows": 8192, "operation": "FilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 3.5, "q1_us": 3.333, "q3_us": 4.167, "run1_median_us": 3.417, "run2_median_us": 4.25, "run3_median_us": 3.334}, + {"suite": "operations", "rows": 8192, "operation": "FilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 4.542, "q1_us": 4.459, "q3_us": 5.583, "run1_median_us": 4.541, "run2_median_us": 5.625, "run3_median_us": 4.4585}, + {"suite": "operations", "rows": 8192, "operation": "FilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 4.125, "q1_us": 3.875, "q3_us": 4.8228, "run1_median_us": 3.875, "run2_median_us": 4.8955, "run3_median_us": 3.917}, + {"suite": "operations", "rows": 8192, "operation": "MaterializeI64", "representation": "narrow_i16", "samples": 420, "median_us": 1.542, "q1_us": 1.5, "q3_us": 1.833, "run1_median_us": 1.5, "run2_median_us": 1.875, "run3_median_us": 1.5}, + {"suite": "operations", "rows": 8192, "operation": "MaterializeI64", "representation": "narrow_i32", "samples": 420, "median_us": 1.625, "q1_us": 1.583, "q3_us": 1.917, "run1_median_us": 1.584, "run2_median_us": 1.959, "run3_median_us": 1.584}, + {"suite": "operations", "rows": 8192, "operation": "MaterializeI64", "representation": "narrow_i8", "samples": 420, "median_us": 2.084, "q1_us": 2.083, "q3_us": 2.542, "run1_median_us": 2.083, "run2_median_us": 2.583, "run3_median_us": 2.083}, + {"suite": "operations", "rows": 8192, "operation": "MaterializeI64", "representation": "primitive_i64", "samples": 420, "median_us": 0.083, "q1_us": 0.042, "q3_us": 0.083, "run1_median_us": 0.083, "run2_median_us": 0.083, "run3_median_us": 0.083}, + {"suite": "operations", "rows": 8192, "operation": "TakeCompare", "representation": "narrow_i16", "samples": 420, "median_us": 3.209, "q1_us": 3.1662, "q3_us": 3.833, "run1_median_us": 3.167, "run2_median_us": 3.875, "run3_median_us": 3.208}, + {"suite": "operations", "rows": 8192, "operation": "TakeCompare", "representation": "narrow_i32", "samples": 420, "median_us": 3.292, "q1_us": 3.209, "q3_us": 3.834, "run1_median_us": 3.25, "run2_median_us": 3.8955, "run3_median_us": 3.25}, + {"suite": "operations", "rows": 8192, "operation": "TakeCompare", "representation": "narrow_i8", "samples": 420, "median_us": 3.125, "q1_us": 3.042, "q3_us": 3.625, "run1_median_us": 3.042, "run2_median_us": 3.708, "run3_median_us": 3.083}, + {"suite": "operations", "rows": 8192, "operation": "TakeCompare", "representation": "primitive_i64", "samples": 420, "median_us": 2.25, "q1_us": 2.167, "q3_us": 2.625, "run1_median_us": 2.208, "run2_median_us": 2.667, "run3_median_us": 2.209}, + {"suite": "operations", "rows": 8192, "operation": "TakeToI64", "representation": "narrow_i16", "samples": 420, "median_us": 3.75, "q1_us": 3.625, "q3_us": 4.542, "run1_median_us": 3.667, "run2_median_us": 4.625, "run3_median_us": 3.625}, + {"suite": "operations", "rows": 8192, "operation": "TakeToI64", "representation": "narrow_i32", "samples": 420, "median_us": 4.042, "q1_us": 3.833, "q3_us": 4.75, "run1_median_us": 3.9375, "run2_median_us": 4.875, "run3_median_us": 3.8125}, + {"suite": "operations", "rows": 8192, "operation": "TakeToI64", "representation": "narrow_i8", "samples": 420, "median_us": 4.166, "q1_us": 4.042, "q3_us": 5.125, "run1_median_us": 4.125, "run2_median_us": 5.167, "run3_median_us": 4.042}, + {"suite": "operations", "rows": 8192, "operation": "TakeToI64", "representation": "primitive_i64", "samples": 420, "median_us": 1.209, "q1_us": 1.167, "q3_us": 1.459, "run1_median_us": 1.209, "run2_median_us": 1.5, "run3_median_us": 1.167}, + {"suite": "operations", "rows": 1048576, "operation": "CompareFilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 342.271, "q1_us": 334.625, "q3_us": 419.084, "run1_median_us": 336.5625, "run2_median_us": 420.8125, "run3_median_us": 335.2705}, + {"suite": "operations", "rows": 1048576, "operation": "CompareFilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 402.625, "q1_us": 393.875, "q3_us": 494.375, "run1_median_us": 398.8335, "run2_median_us": 497.5835, "run3_median_us": 393.9165}, + {"suite": "operations", "rows": 1048576, "operation": "CompareFilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 382.1665, "q1_us": 376.667, "q3_us": 472.1042, "run1_median_us": 378.375, "run2_median_us": 474.0205, "run3_median_us": 377.604}, + {"suite": "operations", "rows": 1048576, "operation": "CompareFilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 458.1045, "q1_us": 451.791, "q3_us": 563.7918, "run1_median_us": 454.0415, "run2_median_us": 567.729, "run3_median_us": 453.167}, + {"suite": "operations", "rows": 1048576, "operation": "Encode", "representation": "narrow_i16", "samples": 420, "median_us": 355.708, "q1_us": 353.5942, "q3_us": 359.8338, "run1_median_us": 356.4165, "run2_median_us": 356.1455, "run3_median_us": 354.375}, + {"suite": "operations", "rows": 1048576, "operation": "Encode", "representation": "narrow_i32", "samples": 420, "median_us": 384.0415, "q1_us": 382.2192, "q3_us": 389.8015, "run1_median_us": 385.5415, "run2_median_us": 383.813, "run3_median_us": 382.6875}, + {"suite": "operations", "rows": 1048576, "operation": "Encode", "representation": "narrow_i8", "samples": 420, "median_us": 334.1455, "q1_us": 332.333, "q3_us": 338.7808, "run1_median_us": 335.2705, "run2_median_us": 334.1665, "run3_median_us": 333.5625}, + {"suite": "operations", "rows": 1048576, "operation": "Encode", "representation": "primitive_i64", "samples": 420, "median_us": 212.958, "q1_us": 212.291, "q3_us": 216.1975, "run1_median_us": 213.9375, "run2_median_us": 213.417, "run3_median_us": 212.4165}, + {"suite": "operations", "rows": 1048576, "operation": "FilterCompare", "representation": "narrow_i16", "samples": 420, "median_us": 156.5835, "q1_us": 154.917, "q3_us": 172.6245, "run1_median_us": 155.4585, "run2_median_us": 173.1665, "run3_median_us": 154.9795}, + {"suite": "operations", "rows": 1048576, "operation": "FilterCompare", "representation": "narrow_i32", "samples": 420, "median_us": 211.583, "q1_us": 209.2913, "q3_us": 232.7398, "run1_median_us": 210.479, "run2_median_us": 233.9375, "run3_median_us": 209.416}, + {"suite": "operations", "rows": 1048576, "operation": "FilterCompare", "representation": "narrow_i8", "samples": 420, "median_us": 264.7295, "q1_us": 263.6352, "q3_us": 293.6042, "run1_median_us": 263.8335, "run2_median_us": 294.0, "run3_median_us": 263.792}, + {"suite": "operations", "rows": 1048576, "operation": "FilterCompare", "representation": "primitive_i64", "samples": 420, "median_us": 379.708, "q1_us": 376.125, "q3_us": 417.2188, "run1_median_us": 376.6245, "run2_median_us": 418.937, "run3_median_us": 378.2085}, + {"suite": "operations", "rows": 1048576, "operation": "FilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 156.021, "q1_us": 154.125, "q3_us": 193.7398, "run1_median_us": 154.25, "run2_median_us": 194.6875, "run3_median_us": 154.458}, + {"suite": "operations", "rows": 1048576, "operation": "FilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 227.271, "q1_us": 224.4163, "q3_us": 281.7292, "run1_median_us": 224.9585, "run2_median_us": 283.291, "run3_median_us": 224.4375}, + {"suite": "operations", "rows": 1048576, "operation": "FilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 342.687, "q1_us": 339.709, "q3_us": 425.6042, "run1_median_us": 340.208, "run2_median_us": 426.6045, "run3_median_us": 340.1045}, + {"suite": "operations", "rows": 1048576, "operation": "FilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 304.854, "q1_us": 301.5102, "q3_us": 378.4893, "run1_median_us": 302.666, "run2_median_us": 380.375, "run3_median_us": 301.625}, + {"suite": "operations", "rows": 1048576, "operation": "MaterializeI64", "representation": "narrow_i16", "samples": 420, "median_us": 177.875, "q1_us": 175.4272, "q3_us": 194.4895, "run1_median_us": 175.396, "run2_median_us": 195.083, "run3_median_us": 176.833}, + {"suite": "operations", "rows": 1048576, "operation": "MaterializeI64", "representation": "narrow_i32", "samples": 420, "median_us": 199.5835, "q1_us": 197.0935, "q3_us": 218.4587, "run1_median_us": 196.9165, "run2_median_us": 218.833, "run3_median_us": 198.438}, + {"suite": "operations", "rows": 1048576, "operation": "MaterializeI64", "representation": "narrow_i8", "samples": 420, "median_us": 188.8955, "q1_us": 186.625, "q3_us": 207.25, "run1_median_us": 186.812, "run2_median_us": 207.4795, "run3_median_us": 187.458}, + {"suite": "operations", "rows": 1048576, "operation": "MaterializeI64", "representation": "primitive_i64", "samples": 420, "median_us": 0.083, "q1_us": 0.042, "q3_us": 0.083, "run1_median_us": 0.042, "run2_median_us": 0.083, "run3_median_us": 0.042}, + {"suite": "operations", "rows": 1048576, "operation": "TakeCompare", "representation": "narrow_i16", "samples": 420, "median_us": 153.583, "q1_us": 146.9998, "q3_us": 160.4898, "run1_median_us": 153.854, "run2_median_us": 160.375, "run3_median_us": 147.75}, + {"suite": "operations", "rows": 1048576, "operation": "TakeCompare", "representation": "narrow_i32", "samples": 420, "median_us": 165.1665, "q1_us": 154.7918, "q3_us": 176.709, "run1_median_us": 164.062, "run2_median_us": 176.896, "run3_median_us": 155.4375}, + {"suite": "operations", "rows": 1048576, "operation": "TakeCompare", "representation": "narrow_i8", "samples": 420, "median_us": 120.75, "q1_us": 116.667, "q3_us": 125.5205, "run1_median_us": 120.958, "run2_median_us": 125.417, "run3_median_us": 117.1045}, + {"suite": "operations", "rows": 1048576, "operation": "TakeCompare", "representation": "primitive_i64", "samples": 420, "median_us": 167.625, "q1_us": 152.604, "q3_us": 189.5835, "run1_median_us": 168.333, "run2_median_us": 193.021, "run3_median_us": 150.146}, + {"suite": "operations", "rows": 1048576, "operation": "TakeToI64", "representation": "narrow_i16", "samples": 420, "median_us": 308.3335, "q1_us": 302.4273, "q3_us": 397.75, "run1_median_us": 305.75, "run2_median_us": 412.833, "run3_median_us": 302.625}, + {"suite": "operations", "rows": 1048576, "operation": "TakeToI64", "representation": "narrow_i32", "samples": 420, "median_us": 331.958, "q1_us": 324.3852, "q3_us": 433.6562, "run1_median_us": 328.5, "run2_median_us": 451.646, "run3_median_us": 324.333}, + {"suite": "operations", "rows": 1048576, "operation": "TakeToI64", "representation": "narrow_i8", "samples": 420, "median_us": 321.3955, "q1_us": 313.7192, "q3_us": 413.833, "run1_median_us": 317.4585, "run2_median_us": 430.875, "run3_median_us": 313.9165}, + {"suite": "operations", "rows": 1048576, "operation": "TakeToI64", "representation": "primitive_i64", "samples": 420, "median_us": 215.771, "q1_us": 164.2082, "q3_us": 263.6255, "run1_median_us": 214.854, "run2_median_us": 275.1045, "run3_median_us": 150.9585}, + {"suite": "operations", "rows": 16777216, "operation": "CompareFilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 5343.479, "q1_us": 5298.6038, "q3_us": 8164.0522, "run1_median_us": 8534.0835, "run2_median_us": 5299.333, "run3_median_us": 5324.3955}, + {"suite": "operations", "rows": 16777216, "operation": "CompareFilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 6389.0415, "q1_us": 6251.802, "q3_us": 9801.3752, "run1_median_us": 10392.979, "run2_median_us": 6265.0205, "run3_median_us": 6280.4165}, + {"suite": "operations", "rows": 16777216, "operation": "CompareFilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 6027.1665, "q1_us": 5979.4272, "q3_us": 7809.99, "run1_median_us": 8167.6045, "run2_median_us": 5980.2085, "run3_median_us": 6000.7915}, + {"suite": "operations", "rows": 16777216, "operation": "CompareFilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 7328.417, "q1_us": 7174.823, "q3_us": 12702.1353, "run1_median_us": 13463.2705, "run2_median_us": 7185.25, "run3_median_us": 7205.8125}, + {"suite": "operations", "rows": 16777216, "operation": "Encode", "representation": "narrow_i16", "samples": 420, "median_us": 5742.083, "q1_us": 5695.7605, "q3_us": 7301.792, "run1_median_us": 8137.771, "run2_median_us": 5698.125, "run3_median_us": 5721.875}, + {"suite": "operations", "rows": 16777216, "operation": "Encode", "representation": "narrow_i32", "samples": 420, "median_us": 6205.8545, "q1_us": 6145.8438, "q3_us": 8071.969, "run1_median_us": 8822.2505, "run2_median_us": 6152.9165, "run3_median_us": 6181.3545}, + {"suite": "operations", "rows": 16777216, "operation": "Encode", "representation": "narrow_i8", "samples": 420, "median_us": 5420.333, "q1_us": 5369.4062, "q3_us": 7052.9065, "run1_median_us": 7754.5205, "run2_median_us": 5374.083, "run3_median_us": 5396.8125}, + {"suite": "operations", "rows": 16777216, "operation": "Encode", "representation": "primitive_i64", "samples": 420, "median_us": 3439.958, "q1_us": 3405.229, "q3_us": 4508.208, "run1_median_us": 4812.521, "run2_median_us": 3408.792, "run3_median_us": 3423.0}, + {"suite": "operations", "rows": 16777216, "operation": "FilterCompare", "representation": "narrow_i16", "samples": 420, "median_us": 2483.3335, "q1_us": 2460.5628, "q3_us": 3319.542, "run1_median_us": 3632.6665, "run2_median_us": 2461.979, "run3_median_us": 2472.4165}, + {"suite": "operations", "rows": 16777216, "operation": "FilterCompare", "representation": "narrow_i32", "samples": 420, "median_us": 3367.042, "q1_us": 3330.3547, "q3_us": 4462.833, "run1_median_us": 4930.6455, "run2_median_us": 3335.0415, "run3_median_us": 3345.0625}, + {"suite": "operations", "rows": 16777216, "operation": "FilterCompare", "representation": "narrow_i8", "samples": 420, "median_us": 4225.4375, "q1_us": 4197.3435, "q3_us": 5232.021, "run1_median_us": 5287.2295, "run2_median_us": 4203.729, "run3_median_us": 4206.1255}, + {"suite": "operations", "rows": 16777216, "operation": "FilterCompare", "representation": "primitive_i64", "samples": 420, "median_us": 6034.896, "q1_us": 5984.5005, "q3_us": 8269.208, "run1_median_us": 8870.917, "run2_median_us": 5991.1455, "run3_median_us": 6007.292}, + {"suite": "operations", "rows": 16777216, "operation": "FilterToI64", "representation": "narrow_i16", "samples": 420, "median_us": 2505.208, "q1_us": 2464.8755, "q3_us": 2844.4482, "run1_median_us": 3486.1245, "run2_median_us": 2732.6875, "run3_median_us": 2463.604}, + {"suite": "operations", "rows": 16777216, "operation": "FilterToI64", "representation": "narrow_i32", "samples": 420, "median_us": 3663.042, "q1_us": 3599.4273, "q3_us": 4388.1247, "run1_median_us": 5274.083, "run2_median_us": 3984.417, "run3_median_us": 3595.3745}, + {"suite": "operations", "rows": 16777216, "operation": "FilterToI64", "representation": "narrow_i8", "samples": 420, "median_us": 5478.208, "q1_us": 5435.5628, "q3_us": 6351.6355, "run1_median_us": 6604.0625, "run2_median_us": 6035.708, "run3_median_us": 5436.0625}, + {"suite": "operations", "rows": 16777216, "operation": "FilterToI64", "representation": "primitive_i64", "samples": 420, "median_us": 4898.375, "q1_us": 4846.3642, "q3_us": 5613.3128, "run1_median_us": 7793.4375, "run2_median_us": 5389.4795, "run3_median_us": 4843.646}, + {"suite": "operations", "rows": 16777216, "operation": "MaterializeI64", "representation": "narrow_i16", "samples": 420, "median_us": 2862.833, "q1_us": 2803.8857, "q3_us": 3974.7082, "run1_median_us": 4316.7085, "run2_median_us": 2803.2915, "run3_median_us": 2857.3335}, + {"suite": "operations", "rows": 16777216, "operation": "MaterializeI64", "representation": "narrow_i32", "samples": 420, "median_us": 3204.271, "q1_us": 3147.7295, "q3_us": 4604.8228, "run1_median_us": 5103.6875, "run2_median_us": 3147.4585, "run3_median_us": 3189.104}, + {"suite": "operations", "rows": 16777216, "operation": "MaterializeI64", "representation": "narrow_i8", "samples": 420, "median_us": 3036.9585, "q1_us": 2986.1352, "q3_us": 4622.948, "run1_median_us": 4982.3745, "run2_median_us": 2985.5, "run3_median_us": 3030.833}, + {"suite": "operations", "rows": 16777216, "operation": "MaterializeI64", "representation": "primitive_i64", "samples": 420, "median_us": 0.25, "q1_us": 0.083, "q3_us": 0.5, "run1_median_us": 0.583, "run2_median_us": 0.084, "run3_median_us": 0.25}, + {"suite": "operations", "rows": 16777216, "operation": "TakeCompare", "representation": "narrow_i16", "samples": 420, "median_us": 6757.5, "q1_us": 6166.3327, "q3_us": 9150.9895, "run1_median_us": 10341.9795, "run2_median_us": 6233.5835, "run3_median_us": 6450.354}, + {"suite": "operations", "rows": 16777216, "operation": "TakeCompare", "representation": "narrow_i32", "samples": 420, "median_us": 8211.5625, "q1_us": 7841.7397, "q3_us": 10887.219, "run1_median_us": 12100.8545, "run2_median_us": 8007.625, "run3_median_us": 8025.479}, + {"suite": "operations", "rows": 16777216, "operation": "TakeCompare", "representation": "narrow_i8", "samples": 420, "median_us": 3897.2085, "q1_us": 3421.3335, "q3_us": 6483.042, "run1_median_us": 7346.0205, "run2_median_us": 3474.125, "run3_median_us": 3697.3545}, + {"suite": "operations", "rows": 16777216, "operation": "TakeCompare", "representation": "primitive_i64", "samples": 420, "median_us": 9435.104, "q1_us": 9005.0832, "q3_us": 11338.6668, "run1_median_us": 13161.271, "run2_median_us": 9323.958, "run3_median_us": 9061.25}, + {"suite": "operations", "rows": 16777216, "operation": "TakeToI64", "representation": "narrow_i16", "samples": 420, "median_us": 11896.354, "q1_us": 11023.0627, "q3_us": 15878.8962, "run1_median_us": 18157.1875, "run2_median_us": 10873.792, "run3_median_us": 11842.75}, + {"suite": "operations", "rows": 16777216, "operation": "TakeToI64", "representation": "narrow_i32", "samples": 420, "median_us": 12349.3125, "q1_us": 11440.2605, "q3_us": 16789.9372, "run1_median_us": 19723.75, "run2_median_us": 11348.562, "run3_median_us": 12318.396}, + {"suite": "operations", "rows": 16777216, "operation": "TakeToI64", "representation": "narrow_i8", "samples": 420, "median_us": 12101.7915, "q1_us": 11292.781, "q3_us": 18038.9162, "run1_median_us": 20118.917, "run2_median_us": 11137.479, "run3_median_us": 12027.9165}, + {"suite": "operations", "rows": 16777216, "operation": "TakeToI64", "representation": "primitive_i64", "samples": 420, "median_us": 9284.688, "q1_us": 8312.771, "q3_us": 11582.052, "run1_median_us": 12790.0625, "run2_median_us": 8209.146, "run3_median_us": 9284.688} + ] +} diff --git a/narrow-array-benchmarks.md b/narrow-array-benchmarks.md new file mode 100644 index 00000000000..efc300efc2c --- /dev/null +++ b/narrow-array-benchmarks.md @@ -0,0 +1,206 @@ + + + +# NarrowArray benchmarks + +Measured on 2026-09-23 against comparison PR #9948 at +`c215b0e423f558ef83ac0f99841160d70237a9b7`. All logical-i64 comparisons use the +same executable and kernels. The existing primitive comparison benchmarks and kernels +are unchanged by this draft. + +The strongest measured compute benefit is i8 comparison. Smaller storage alone does not +make every operation faster: i32/i16 constant comparison and gathers that return i64 +remain slower in this prototype. These are array microbenchmarks, not end-to-end queries. + +## Buffer size + +These are measured `ArrayRef::nbytes()` values for 1,048,576 non-null rows: + +| Logical i64 representation | Buffer bytes | MiB | Reduction | +| --- | ---: | ---: | ---: | +| Primitive i64 | 8,388,608 | 8 | baseline | +| Narrow over i32 | 4,194,304 | 4 | 50% | +| Narrow over i16 | 2,097,152 | 2 | 75% | +| Narrow over i8 | 1,048,576 | 1 | 87.5% | + +The same ratios were verified at 8,192 and 16,777,216 rows. NarrowArray owns no value +buffers; the child holds the narrower buffer. `nbytes()` does not count array objects, +statistics, allocator bookkeeping, or process RSS. Nullable arrays also retain validity +storage. No file-compression or serialized-size gain is claimed here. + +## Comparisons at 1,048,576 rows + +Pooled median time in microseconds; lower is better: + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| `x < 16` | 171.50 | 196.48 | 208.60 | 38.98 | +| `x == 16` | 179.35 | 206.44 | 220.19 | 41.67 | +| `x < y` | 296.02 | 243.25 | 256.94 | 49.17 | +| `x == y` | 305.44 | 244.58 | 257.90 | 49.69 | + +For `<`, i8 storage is 4.40x faster against a constant and 6.02x faster against another +column. Across the three individual passes those ratios range from 4.33–4.41x and +6.03–7.02x, respectively. i32/i16 storage is about 15%/22% slower against a constant, +but improves column comparisons by about 1.22x/1.15x in the pooled measurements. + +Plain native-width controls use the same child buffers as the wrappers: + +| Storage | Native `< constant` (us) | Narrow `< constant` (us) | Native `< column` (us) | Narrow `< column` (us) | +| --- | ---: | ---: | ---: | ---: | +| i32 | 196.04 | 196.48 | 242.75 | 243.25 | +| i16 | 208.40 | 208.60 | 257.27 | 256.94 | +| i8 | 38.50 | 38.98 | 48.38 | 49.17 | + +The wrapper's difference from a native array at the same width is small at this row count. +The 8-bit kernel optimization in the base PR is responsible for the large comparison +advantage; NarrowArray makes that kernel available while preserving logical i64. + +## Selection and pipelines at 1,048,576 rows + +Times in microseconds. Filter selects approximately 50% of rows. Take gathers N/8 +indices in a deterministic pseudorandom order. Inputs, masks, and indices are prebuilt. +Every operation is executed to its final primitive or boolean result before stopping the timer. + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| Filter → i64 buffer | 304.85 | 227.27 | 156.02 | 342.69 | +| Take → i64 buffer | 215.77 | 331.96 | 308.33 | 321.40 | +| Compare → filter → i64 buffer | 458.10 | 402.62 | 342.27 | 382.17 | +| Filter → compare → bool bitmap | 379.71 | 211.58 | 156.58 | 264.73 | +| Take → compare → bool bitmap | 167.62 | 165.17 | 153.58 | 120.75 | + +- Compare/filter with i8 storage is about 1.20x faster even with a final i64 result. +- i16 is the strongest filtering representation in this fixture: filter → i64 is 1.95x + faster and filter → compare is 2.42x faster. i8 filter → i64 is about 12% slower. +- Take → compare benefits from i8 storage (1.39x), but take → i64 is about 49% slower. + The current canonical fallback casts the narrow child to i64; the existing dictionary + cast rule pushes that cast into all dictionary values before gathering. Avoiding that + early widening is a follow-up optimization, not a benefit implemented by this draft. + +## Encoding and canonical materialization + +Times in microseconds for 1,048,576 rows: + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| Encode, fresh bounds scan | 212.96 | 384.04 | 355.71 | 334.15 | +| Materialize canonical i64 | 0.08 | 199.58 | 177.88 | 188.90 | + +Encoding inputs require the indicated storage width: the common 0..31 pattern is shifted +by 0 for i8, 128 for i16, 32,768 for i32, and 2,147,483,648 for i64. The i64 column is +therefore the cost of discovering that narrowing is impossible, not a conversion baseline. +Each sample uses fresh array statistics so the bounds scan cannot disappear into a cache. +Input buffer generation and creation of the fresh array handle are outside the timer. + +The i8 encoding cost is about 334 us. The pooled constant-comparison saving is about +133 us per pass: roughly three comparisons amortize conversion, or three to four using +individual-pass medians. Encoding solely for one comparison loses overall. Constructing +NarrowArray around an existing narrow child avoids conversion and the bounds scan. + +Canonical materialization is an additional cost: about 189 us to widen i8 to i64 here. +The primitive-i64 materialization case is a no-op handle operation, not a buffer copy. +Arithmetic, aggregates, and mixed-storage-width comparisons have no specialized Narrow +kernel in this draft and may pay for widening. + +## Row-count sweep + +All times below are microseconds. These tables use `<`; equality and all native-width +controls are in the machine-readable results. + +### 8,192 rows + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| `x < 16` | 2.21 | 2.54 | 2.62 | 1.29 | +| `x < y` | 2.71 | 2.42 | 2.42 | 1.08 | + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| FilterToI64 | 4.12 | 3.50 | 2.88 | 4.54 | +| TakeToI64 | 1.21 | 4.04 | 3.75 | 4.17 | +| CompareFilterToI64 | 7.46 | 5.83 | 5.29 | 5.88 | +| FilterCompare | 6.62 | 3.88 | 3.42 | 4.33 | +| TakeCompare | 2.25 | 3.29 | 3.21 | 3.12 | +| Encode | 2.88 | 4.40 | 3.96 | 3.79 | +| MaterializeI64 | 0.08 | 1.62 | 1.54 | 2.08 | + +### 16,777,216 rows + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| `x < 16` | 3568.77 | 3942.46 | 4217.94 | 738.85 | +| `x < y` | 3905.52 | 2655.87 | 2815.94 | 544.52 | + +| Operation | Primitive i64 | Narrow/i32 | Narrow/i16 | Narrow/i8 | +| --- | ---: | ---: | ---: | ---: | +| FilterToI64 | 4898.38 | 3663.04 | 2505.21 | 5478.21 | +| TakeToI64 | 9284.69 | 12349.31 | 11896.35 | 12101.79 | +| CompareFilterToI64 | 7328.42 | 6389.04 | 5343.48 | 6027.17 | +| FilterCompare | 6034.90 | 3367.04 | 2483.33 | 4225.44 | +| TakeCompare | 9435.10 | 8211.56 | 6757.50 | 3897.21 | +| Encode | 3439.96 | 6205.85 | 5742.08 | 5420.33 | +| MaterializeI64 | 0.25 | 3204.27 | 2862.83 | 3036.96 | + +Small-array overhead matters: at 8K rows the i8 comparison gains are 1.71x/2.50x, +and take → compare is slower for every narrow width. Large-buffer selection and allocation +timings vary more between passes; consult the individual pass medians and quartiles rather +than treating the pooled values as precise latency guarantees. + +## Method + +- Apple M5 Max, aarch64, macOS 26.6.1 (25G76), Rust 1.98.0. +- Standard repository bench profile: optimized, 16 codegen units, LTO off. Repository + `force-frame-pointers=yes`; no additional `RUSTFLAGS` or target-CPU override. +- Three sequential passes, 140 samples per case per pass. Representations rotate both their + starting position and traversal direction on every round. No concurrent builds or other + benchmarks were launched during measurement. +- The tables are pooled medians over 420 samples. Absolute times varied across passes; + e.g. the 1M-row i64 constant comparison medians were 171, 223, and 141 us, while the + corresponding narrow-i8 medians were 39, 52, and 32 us. Relative comparison benefits + persisted. This was not a pinned-core or controlled-frequency experiment. +- All compute inputs are ordinary uncompressed PrimitiveArray buffers with non-null values + in 0..31. Native-width controls expose that width as their logical type; every Narrow + representation keeps logical i64. Pairs have matching storage widths on both sides. +- Timings include expression construction, execution, and result allocation. Input creation, + conversion to the chosen input representation, execution-context creation, and destruction + of the returned result are outside timing. Inputs are reused; no forced cold-cache flush. +- Comparison/operation results are checked against the i64 reference before timing. Encoding + checks both value equality and output buffer size. Mask and index generation are excluded; + compare → filter includes construction of the predicate mask. +- The public execution path may reorder or push operations through children. These pipeline + measurements do not require a particular physical execution order. + +Sources: [comparison matrix](vortex-array/benches/narrow_compare.rs), +[selection/encoding matrix](vortex-array/benches/narrow.rs). +[Machine-readable results](narrow-array-benchmark-results.json) contain every case, quartiles, +and individual-pass medians. Raw per-sample CSVs are in +`/private/tmp/narrow-{compare,operations}-run{1,2,3}.csv` on the measurement host. + +Build command: + +```sh +CARGO_TARGET_DIR=/Users/matt/Desktop/repo/vortex/target \ + cargo bench -p vortex-array --bench narrow --bench narrow_compare --no-run +``` + +The build prints the executable paths. The measured binaries were +`target/release/deps/narrow_compare-081029d9296a0cf7` and +`target/release/deps/narrow-729aa8d2feff4afe` under that target directory. Each was run with +`VORTEX_NARROW_INTERLEAVED=1` three times, sequentially, redirecting stdout to its CSV; +`narrow` also emits measured buffer bytes on stderr. Without that environment variable, +the same targets expose ordinary Divan benchmarks via `cargo bench`. + +## Validation + +- `cargo nextest run -p vortex-array -p vortex-fastlanes`: 4,121 passed, one existing skip. +- `cargo test --doc -p vortex-array`: 76 passed, 21 ignored. +- `cargo clippy -p vortex-array -p vortex-fastlanes --all-targets --all-features -- -D warnings`: passed. +- `cargo clippy --all-targets --all-features`: passed across the workspace. +- `cargo +nightly-2026-09-10 fmt --all --check`: passed. +- Three passes of both benchmark matrices: all result/size assertions passed. + +The workspace build warned that clang-format was unavailable for a generated DuckDB header; +no C++ or CUDA sources were changed. + +x86 performance and end-to-end query workloads have not been measured for this draft. diff --git a/narrow-array-design.md b/narrow-array-design.md new file mode 100644 index 00000000000..b7092e09382 --- /dev/null +++ b/narrow-array-design.md @@ -0,0 +1,73 @@ + + + +# NarrowArray prototype + +`NarrowArray` is an ordinary array encoding that keeps a wide logical integer dtype while +storing its values in a narrower child. This draft is stacked on comparison optimization +PR #9948, which specializes bitmap construction for primitive i8/u8 comparisons. + +```text +NarrowArray(dtype = i64) +└── PrimitiveArray(dtype = i8) +``` + +The child can use any encoding with the required integer dtype, including bitpacking or a +dictionary. The wrapper stores no value buffers. Signedness and nullability must match; +the child must be strictly narrower. Nested wrappers are flattened. + +## Construction + +- `NarrowArray::try_new(child, dtype)` wraps an existing narrow child without scanning it. +- `NarrowArray::encode(primitive, ctx)` scans non-null bounds, selects the smallest fitting + integer type of the same signedness, and converts the buffer once. It returns the original + array if no smaller type fits. Existing exact bounds can avoid the scan. + +This preserves a useful distinction from a general cast: every child value is representable +in the logical dtype, and widening preserves ordering and equality. The invariant allows +comparisons to use the child's dtype without inspecting individual values. + +## Execution + +| Operation | Behavior | +| --- | --- | +| Compare to a fitting constant | Cast the constant once and compare the narrow child. | +| Compare to an out-of-range constant | Produce the determined boolean values, preserving nulls. | +| Compare two NarrowArrays with the same child dtype | Compare their children directly. | +| Slice, filter, take, mask | Push into the child and retain the logical dtype. | +| Cast to the child's exact dtype | Unwrap the child. | +| Cast to another safely wider dtype | Rewrap the same child. | +| Scalar access | Return a scalar at the logical dtype. | +| Canonical execution | Execute a widening cast, allowing the child's cast kernel to participate. | +| Other operations, including mixed child widths | Use the existing canonical fallback. | + +Selection may retain a lazy child. Calling `execute::` on the result still +requests a wide canonical buffer. The selection/comparison pipeline benchmarks execute through +the public array operations; they do not count construction of an unevaluated child as work done. + +## Serialization + +The prototype uses encoding ID `vortex.narrow` and one metadata byte for the child's PType. +Deserialization reconstructs the child at its stored width. Widening happens when a consumer +requests canonical execution, not while reading the NarrowArray. + +The encoding is registered in the array session. No released file edition has been modified +to admit it, and the compressor does not automatically produce it. + +## Scope and open work + +This draft supports signed and unsigned primitive integers. It does not change +`PrimitiveArray::narrow`, decimal storage, or DecimalByteParts. DecimalArray already permits +coefficient storage widths independent of its logical decimal dtype; decimal kernels and +DecimalByteParts integration need a separate design and measurements. + +There are no specialized arithmetic or aggregate kernels yet. Arithmetic must retain logical +overflow behavior, so operations such as `i64(120) + i64(120)` cannot simply execute in i8. +General cast expressions are not rewritten into NarrowArray, avoiding a loop with canonical +widening. Mixed-width comparison dispatch and automatic compressor selection remain open. +Canonical execution after take also needs attention: the existing dictionary cast rule +widens the full values child before gathering, which regresses the take-to-i64 benchmark. + +The measured benefits and costs are recorded in [the benchmark report](narrow-array-benchmarks.md). +The report distinguishes buffer savings from whole-process memory usage and prebuilt-array +compute from encoding and materialization costs. diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index cc5d869aaa1..c0afce4858b 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -315,3 +315,11 @@ harness = false [[bench]] name = "slice_dict_primitive" harness = false + +[[bench]] +name = "narrow_compare" +harness = false + +[[bench]] +name = "narrow" +harness = false diff --git a/vortex-array/benches/narrow.rs b/vortex-array/benches/narrow.rs new file mode 100644 index 00000000000..ad90f019b32 --- /dev/null +++ b/vortex-array/benches/narrow.rs @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Selection, materialization, and encoding costs for logical i64 arrays with narrow storage. + +#![expect( + clippy::unwrap_used, + reason = "benchmark fixtures satisfy encoding invariants" +)] + +use std::hint::black_box; +use std::time::Instant; + +use divan::Bencher; +use divan::counter::ItemsCount; +use vortex_array::ArrayRef; +use vortex_array::ExecutionCtx; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::array_session; +use vortex_array::arrays::BoolArray; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::NarrowArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::arrays::bool::BoolArrayExt; +use vortex_array::assert_arrays_eq; +use vortex_array::builtins::ArrayBuiltins; +use vortex_array::dtype::NativePType; +use vortex_array::dtype::PType; +use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_mask::Mask; + +const WIDTHS: &[PType] = &[PType::I64, PType::I32, PType::I16, PType::I8]; +const OPERATIONS: &[Operation] = &[ + Operation::FilterToI64, + Operation::TakeToI64, + Operation::CompareFilterToI64, + Operation::FilterCompare, + Operation::TakeCompare, + Operation::MaterializeI64, + Operation::Encode, +]; + +#[derive(Clone, Copy, Debug)] +enum Operation { + FilterToI64, + TakeToI64, + CompareFilterToI64, + FilterCompare, + TakeCompare, + MaterializeI64, + Encode, +} + +struct Input { + values: ArrayRef, + mask: Mask, + indices: ArrayRef, + constant: ArrayRef, + encode_values: Buffer, +} + +fn value(i: usize) -> i64 { + (i.wrapping_mul(2_654_435_761).rotate_right(13) & 31) as i64 +} + +fn input(len: usize, storage: PType, ctx: &mut ExecutionCtx) -> Input { + let wide = PrimitiveArray::from_iter((0..len).map(value)).into_array(); + let values = if storage == PType::I64 { + wide + } else { + let child = wide + .cast(storage.into()) + .unwrap() + .execute::(ctx) + .unwrap(); + NarrowArray::try_new(child.into_array(), PType::I64.into()) + .unwrap() + .into_array() + }; + // Force encode to choose the requested width rather than always choosing i8. + let offset = match storage { + PType::I8 => 0, + PType::I16 => 128, + PType::I32 => 32_768, + PType::I64 => 2_147_483_648, + _ => unreachable!(), + }; + Input { + values, + mask: Mask::from_iter((0..len).map(|i| value(i) < 16)), + indices: PrimitiveArray::from_iter( + (0..len / 8).map(|i| { + u32::try_from(i.wrapping_mul(2_654_435_761).rotate_right(13) % len).unwrap() + }), + ) + .into_array(), + constant: ConstantArray::new(16i64, len).into_array(), + encode_values: Buffer::from_iter((0..len).map(|i| offset + value(i))), + } +} + +fn run( + op: Operation, + input: &Input, + encode_source: PrimitiveArray, + ctx: &mut ExecutionCtx, +) -> ArrayRef { + match op { + Operation::Encode => NarrowArray::encode(encode_source, ctx).unwrap(), + Operation::MaterializeI64 => input + .values + .clone() + .execute::(ctx) + .unwrap() + .into_array(), + Operation::FilterToI64 => input + .values + .filter(input.mask.clone()) + .unwrap() + .execute::(ctx) + .unwrap() + .into_array(), + Operation::TakeToI64 => input + .values + .take(input.indices.clone()) + .unwrap() + .execute::(ctx) + .unwrap() + .into_array(), + Operation::CompareFilterToI64 => { + let selected = input + .values + .binary(input.constant.clone(), Operator::Lt) + .unwrap() + .execute::(ctx) + .unwrap() + .to_mask_fill_null_false(ctx); + input + .values + .filter(selected) + .unwrap() + .execute::(ctx) + .unwrap() + .into_array() + } + Operation::FilterCompare | Operation::TakeCompare => { + let selected = if matches!(op, Operation::FilterCompare) { + input.values.filter(input.mask.clone()).unwrap() + } else { + input.values.take(input.indices.clone()).unwrap() + }; + let constant = ConstantArray::new(16i64, selected.len()).into_array(); + selected + .binary(constant, Operator::Lt) + .unwrap() + .execute::(ctx) + .unwrap() + .into_array() + } + } +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn compare_filter(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::CompareFilterToI64); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn filter(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::FilterToI64); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn take(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::TakeToI64); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn filter_compare(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::FilterCompare); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn take_compare(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::TakeCompare); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn materialize(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::MaterializeI64); +} + +#[divan::bench(args = [8_192, 1_048_576], types = [i64, i32, i16, i8])] +fn encode(bencher: Bencher, len: usize) { + bench(bencher, len, T::PTYPE, Operation::Encode); +} + +fn bench(bencher: Bencher, len: usize, storage: PType, op: Operation) { + let session = array_session(); + let input = input(len, storage, &mut session.create_execution_ctx()); + bencher + .counter(ItemsCount::new(len)) + .with_inputs(|| { + // Fresh array statistics include the bounds scan in every encoding sample. + ( + PrimitiveArray::new(input.encode_values.clone(), Validity::NonNullable), + session.create_execution_ctx(), + ) + }) + .bench_values(|(source, mut ctx)| run(op, &input, source, &mut ctx)); +} + +#[expect(clippy::use_debug, reason = "enum variants name the benchmark cases")] +fn interleaved() { + const SAMPLES: usize = 140; + let session = array_session(); + println!("rows,operation,representation,sample,nanoseconds"); + for len in [8_192, 1_048_576, 16_777_216] { + let inputs = WIDTHS + .iter() + .map(|&width| input(len, width, &mut session.create_execution_ctx())) + .collect::>(); + let names = ["primitive_i64", "narrow_i32", "narrow_i16", "narrow_i8"]; + for (name, input) in names.iter().zip(&inputs) { + eprintln!("size,{len},{name},{}", input.values.nbytes()); + } + for &op in OPERATIONS { + let mut expected: Option = None; + for (input, &width) in inputs.iter().zip(WIDTHS) { + let source = + PrimitiveArray::new(input.encode_values.clone(), Validity::NonNullable); + let result = run( + op, + input, + source.clone(), + &mut session.create_execution_ctx(), + ); + if matches!(op, Operation::Encode) { + assert_eq!(result.nbytes(), (len * width.byte_width()) as u64); + assert_arrays_eq!(result, source, &mut session.create_execution_ctx()); + } else if let Some(expected) = &expected { + assert_arrays_eq!(result, expected, &mut session.create_execution_ctx()); + } else { + expected = Some(result); + } + } + let mut samples = vec![Vec::with_capacity(SAMPLES); inputs.len()]; + for sample in 0..SAMPLES { + let reverse = !(sample / inputs.len()).is_multiple_of(2); + for step in 0..inputs.len() { + let offset = if reverse { inputs.len() - step } else { step }; + let index = (sample + offset) % inputs.len(); + let input = &inputs[index]; + let source = + PrimitiveArray::new(input.encode_values.clone(), Validity::NonNullable); + let mut ctx = session.create_execution_ctx(); + let start = Instant::now(); + let result = run(op, input, source, &mut ctx); + let elapsed = start.elapsed(); + black_box(result); + samples[index].push(elapsed); + } + } + for (name, samples) in names.iter().zip(samples) { + for (sample, duration) in samples.into_iter().enumerate() { + println!("{len},{op:?},{name},{sample},{}", duration.as_nanos()); + } + } + } + } +} + +fn main() { + if std::env::var_os("VORTEX_NARROW_INTERLEAVED").is_some() { + interleaved(); + } else { + divan::main(); + } +} diff --git a/vortex-array/benches/narrow_compare.rs b/vortex-array/benches/narrow_compare.rs new file mode 100644 index 00000000000..6939fb6cc37 --- /dev/null +++ b/vortex-array/benches/narrow_compare.rs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Compare ordinary primitive buffers across native and NarrowArray storage widths. + +#![expect( + clippy::unwrap_used, + reason = "benchmark inputs satisfy the encoding invariants" +)] + +use std::hint::black_box; +use std::time::Duration; +use std::time::Instant; + +use divan::Bencher; +use divan::counter::ItemsCount; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::array_session; +use vortex_array::arrays::BoolArray; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::NarrowArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::builtins::ArrayBuiltins; +use vortex_array::dtype::NativePType; +use vortex_array::dtype::PType; +use vortex_array::scalar::Scalar; +use vortex_array::scalar_fn::fns::operators::Operator; + +const CASES: &[(usize, Operator)] = &[ + (8_192, Operator::Lt), + (1_048_576, Operator::Lt), + (16_777_216, Operator::Lt), + (8_192, Operator::Eq), + (1_048_576, Operator::Eq), + (16_777_216, Operator::Eq), +]; + +fn main() { + if std::env::var_os("VORTEX_NARROW_INTERLEAVED").is_some() { + interleaved(); + } else { + divan::main(); + } +} + +fn input>(len: usize, offset: usize) -> ArrayRef { + PrimitiveArray::from_iter((0..len).map(|i| { + let value = i + .wrapping_add(offset) + .wrapping_mul(2_654_435_761) + .rotate_right(13) + & 31; + >::from(i8::try_from(value).unwrap()) + })) + .into_array() +} + +fn narrow(values: ArrayRef) -> ArrayRef { + NarrowArray::try_new(values, PType::I64.into()) + .unwrap() + .into_array() +} + +fn bench_compare(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef, operator: Operator) { + let session = array_session(); + bencher + .counter(ItemsCount::new(lhs.len())) + .with_inputs(|| session.create_execution_ctx()) + .bench_refs(|ctx| { + lhs.binary(rhs.clone(), operator) + .unwrap() + .execute::(ctx) + .unwrap() + }); +} + +fn bench_constant(bencher: Bencher, lhs: ArrayRef, operator: Operator) { + let rhs = + ConstantArray::new(Scalar::from(16i64).cast(lhs.dtype()).unwrap(), lhs.len()).into_array(); + bench_compare(bencher, lhs, rhs, operator); +} + +#[divan::bench(types = [i64, i32, i16, i8], args = CASES)] +fn constant_primitive>( + bencher: Bencher, + (len, operator): (usize, Operator), +) { + bench_constant(bencher, input::(len, 0), operator); +} + +#[divan::bench(types = [i32, i16, i8], args = CASES)] +fn constant_narrow>( + bencher: Bencher, + (len, operator): (usize, Operator), +) { + bench_constant(bencher, narrow(input::(len, 0)), operator); +} + +#[divan::bench(types = [i64, i32, i16, i8], args = CASES)] +fn pair_primitive>(bencher: Bencher, (len, operator): (usize, Operator)) { + bench_compare(bencher, input::(len, 0), input::(len, 17), operator); +} + +#[divan::bench(types = [i32, i16, i8], args = CASES)] +fn pair_narrow>(bencher: Bencher, (len, operator): (usize, Operator)) { + bench_compare( + bencher, + narrow(input::(len, 0)), + narrow(input::(len, 17)), + operator, + ); +} + +fn interleaved() { + const SAMPLES: usize = 140; + println!("rows,shape,operator,representation,sample,nanoseconds"); + let session = array_session(); + for len in [8_192, 1_048_576, 16_777_216] { + let inputs = [ + ("i64", input::(len, 0), input::(len, 17)), + ("i32", input::(len, 0), input::(len, 17)), + ("i16", input::(len, 0), input::(len, 17)), + ("i8", input::(len, 0), input::(len, 17)), + ]; + for shape in ["constant", "pair"] { + let mut cases = Vec::new(); + for (width, lhs, rhs) in &inputs { + let rhs = if shape == "constant" { + ConstantArray::new(Scalar::from(16i64).cast(lhs.dtype()).unwrap(), len) + .into_array() + } else { + rhs.clone() + }; + cases.push((format!("primitive_{width}"), lhs.clone(), rhs.clone())); + if *width != "i64" { + let rhs = if shape == "constant" { + ConstantArray::new(16i64, len).into_array() + } else { + narrow(rhs) + }; + cases.push((format!("narrow_{width}"), narrow(lhs.clone()), rhs)); + } + } + for operator in [Operator::Lt, Operator::Eq] { + let mut expected = None; + for (_, lhs, rhs) in &cases { + let bits = lhs + .binary(rhs.clone(), operator) + .unwrap() + .execute::(&mut session.create_execution_ctx()) + .unwrap() + .into_bit_buffer(); + if let Some(expected) = &expected { + assert_eq!(&bits, expected); + } else { + expected = Some(bits); + } + } + let mut samples = (0..cases.len()) + .map(|_| Vec::::with_capacity(SAMPLES)) + .collect::>(); + for sample in 0..SAMPLES { + // Rotate both the first case and direction to distribute timing/cache drift. + let start = sample % cases.len(); + let reverse = !(sample / cases.len()).is_multiple_of(2); + for step in 0..cases.len() { + let offset = if reverse { cases.len() - step } else { step }; + let index = (start + offset) % cases.len(); + let (_, lhs, rhs) = &cases[index]; + let mut ctx = session.create_execution_ctx(); + let started = Instant::now(); + let result = lhs + .binary(rhs.clone(), operator) + .unwrap() + .execute::(&mut ctx) + .unwrap(); + let elapsed = started.elapsed(); + black_box(result); + samples[index].push(elapsed); + } + } + for ((representation, ..), durations) in cases.iter().zip(samples) { + for (sample, duration) in durations.into_iter().enumerate() { + println!( + "{len},{shape},{operator},{representation},{sample},{}", + duration.as_nanos(), + ); + } + } + } + } + } +} diff --git a/vortex-array/src/arrays/mod.rs b/vortex-array/src/arrays/mod.rs index f96eebead65..d366b1065a7 100644 --- a/vortex-array/src/arrays/mod.rs +++ b/vortex-array/src/arrays/mod.rs @@ -86,6 +86,10 @@ pub mod masked; pub use masked::Masked; pub use masked::MaskedArray; +pub mod narrow; +pub use narrow::Narrow; +pub use narrow::NarrowArray; + pub mod null; pub use null::Null; pub use null::NullArray; @@ -146,6 +150,7 @@ pub(crate) fn initialize(session: &VortexSession) { list::initialize(session); listview::initialize(session); map::initialize(session); + narrow::initialize(session); patched::initialize(session); primitive::initialize(session); struct_::initialize(session); diff --git a/vortex-array/src/arrays/narrow/compute.rs b/vortex-array/src/arrays/narrow/compute.rs new file mode 100644 index 00000000000..ff1cf3f86c1 --- /dev/null +++ b/vortex-array/src/arrays/narrow/compute.rs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use std::ops::Range; + +use vortex_buffer::BitBuffer; +use vortex_error::VortexResult; +use vortex_mask::Mask; +use vortex_session::VortexSession; + +use super::Narrow; +use super::NarrowArray; +use super::NarrowArraySlotsExt; +use crate::ArrayRef; +use crate::ArrayView; +use crate::ExecutionCtx; +use crate::IntoArray; +use crate::arrays::BoolArray; +use crate::arrays::Constant; +use crate::arrays::ConstantArray; +use crate::arrays::Primitive; +use crate::arrays::dict::TakeReduce; +use crate::arrays::dict::TakeReduceAdaptor; +use crate::arrays::filter::FilterReduce; +use crate::arrays::filter::FilterReduceAdaptor; +use crate::arrays::slice::SliceReduce; +use crate::arrays::slice::SliceReduceAdaptor; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::optimizer::kernels::ArrayKernelsExt; +use crate::optimizer::rules::ParentRuleSet; +use crate::scalar_fn::ScalarFnVTable; +use crate::scalar_fn::fns::binary::Binary; +use crate::scalar_fn::fns::binary::CompareExecuteAdaptor; +use crate::scalar_fn::fns::binary::CompareKernel; +use crate::scalar_fn::fns::binary::execute_compare; +use crate::scalar_fn::fns::cast::CastReduce; +use crate::scalar_fn::fns::cast::CastReduceAdaptor; +use crate::scalar_fn::fns::mask::MaskReduce; +use crate::scalar_fn::fns::mask::MaskReduceAdaptor; +use crate::scalar_fn::fns::operators::CompareOperator; + +pub(super) const PARENT_RULES: ParentRuleSet = ParentRuleSet::new(&[ + ParentRuleSet::lift(&SliceReduceAdaptor(Narrow)), + ParentRuleSet::lift(&FilterReduceAdaptor(Narrow)), + ParentRuleSet::lift(&TakeReduceAdaptor(Narrow)), + ParentRuleSet::lift(&MaskReduceAdaptor(Narrow)), + ParentRuleSet::lift(&CastReduceAdaptor(Narrow)), +]); + +pub(crate) fn initialize(session: &VortexSession) { + session.kernels().register_execute_parent_kernel( + Binary.id(), + Narrow, + CompareExecuteAdaptor(Narrow), + ); +} + +fn rewrap(array: ArrayView<'_, Narrow>, values: ArrayRef) -> VortexResult> { + let dtype = array.dtype().with_nullability(values.dtype().nullability()); + Ok(Some(NarrowArray::try_new(values, dtype)?.into_array())) +} + +impl SliceReduce for Narrow { + fn slice(array: ArrayView<'_, Self>, range: Range) -> VortexResult> { + rewrap(array, array.values().slice(range)?) + } +} + +impl FilterReduce for Narrow { + fn filter(array: ArrayView<'_, Self>, mask: &Mask) -> VortexResult> { + rewrap(array, array.values().filter(mask.clone())?) + } +} + +impl TakeReduce for Narrow { + fn take(array: ArrayView<'_, Self>, indices: &ArrayRef) -> VortexResult> { + rewrap(array, array.values().take(indices.clone())?) + } +} + +impl MaskReduce for Narrow { + fn mask(array: ArrayView<'_, Self>, mask: &ArrayRef) -> VortexResult> { + rewrap(array, array.values().clone().mask(mask.clone())?) + } +} + +impl CastReduce for Narrow { + fn cast(array: ArrayView<'_, Self>, dtype: &DType) -> VortexResult> { + if dtype == array.values().dtype() { + return Ok(Some(array.values().clone())); + } + if super::validate_dtypes(array.values().dtype(), dtype).is_ok() { + return Ok(Some( + NarrowArray::try_new(array.values().clone(), dtype.clone())?.into_array(), + )); + } + Ok(None) + } +} + +impl CompareKernel for Narrow { + fn compare( + lhs: ArrayView<'_, Self>, + rhs: &ArrayRef, + operator: CompareOperator, + ctx: &mut ExecutionCtx, + ) -> VortexResult> { + if let Some(rhs) = rhs.as_opt::() + && lhs + .values() + .dtype() + .eq_ignore_nullability(rhs.values().dtype()) + { + return compare_values(lhs.values(), rhs.values(), operator, ctx).map(Some); + } + let Some(constant) = rhs.as_constant() else { + return Ok(None); + }; + let nullability = lhs.dtype().nullability() | rhs.dtype().nullability(); + let storage_dtype = lhs.values().dtype().with_nullability(nullability); + if let Ok(value) = constant.cast(&storage_dtype) { + return compare_values( + lhs.values(), + &ConstantArray::new(value, lhs.len()).into_array(), + operator, + ctx, + ) + .map(Some); + } + + // A non-null integer that cannot fit is outside the entire child's type range. + let below = lhs.dtype().is_signed_int() && i64::try_from(&constant)? < 0; + let result = match operator { + CompareOperator::Eq => false, + CompareOperator::NotEq => true, + CompareOperator::Lt | CompareOperator::Lte => !below, + CompareOperator::Gt | CompareOperator::Gte => below, + }; + let validity = lhs + .values() + .validity()? + .cast_nullability(nullability, lhs.len(), ctx)?; + Ok(Some( + BoolArray::try_new( + BitBuffer::full_in(result, lhs.len(), ctx.allocator().clone()), + validity, + )? + .into_array(), + )) + } +} + +fn compare_values( + lhs: &ArrayRef, + rhs: &ArrayRef, + operator: CompareOperator, + ctx: &mut ExecutionCtx, +) -> VortexResult { + // Primitive children need no further encoding dispatch or intermediate expression. + if lhs.is::() && (rhs.is::() || rhs.is::()) { + execute_compare(lhs, rhs, operator, ctx) + } else { + lhs.binary(rhs.clone(), operator.into()) + } +} diff --git a/vortex-array/src/arrays/narrow/mod.rs b/vortex-array/src/arrays/narrow/mod.rs new file mode 100644 index 00000000000..0fe5bdf7d53 --- /dev/null +++ b/vortex-array/src/arrays/narrow/mod.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Integer arrays backed by a narrower integer child of the same signedness. +//! +//! [`NarrowArray`] preserves the logical dtype while selection and comparison kernels operate +//! on its child. Canonical execution widens the values into a [`PrimitiveArray`]. + +mod compute; +mod vtable; + +#[cfg(test)] +mod tests; + +pub(crate) use compute::initialize; +use vortex_error::VortexResult; +use vortex_error::vortex_ensure; + +use crate::Array; +use crate::ArrayParts; +use crate::ArrayRef; +use crate::EmptyArrayData; +use crate::ExecutionCtx; +use crate::IntoArray; +use crate::aggregate_fn::NumericalAggregateOpts; +use crate::aggregate_fn::fns::min_max::min_max; +use crate::array_slots; +use crate::arrays::PrimitiveArray; +use crate::arrays::primitive::PrimitiveArrayExt; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::dtype::PType; + +/// An integer encoding whose child has a smaller width and the same signedness. +#[derive(Clone, Debug)] +pub struct Narrow; + +/// A logically wide integer array backed by narrower integer values. +pub type NarrowArray = Array; + +/// The physical child of a [`NarrowArray`]. +#[array_slots(Narrow)] +pub struct NarrowSlots { + /// Values with the same signedness and nullability as the logical dtype. + #[slot(0)] + pub values: ArrayRef, +} + +impl NarrowArray { + /// Wrap narrower integer values with a wider logical dtype, without reading their buffers. + /// + /// The dtypes must have the same signedness and nullability, and `dtype` must be strictly + /// wider. The child may use any encoding. Nested narrow wrappers are flattened. + pub fn try_new(mut values: ArrayRef, dtype: DType) -> VortexResult { + validate_dtypes(values.dtype(), &dtype)?; + while let Some(inner) = values.as_opt::() { + values = inner.values().clone(); + } + let len = values.len(); + Self::try_from_parts( + ArrayParts::new(Narrow, dtype, len, EmptyArrayData) + .with_slots(NarrowSlots { values }.into_slots()), + ) + } + + /// Narrow an integer buffer while preserving its logical dtype and signedness. + /// + /// Selects storage using the non-null minimum and maximum, then casts the values once. + /// Returns the original array when no smaller type in the same signedness family fits. + pub fn encode(array: PrimitiveArray, ctx: &mut ExecutionCtx) -> VortexResult { + let ptype = array.ptype(); + vortex_ensure!(ptype.is_int(), "NarrowArray requires integer values"); + if ptype.byte_width() == 1 { + return Ok(array.into_array()); + } + let bounds = min_max(array.as_ref(), ctx, NumericalAggregateOpts::default())?; + let candidates = if ptype.is_signed_int() { + [PType::I8, PType::I16, PType::I32] + } else { + [PType::U8, PType::U16, PType::U32] + }; + for candidate in candidates { + if candidate.byte_width() >= ptype.byte_width() { + break; + } + let storage_dtype = DType::Primitive(candidate, array.dtype().nullability()); + if bounds.as_ref().is_some_and(|bounds| { + bounds.min.cast(&storage_dtype).is_err() || bounds.max.cast(&storage_dtype).is_err() + }) { + continue; + } + let values = array + .as_ref() + .cast(storage_dtype)? + .execute::(ctx)?; + return Ok(Self::try_new(values.into_array(), array.dtype().clone())?.into_array()); + } + Ok(array.into_array()) + } +} + +pub(super) fn validate_dtypes(storage: &DType, logical: &DType) -> VortexResult<()> { + let storage_ptype = PType::try_from(storage)?; + let logical_ptype = PType::try_from(logical)?; + vortex_ensure!( + storage_ptype.is_int() && logical_ptype.is_int(), + "NarrowArray requires integer dtypes, got {storage} and {logical}" + ); + vortex_ensure!( + storage_ptype.is_signed_int() == logical_ptype.is_signed_int(), + "NarrowArray must preserve signedness, got {storage} and {logical}" + ); + vortex_ensure!( + storage_ptype.byte_width() < logical_ptype.byte_width(), + "NarrowArray storage {storage} must be strictly narrower than {logical}" + ); + vortex_ensure!( + storage.nullability() == logical.nullability(), + "NarrowArray must preserve nullability, got {storage} and {logical}" + ); + Ok(()) +} diff --git a/vortex-array/src/arrays/narrow/tests.rs b/vortex-array/src/arrays/narrow/tests.rs new file mode 100644 index 00000000000..a98da403905 --- /dev/null +++ b/vortex-array/src/arrays/narrow/tests.rs @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use rstest::rstest; +use vortex_buffer::ByteBufferMut; +use vortex_buffer::buffer; +use vortex_error::VortexResult; +use vortex_mask::Mask; +use vortex_session::registry::ReadContext; + +use super::Narrow; +use super::NarrowArray; +use super::NarrowArraySlotsExt; +use crate::ArrayContext; +use crate::IntoArray; +use crate::VTable; +use crate::VortexSessionExecute; +use crate::array_session; +use crate::arrays::BoolArray; +use crate::arrays::ConstantArray; +use crate::arrays::DictArray; +use crate::arrays::Primitive; +use crate::arrays::PrimitiveArray; +use crate::assert_arrays_eq; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::dtype::Nullability; +use crate::dtype::PType; +use crate::scalar::Scalar; +use crate::scalar_fn::fns::operators::Operator; +use crate::serde::SerializeOptions; +use crate::serde::SerializedArray; +use crate::validity::Validity; + +#[rstest] +fn test_integer_widths( + #[values(PType::I8, PType::I16, PType::I32, PType::U8, PType::U16, PType::U32)] storage: PType, + #[values(PType::I16, PType::I32, PType::I64, PType::U16, PType::U32, PType::U64)] + logical: PType, +) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let values = buffer![0i64, 1, 42] + .into_array() + .cast(storage.into())? + .execute::(&mut ctx)? + .into_array(); + let result = NarrowArray::try_new(values.clone(), logical.into()); + if storage.is_signed_int() != logical.is_signed_int() + || storage.byte_width() >= logical.byte_width() + { + assert!(result.is_err()); + return Ok(()); + } + let array = result?; + assert_eq!(array.dtype(), &DType::from(logical)); + assert_eq!(array.values().dtype(), &DType::from(storage)); + assert_eq!(array.as_ref().nbytes(), 3 * storage.byte_width() as u64); + let expected = values.cast(logical.into())?; + assert_arrays_eq!(array, expected, &mut ctx); + Ok(()) +} + +#[test] +fn test_reject_nonintegers_and_nullability_changes() { + assert!(NarrowArray::try_new(buffer![1f32].into_array(), PType::F64.into()).is_err()); + assert!(NarrowArray::try_new(buffer![1i8].into_array(), PType::F64.into()).is_err()); + assert!( + NarrowArray::try_new( + buffer![1i8].into_array(), + DType::Primitive(PType::I64, Nullability::Nullable), + ) + .is_err() + ); + assert!( + NarrowArray::try_new( + PrimitiveArray::from_option_iter([Some(1i8), None]).into_array(), + PType::I64.into(), + ) + .is_err() + ); +} + +#[test] +fn test_flatten_nested_wrappers() -> VortexResult<()> { + let inner = NarrowArray::try_new(buffer![1i8, 2, 3].into_array(), PType::I16.into())?; + let outer = NarrowArray::try_new(inner.into_array(), PType::I64.into())?; + assert!(outer.values().is::()); + assert_eq!(outer.values().dtype(), &DType::from(PType::I8)); + Ok(()) +} + +#[rstest] +#[case(vec![-128i64, 127], PType::I8)] +#[case(vec![0, 255], PType::I16)] +#[case(vec![-32769, 32768], PType::I32)] +#[case(vec![i64::MIN, i64::MAX], PType::I64)] +fn test_encode_signed(#[case] values: Vec, #[case] storage: PType) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let original = PrimitiveArray::from_iter(values); + let encoded = NarrowArray::encode(original.clone(), &mut ctx)?; + assert_eq!(encoded.dtype(), original.dtype()); + if storage == PType::I64 { + assert!(encoded.is::()); + } else { + assert_eq!( + encoded.as_::().values().dtype(), + &DType::from(storage) + ); + } + assert_arrays_eq!(encoded, original, &mut ctx); + Ok(()) +} + +#[rstest] +#[case(vec![0u64, 255], PType::U8)] +#[case(vec![0, 256], PType::U16)] +#[case(vec![0, 65536], PType::U32)] +#[case(vec![0, u64::MAX], PType::U64)] +fn test_encode_unsigned(#[case] values: Vec, #[case] storage: PType) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let original = PrimitiveArray::from_iter(values); + let encoded = NarrowArray::encode(original.clone(), &mut ctx)?; + assert_eq!(encoded.dtype(), original.dtype()); + if storage == PType::U64 { + assert!(encoded.is::()); + } else { + assert_eq!( + encoded.as_::().values().dtype(), + &DType::from(storage) + ); + } + assert_arrays_eq!(encoded, original, &mut ctx); + Ok(()) +} + +#[test] +fn test_encode_ignores_null_slot_values() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let original = PrimitiveArray::new( + buffer![i64::MIN, 42, i64::MAX], + Validity::from_iter([false, true, false]), + ); + let encoded = NarrowArray::encode(original.clone(), &mut ctx)?; + assert_eq!( + encoded.as_::().values().dtype(), + &DType::Primitive(PType::I8, Nullability::Nullable) + ); + assert_eq!( + encoded.execute_scalar(0, &mut ctx)?, + Scalar::null(original.dtype().clone()) + ); + assert_eq!( + encoded.execute_scalar(1, &mut ctx)?, + Scalar::primitive(42i64, Nullability::Nullable) + ); + assert_arrays_eq!(encoded, original, &mut ctx); + Ok(()) +} + +#[rstest] +#[case(Vec::new())] +#[case(vec![None, None])] +fn test_empty_and_all_null(#[case] values: Vec>) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let original = PrimitiveArray::from_option_iter(values); + let encoded = NarrowArray::encode(original.clone(), &mut ctx)?; + assert_eq!( + encoded.as_::().values().dtype(), + &DType::Primitive(PType::I8, Nullability::Nullable) + ); + assert_arrays_eq!(encoded, original, &mut ctx); + Ok(()) +} + +#[test] +fn test_selection_preserves_narrow_storage() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let array = NarrowArray::try_new(buffer![10i8, 20, 30, 40].into_array(), PType::I64.into())? + .into_array(); + let sliced = array.slice(1..3)?; + let filtered = array.filter(Mask::from_iter([true, false, true, false]))?; + let taken = array.take(buffer![3u32, 1].into_array())?; + for result in [&sliced, &filtered, &taken] { + assert_eq!(result.dtype(), &DType::from(PType::I64)); + assert_eq!( + result.as_::().values().dtype(), + &DType::from(PType::I8) + ); + } + assert_arrays_eq!(sliced, buffer![20i64, 30].into_array(), &mut ctx); + assert_arrays_eq!(filtered, buffer![10i64, 30].into_array(), &mut ctx); + assert_arrays_eq!(taken, buffer![40i64, 20].into_array(), &mut ctx); + + let indices = PrimitiveArray::from_option_iter([Some(3u32), None, Some(0)]).into_array(); + let taken = array.take(indices)?; + assert_eq!( + taken.as_::().values().dtype(), + &DType::Primitive(PType::I8, Nullability::Nullable) + ); + assert_arrays_eq!( + taken, + PrimitiveArray::from_option_iter([Some(40i64), None, Some(10)]), + &mut ctx + ); + + let masked = array.mask(BoolArray::from_iter([true, false, true, false]).into_array())?; + assert_eq!( + masked.as_::().values().dtype(), + &DType::Primitive(PType::I8, Nullability::Nullable) + ); + assert_arrays_eq!( + masked, + PrimitiveArray::from_option_iter([Some(10i64), None, Some(30), None]), + &mut ctx + ); + Ok(()) +} + +#[rstest] +fn test_compare_signed_constants( + #[values(-1000i64, -129, -128, 0, 127, 128, 1000)] constant: i64, + #[values( + Operator::Eq, + Operator::NotEq, + Operator::Lt, + Operator::Lte, + Operator::Gt, + Operator::Gte + )] + op: Operator, + #[values(false, true)] swapped: bool, +) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let values = + PrimitiveArray::from_option_iter([Some(-128i8), Some(0), None, Some(127)]).into_array(); + let dtype = DType::Primitive(PType::I64, Nullability::Nullable); + let narrow = NarrowArray::try_new(values, dtype)?.into_array(); + let wide = + PrimitiveArray::from_option_iter([Some(-128i64), Some(0), None, Some(127)]).into_array(); + let rhs = + ConstantArray::new(Scalar::primitive(constant, Nullability::Nullable), 4).into_array(); + let (actual, expected) = if swapped { + (rhs.binary(narrow, op)?, rhs.binary(wide, op)?) + } else { + (narrow.binary(rhs.clone(), op)?, wide.binary(rhs, op)?) + }; + assert_arrays_eq!(actual, expected, &mut ctx); + Ok(()) +} + +#[rstest] +fn test_compare_unsigned_constants( + #[values(0u64, 255, 256, u64::MAX)] constant: u64, + #[values( + Operator::Eq, + Operator::NotEq, + Operator::Lt, + Operator::Lte, + Operator::Gt, + Operator::Gte + )] + op: Operator, +) -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let values = + PrimitiveArray::from_option_iter([Some(0u8), Some(127), None, Some(255)]).into_array(); + let dtype = DType::Primitive(PType::U64, Nullability::Nullable); + let narrow = NarrowArray::try_new(values, dtype)?.into_array(); + let wide = + PrimitiveArray::from_option_iter([Some(0u64), Some(127), None, Some(255)]).into_array(); + let rhs = ConstantArray::new(constant, 4).into_array(); + assert_arrays_eq!( + narrow.binary(rhs.clone(), op)?, + wide.binary(rhs, op)?, + &mut ctx + ); + Ok(()) +} + +#[test] +fn test_null_constant_and_arithmetic_fallback() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let narrow = + NarrowArray::try_new(buffer![120i8, 100].into_array(), PType::I64.into())?.into_array(); + let null = ConstantArray::new( + Scalar::null(DType::Primitive(PType::I64, Nullability::Nullable)), + 2, + ) + .into_array(); + assert_arrays_eq!( + narrow.binary(null, Operator::Eq)?, + ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 2), + &mut ctx + ); + let sum = narrow.binary(ConstantArray::new(120i64, 2).into_array(), Operator::Add)?; + assert_arrays_eq!(sum, buffer![240i64, 220].into_array(), &mut ctx); + let unwrapped = narrow.cast(PType::I8.into())?; + assert!(unwrapped.is::()); + assert_arrays_eq!(unwrapped, buffer![120i8, 100].into_array(), &mut ctx); + Ok(()) +} + +#[test] +fn test_narrow_pair_and_mixed_width_fallback() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let lhs = + NarrowArray::try_new(buffer![1i8, 5, 8].into_array(), PType::I64.into())?.into_array(); + for values in [ + buffer![2i8, 5, 7].into_array(), + buffer![2i16, 5, 7].into_array(), + ] { + let rhs = NarrowArray::try_new(values, PType::I64.into())?.into_array(); + assert_arrays_eq!( + lhs.binary(rhs, Operator::Lt)?, + BoolArray::from_iter([true, false, false]), + &mut ctx + ); + } + Ok(()) +} + +#[test] +fn test_dictionary_child_and_serialization() -> VortexResult<()> { + let session = array_session(); + let mut ctx = session.create_execution_ctx(); + let values = DictArray::try_new( + buffer![0u8, 1, 0].into_array(), + buffer![10i8, 20].into_array(), + )? + .into_array(); + let array = NarrowArray::try_new(values, PType::I64.into())?.into_array(); + let array_ctx = ArrayContext::empty(); + let buffers = array.serialize(&array_ctx, &session, &SerializeOptions::default())?; + let mut bytes = ByteBufferMut::empty(); + for buffer in buffers { + bytes.extend_from_slice(&buffer); + } + let decoded = SerializedArray::try_from(bytes.freeze())?.decode( + array.dtype(), + array.len(), + &ReadContext::new(array_ctx.to_ids()), + &session, + )?; + let values = decoded.as_::().values().clone(); + assert_eq!(values.dtype(), &DType::from(PType::I8)); + assert!(values.is::()); + assert_arrays_eq!(decoded, buffer![10i64, 20, 10].into_array(), &mut ctx); + Ok(()) +} + +#[rstest] +#[case(vec![])] +#[case(vec![255])] +#[case(vec![PType::I8 as u8, 0])] +#[case(vec![PType::U8 as u8])] +#[case(vec![PType::I64 as u8])] +#[case(vec![PType::F32 as u8])] +fn test_reject_invalid_metadata(#[case] metadata: Vec) { + let children = vec![buffer![1i8].into_array()]; + assert!( + Narrow + .deserialize( + &PType::I64.into(), + 1, + &metadata, + &[], + &children, + &array_session(), + ) + .is_err() + ); +} diff --git a/vortex-array/src/arrays/narrow/vtable.rs b/vortex-array/src/arrays/narrow/vtable.rs new file mode 100644 index 00000000000..f2ee7aa30ba --- /dev/null +++ b/vortex-array/src/arrays/narrow/vtable.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use vortex_error::VortexResult; +use vortex_error::vortex_ensure; +use vortex_error::vortex_err; +use vortex_error::vortex_panic; +use vortex_session::VortexSession; +use vortex_session::registry::CachedId; + +use super::Narrow; +use super::NarrowArraySlotsExt; +use super::NarrowSlots; +use super::compute::PARENT_RULES; +use super::validate_dtypes; +use crate::Array; +use crate::ArrayId; +use crate::ArrayParts; +use crate::ArrayRef; +use crate::ArrayView; +use crate::EmptyArrayData; +use crate::ExecutionCtx; +use crate::ExecutionResult; +use crate::VTable; +use crate::array::with_empty_buffers; +use crate::buffer::BufferHandle; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::dtype::PType; +use crate::scalar::Scalar; +use crate::serde::ArrayChildren; +use crate::vtable::OperationsVTable; +use crate::vtable::ValidityChild; +use crate::vtable::ValidityVTableFromChild; + +impl VTable for Narrow { + type TypedArrayData = EmptyArrayData; + type OperationsVTable = Self; + type ValidityVTable = ValidityVTableFromChild; + + fn id(&self) -> ArrayId { + static ID: CachedId = CachedId::new("vortex.narrow"); + *ID + } + + fn validate( + &self, + _data: &EmptyArrayData, + dtype: &DType, + len: usize, + slots: &[Option], + ) -> VortexResult<()> { + vortex_ensure!(slots.len() == 1, "NarrowArray requires one child"); + let values = slots[0] + .as_ref() + .ok_or_else(|| vortex_err!("NarrowArray requires a values child"))?; + validate_dtypes(values.dtype(), dtype)?; + vortex_ensure!(values.len() == len, "NarrowArray child length must match"); + Ok(()) + } + + fn nbuffers(_array: ArrayView<'_, Self>) -> usize { + 0 + } + + fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle { + vortex_panic!("NarrowArray buffer index {idx} out of bounds") + } + + fn buffer_name(_array: ArrayView<'_, Self>, _idx: usize) -> Option { + None + } + + fn with_buffers( + &self, + array: ArrayView<'_, Self>, + buffers: &[BufferHandle], + ) -> VortexResult> { + with_empty_buffers(self, array, buffers) + } + + fn serialize( + array: ArrayView<'_, Self>, + _session: &VortexSession, + ) -> VortexResult>> { + Ok(Some(vec![PType::try_from(array.values().dtype())? as u8])) + } + + fn deserialize( + &self, + dtype: &DType, + len: usize, + metadata: &[u8], + buffers: &[BufferHandle], + children: &dyn ArrayChildren, + _session: &VortexSession, + ) -> VortexResult> { + vortex_ensure!(buffers.is_empty(), "NarrowArray expects no buffers"); + vortex_ensure!(children.len() == 1, "NarrowArray requires one child"); + let [storage_ptype] = metadata else { + return Err(vortex_err!( + "NarrowArray expects one storage-type metadata byte" + )); + }; + let storage_ptype = PType::try_from(i32::from(*storage_ptype)) + .map_err(|err| vortex_err!("Invalid NarrowArray storage type: {err}"))?; + let storage_dtype = DType::Primitive(storage_ptype, dtype.nullability()); + validate_dtypes(&storage_dtype, dtype)?; + let values = children.get(0, &storage_dtype, len)?; + Ok(ArrayParts::new(Self, dtype.clone(), len, EmptyArrayData) + .with_slots(NarrowSlots { values }.into_slots())) + } + + fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String { + NarrowSlots::NAMES[idx].to_string() + } + + fn execute(array: Array, _ctx: &mut ExecutionCtx) -> VortexResult { + // Yield the widening cast so the child's own cast kernel can fuse decoding and widening. + Ok(ExecutionResult::done( + array.values().cast(array.dtype().clone())?, + )) + } + + fn reduce_parent( + array: ArrayView<'_, Self>, + parent: &ArrayRef, + child_idx: usize, + ) -> VortexResult> { + PARENT_RULES.evaluate(array, parent, child_idx) + } +} + +impl OperationsVTable for Narrow { + type ProbeState = (); + + fn scalar_at( + array: ArrayView<'_, Self>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + array + .values() + .execute_scalar(index, ctx)? + .cast(array.dtype()) + } +} + +impl ValidityChild for Narrow { + fn validity_child(array: ArrayView<'_, Self>) -> ArrayRef { + array.values().clone() + } +} diff --git a/vortex-array/src/session/mod.rs b/vortex-array/src/session/mod.rs index e9d0d36a55f..eb45c45c869 100644 --- a/vortex-array/src/session/mod.rs +++ b/vortex-array/src/session/mod.rs @@ -29,6 +29,7 @@ use crate::arrays::List; use crate::arrays::ListView; use crate::arrays::Map; use crate::arrays::Masked; +use crate::arrays::Narrow; use crate::arrays::Null; use crate::arrays::PiecewiseSequence; use crate::arrays::Primitive; @@ -105,6 +106,7 @@ impl Default for ArraySession { this.register(Dict); this.register(List); this.register(Masked); + this.register(Narrow); this.register(PiecewiseSequence); this.register(VarBin);