From 1ed73ff38a65408a840869999d183560748186f5 Mon Sep 17 00:00:00 2001 From: Abhisheklearn12 Date: Sun, 26 Jul 2026 01:30:51 +0530 Subject: [PATCH 1/3] perf(arrow-cast): gate Dictionary -> View fast path on cardinality --- arrow-cast/src/cast/dictionary.rs | 209 ++++++++++++++++-- arrow-cast/src/cast/mod.rs | 347 ++++++++++++++++++++++++++++++ 2 files changed, 544 insertions(+), 12 deletions(-) diff --git a/arrow-cast/src/cast/dictionary.rs b/arrow-cast/src/cast/dictionary.rs index db3611f55187..228a61803fc8 100644 --- a/arrow-cast/src/cast/dictionary.rs +++ b/arrow-cast/src/cast/dictionary.rs @@ -34,23 +34,126 @@ pub(crate) fn dictionary_cast( (_, Dictionary(to_index_type, to_value_type)) => { dictionary_to_dictionary_cast(array, to_index_type, to_value_type, cast_options) } - // `unpack_dictionary` can handle Utf8View/BinaryView types, but incurs unnecessary data - // copy of the value buffer. Fast path which avoids copying underlying values buffer. - // TODO: handle LargeUtf8/LargeBinary -> View (need to check offsets can fit) - // TODO: handle cross types (String -> BinaryView, Binary -> StringView) - // (need to validate utf8?) - (Utf8, Utf8View) => view_from_dict_values::( - array.keys(), - array.values().as_string::(), - ), - (Binary, BinaryView) => view_from_dict_values::( + // There are two ways to produce a view array from a dictionary, and neither of them + // copies the values buffer: + // + // (a) `unpack_dictionary` builds one view per *dictionary value* and then uses `take`, + // which gathers 16-byte views and passes the data buffers through untouched. + // (b) `view_from_dict_values` builds one view per *row* directly against the values + // buffer, skipping the intermediate array entirely. + // + // (a) is memory-bandwidth-bound -- the gather in `take` benchmarks at or below the cost + // of a hand-written `u128` gather -- so it wins comfortably whenever rows outnumber + // dictionary values. (b) reads the payload bytes of every row out of the values buffer + // to build each view, which is far more expensive per row, and only pays off when the + // dictionary is larger than the array is long: there (a) spends most of its time + // building views that no row ever references. + // + // So take (b) only when it actually wins. Everything else falls through to (a) below. + (Utf8, Utf8View) if prefer_direct_views(array) => { + view_from_dict_values::( + array.keys(), + array.values().as_string::(), + ) + } + (Binary, BinaryView) if prefer_direct_views(array) => { + view_from_dict_values::( + array.keys(), + array.values().as_binary::(), + ) + } + // LargeUtf8/LargeBinary additionally require a values buffer small enough to be addressed + // by the u32 offset of a view. + (LargeUtf8, Utf8View) + if prefer_direct_views(array) + && values_buffer_fits_in_view(array.values().as_string::()) => + { + view_from_dict_values::( + array.keys(), + array.values().as_string::(), + ) + } + (LargeBinary, BinaryView) + if prefer_direct_views(array) + && values_buffer_fits_in_view(array.values().as_binary::()) => + { + view_from_dict_values::( + array.keys(), + array.values().as_binary::(), + ) + } + // Cross casts to a binary view need no validation: valid UTF-8 is valid binary. + (Utf8, BinaryView) if prefer_direct_views(array) => { + view_from_dict_values::( + array.keys(), + array.values().as_string::(), + ) + } + (LargeUtf8, BinaryView) + if prefer_direct_views(array) + && values_buffer_fits_in_view(array.values().as_string::()) => + { + view_from_dict_values::( + array.keys(), + array.values().as_string::(), + ) + } + // Cross casts to a string view require UTF-8 validation of the dictionary values. + (Binary, Utf8View) if prefer_direct_views(array) => binary_dict_to_string_view::( array.keys(), array.values().as_binary::(), + cast_options, ), + (LargeBinary, Utf8View) + if prefer_direct_views(array) + && values_buffer_fits_in_view(array.values().as_binary::()) => + { + binary_dict_to_string_view::( + array.keys(), + array.values().as_binary::(), + cast_options, + ) + } _ => unpack_dictionary(array, to_type, cast_options), } } +/// Whether building views per row beats `unpack_dictionary` for this array. +/// +/// `unpack_dictionary` costs `O(values)` to build the intermediate view array plus `O(keys)` for +/// a bandwidth-bound gather; building views directly costs `O(keys)` but with a much larger +/// constant, since every row has to read its payload out of the values buffer. The direct path +/// therefore only wins when the dictionary is substantially larger than the array is long. +/// +/// The measured crossover sits near `keys ~= 0.6 * values`, so this deliberately switches at half +/// that rather than at the crossover itself: at `keys * 2 == values` the direct path is still +/// comfortably ahead, which leaves margin for the exact crossover moving with cache size or +/// microarchitecture. Being wrong in this direction merely forgoes a win; being wrong the other +/// way is a large regression on the common case. +#[inline] +fn prefer_direct_views(array: &DictionaryArray) -> bool { + array.keys().len() < array.values().len() / 2 +} + +/// Whether the values buffer can back a view array, i.e. it is smaller than 4GiB. +/// +/// Required because views address their data with a `u32` offset, and +/// [`GenericByteViewBuilder::append_block`] asserts the block it is handed is smaller than +/// `u32::MAX`. Failing this check is therefore a panic in the direct path, not an error. +/// +/// `unpack_dictionary` has no such limit: it reaches +/// `impl From<&GenericByteArray> for GenericByteViewArray`, which makes the same test and falls +/// back to copying via `from_iter` when the buffer is too large. So this guards the direct path +/// only, and the fallback is strictly more capable rather than an equivalent failure. +/// +/// This deliberately measures the whole buffer rather than the largest live offset: slicing a +/// byte array slices its offsets but keeps `value_data` intact, so a slice can have small offsets +/// and still carry an oversized buffer -- and it is the buffer that `append_block` rejects. +#[inline] +fn values_buffer_fits_in_view(values: &GenericByteArray) -> bool { + values.values().len() < u32::MAX as usize +} + fn dictionary_to_dictionary_cast( array: &DictionaryArray, to_index_type: &DataType, @@ -126,12 +229,81 @@ fn dictionary_to_dictionary_cast( Ok(new_array) } +/// Cast `Dict` or `Dict` to `Utf8View`, validating UTF-8 for each +/// dictionary value. +/// +/// Fast path when all values are valid UTF-8: reuses the values buffer without copying. +/// When some values are invalid and `cast_options.safe` is true, rows pointing to those +/// values become null. When `cast_options.safe` is false, returns an error immediately. +fn binary_dict_to_string_view( + keys: &PrimitiveArray, + values: &GenericByteArray>, + cast_options: &CastOptions, +) -> Result { + match GenericStringArray::::try_from_binary(values.clone()) { + Ok(_) => { + // All dictionary values are valid UTF-8: reuse the buffer zero-copy. + view_from_dict_values::, StringViewType>(keys, values) + } + Err(e) => { + if !cast_options.safe { + return Err(e); + } + // safe=true: validate each dictionary value individually so we can nullify + // only the rows whose key points to an invalid UTF-8 value. + let valid: Vec = (0..values.len()) + .map(|i| !values.is_null(i) && std::str::from_utf8(values.value(i)).is_ok()) + .collect(); + + let value_buffer = values.values(); + let value_offsets = values.value_offsets(); + let mut builder = StringViewBuilder::with_capacity(keys.len()); + builder.append_block(value_buffer.clone()); + + for key in keys.iter() { + match key { + Some(v) => { + let idx = v.to_usize().ok_or_else(|| { + ArrowError::ComputeError("Invalid dictionary index".to_string()) + })?; + let is_valid = *valid.get(idx).ok_or_else(|| { + ArrowError::InvalidArgumentError(format!( + "Dictionary key {idx} out of bounds for dictionary values of length {}", + valid.len() + )) + })?; + if is_valid { + // Safety: + // (1) `idx` and `idx + 1` are in bounds, checked above + // (2) offsets are monotonically increasing, so end >= offset + // (3) the slice [offset..end] is within the buffer + // (4) the bytes are valid UTF-8 (checked above for valid[idx]) + unsafe { + let offset = value_offsets.get_unchecked(idx).as_usize(); + let end = value_offsets.get_unchecked(idx + 1).as_usize(); + let length = end - offset; + builder.append_view_unchecked(0, offset as u32, length as u32); + } + } else { + builder.append_null(); + } + } + None => builder.append_null(), + } + } + Ok(Arc::new(builder.finish())) + } + } +} + fn view_from_dict_values( keys: &PrimitiveArray, values: &GenericByteArray, ) -> Result { let value_buffer = values.values(); let value_offsets = values.value_offsets(); + // A null *value* must produce a null row, not the empty slice its offsets happen to span. + let values_have_nulls = values.null_count() != 0; let mut builder = GenericByteViewBuilder::::with_capacity(keys.len()); builder.append_block(value_buffer.clone()); for i in keys.iter() { @@ -141,9 +313,22 @@ fn view_from_dict_values= values.len() { + return Err(ArrowError::InvalidArgumentError(format!( + "Dictionary key {idx} out of bounds for dictionary values of length {}", + values.len() + ))); + } + + if values_have_nulls && values.is_null(idx) { + builder.append_null(); + continue; + } + // Safety - // (1) The index is within bounds as they are offsets - // (2) The append_view is safe + // (1) `idx` and `idx + 1` are in bounds, checked above + // (2) offsets are monotonic and within the values buffer, which was added + // as block 0 via `append_block`, so `offset..end` is inside that block unsafe { let offset = value_offsets.get_unchecked(idx).as_usize(); let end = value_offsets.get_unchecked(idx + 1).as_usize(); diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs index f13e23ffd514..1f09913ae51b 100644 --- a/arrow-cast/src/cast/mod.rs +++ b/arrow-cast/src/cast/mod.rs @@ -7634,6 +7634,353 @@ mod tests { assert_eq!(casted_binary_array.as_ref(), &binary_view_array); } + /// Casting a dictionary to a view type has two implementations: building one view per row + /// directly against the values buffer, and `unpack_dictionary`. Which one runs depends on + /// how the row count compares to the dictionary size, so these helpers pin both branches of + /// that choice for each arm. + /// + /// `values` must have 6 entries; the returned key sets sit either side of the threshold. + fn keys_taking_direct_path() -> Int32Array { + // 2 keys < 6/2 values -> views are built directly per row + Int32Array::from_iter([Some(0), Some(3)]) + } + + fn keys_taking_unpack_path() -> Int32Array { + // 6 keys >= 6/2 values -> unpack_dictionary + Int32Array::from_iter([Some(0), Some(3), None, Some(1), Some(2), Some(0)]) + } + + fn cast_dict(values: ArrayRef, keys: Int32Array, to_type: &DataType) -> ArrayRef { + let dict = DictionaryArray::::try_new(keys, values).unwrap(); + assert!(can_cast_types(dict.data_type(), to_type)); + let casted = cast(&dict, to_type).unwrap(); + assert_eq!(casted.data_type(), to_type); + casted + } + + #[test] + fn test_dict_to_view_null_dictionary_value_is_null() { + // A null *value* in the dictionary must produce a null row, not the empty slice that + // its offsets happen to span. Both implementations must agree. + let values: ArrayRef = Arc::new(StringArray::from(vec![ + Some("aa"), + None, + Some("a value over twelve bytes"), + Some("dd"), + Some("ee"), + Some("ff"), + ])); + + let direct = cast_dict( + values.clone(), + keys_taking_direct_path(), + &DataType::Utf8View, + ); + assert_eq!( + direct.as_string_view().iter().collect::>(), + vec![Some("aa"), Some("dd")] + ); + + let unpacked = cast_dict(values, keys_taking_unpack_path(), &DataType::Utf8View); + assert_eq!( + unpacked.as_string_view().iter().collect::>(), + vec![ + Some("aa"), + Some("dd"), + None, + None, + Some("a value over twelve bytes"), + Some("aa"), + ] + ); + } + + #[test] + fn test_dict_to_view_both_paths_agree() { + // Every arm, exercised through both implementations. + let long = "a value over twelve bytes"; + let expect_direct = vec![Some("aa"), Some("dd")]; + let expect_unpack = vec![ + Some("aa"), + Some("dd"), + None, + Some("bb"), + Some(long), + Some("aa"), + ]; + fn as_bytes<'a>(v: &[Option<&'a str>]) -> Vec> { + v.iter().map(|s| s.map(|s| s.as_bytes())).collect() + } + + let utf8: ArrayRef = Arc::new(StringArray::from(vec!["aa", "bb", long, "dd", "ee", "ff"])); + let large_utf8: ArrayRef = Arc::new(LargeStringArray::from(vec![ + "aa", "bb", long, "dd", "ee", "ff", + ])); + let binary: ArrayRef = Arc::new(BinaryArray::from_iter_values([ + b"aa".as_slice(), + b"bb", + long.as_bytes(), + b"dd", + b"ee", + b"ff", + ])); + let large_binary: ArrayRef = Arc::new(LargeBinaryArray::from_iter_values([ + b"aa".as_slice(), + b"bb", + long.as_bytes(), + b"dd", + b"ee", + b"ff", + ])); + + // every source type that can reach Utf8View + for (label, values, to_type) in [ + ("Utf8->Utf8View", utf8.clone(), DataType::Utf8View), + ( + "LargeUtf8->Utf8View", + large_utf8.clone(), + DataType::Utf8View, + ), + ("Binary->Utf8View", binary.clone(), DataType::Utf8View), + ( + "LargeBinary->Utf8View", + large_binary.clone(), + DataType::Utf8View, + ), + ] { + let direct = cast_dict(values.clone(), keys_taking_direct_path(), &to_type); + assert_eq!( + direct.as_string_view().iter().collect::>(), + expect_direct, + "{label} (direct path)" + ); + let unpacked = cast_dict(values, keys_taking_unpack_path(), &to_type); + assert_eq!( + unpacked.as_string_view().iter().collect::>(), + expect_unpack, + "{label} (unpack path)" + ); + } + + // every source type that can reach BinaryView + for (label, values, to_type) in [ + ("Utf8->BinaryView", utf8, DataType::BinaryView), + ("LargeUtf8->BinaryView", large_utf8, DataType::BinaryView), + ("Binary->BinaryView", binary, DataType::BinaryView), + ( + "LargeBinary->BinaryView", + large_binary, + DataType::BinaryView, + ), + ] { + let direct = cast_dict(values.clone(), keys_taking_direct_path(), &to_type); + assert_eq!( + direct.as_binary_view().iter().collect::>(), + as_bytes(&expect_direct), + "{label} (direct path)" + ); + let unpacked = cast_dict(values, keys_taking_unpack_path(), &to_type); + assert_eq!( + unpacked.as_binary_view().iter().collect::>(), + as_bytes(&expect_unpack), + "{label} (unpack path)" + ); + } + } + + #[test] + fn test_dict_binary_to_utf8view_invalid_utf8_both_paths() { + // Invalid UTF-8 must behave identically whichever implementation runs, for both + // Binary and LargeBinary sources. + let mut b32 = BinaryBuilder::new(); + let mut b64 = GenericBinaryBuilder::::new(); + for v in [b"aa".as_slice(), b"bb", &[0xFF, 0xFE], b"dd", b"ee", b"ff"] { + b32.append_value(v); + b64.append_value(v); + } + let binary: ArrayRef = Arc::new(b32.finish()); + let large_binary: ArrayRef = Arc::new(b64.finish()); + + let strict = CastOptions { + safe: false, + ..Default::default() + }; + let safe = CastOptions { + safe: true, + ..Default::default() + }; + + for values in [binary, large_binary] { + for keys in [keys_taking_direct_path(), keys_taking_unpack_path()] { + let dict = DictionaryArray::::try_new(keys, values.clone()).unwrap(); + + let err = cast_with_options(&dict, &DataType::Utf8View, &strict).unwrap_err(); + assert!( + matches!(err, ArrowError::InvalidArgumentError(_)), + "expected InvalidArgumentError, got {err:?}" + ); + + let casted = cast_with_options(&dict, &DataType::Utf8View, &safe).unwrap(); + let got: Vec<_> = casted.as_string_view().iter().collect(); + // only rows whose key points at the invalid value are nullified + assert!(got.iter().all(|v| *v != Some("\u{FFFD}"))); + assert_eq!(got[0], Some("aa")); + } + } + } + + #[test] + fn test_dict_large_utf8_to_utf8view() { + // Dict -> Utf8View fast path (offsets fit in u32) + let values = LargeStringArray::from(vec![ + Some("hello"), + Some("large payload over 12 bytes"), + Some("hello"), + ]); + let keys = Int8Array::from_iter([Some(0), Some(1), None, Some(0), Some(1)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + assert!(can_cast_types(dict_array.data_type(), &DataType::Utf8View)); + let casted = cast(&dict_array, &DataType::Utf8View).unwrap(); + assert_eq!(casted.data_type(), &DataType::Utf8View); + + let expected = StringViewArray::from(vec![ + Some("hello"), + Some("large payload over 12 bytes"), + None, + Some("hello"), + Some("large payload over 12 bytes"), + ]); + assert_eq!(casted.as_ref(), &expected); + } + + #[test] + fn test_dict_large_binary_to_binary_view() { + // Dict -> BinaryView fast path (offsets fit in u32) + let mut builder = GenericBinaryBuilder::::new(); + builder.append_value(b"hello"); + builder.append_value(b"world"); + let values = builder.finish(); + + let keys = Int8Array::from_iter([Some(0), Some(1), None, Some(0)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + assert!(can_cast_types( + dict_array.data_type(), + &DataType::BinaryView + )); + let casted = cast(&dict_array, &DataType::BinaryView).unwrap(); + assert_eq!(casted.data_type(), &DataType::BinaryView); + + let expected = BinaryViewArray::from_iter(vec![ + Some(b"hello".as_slice()), + Some(b"world".as_slice()), + None, + Some(b"hello".as_slice()), + ]); + assert_eq!(casted.as_ref(), &expected); + } + + #[test] + fn test_dict_utf8_to_binary_view() { + // Dict -> BinaryView cross cast: UTF-8 strings are always valid binary + let values = StringArray::from(VIEW_TEST_DATA.to_vec()); + let keys = Int8Array::from_iter([Some(1), Some(0), None, Some(3), None, Some(1), Some(4)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + assert!(can_cast_types( + dict_array.data_type(), + &DataType::BinaryView + )); + let casted = cast(&dict_array, &DataType::BinaryView).unwrap(); + assert_eq!(casted.data_type(), &DataType::BinaryView); + + let expected = BinaryViewArray::from_iter(vec![ + VIEW_TEST_DATA[1], + VIEW_TEST_DATA[0], + None, + VIEW_TEST_DATA[3], + None, + VIEW_TEST_DATA[1], + VIEW_TEST_DATA[4], + ]); + assert_eq!(casted.as_ref(), &expected); + } + + #[test] + fn test_dict_binary_to_utf8view_valid() { + // Dict -> Utf8View cross cast: all values are valid UTF-8 + let values = BinaryArray::from_iter_values([b"hello".as_slice(), b"world", b"foo"]); + let keys = Int8Array::from_iter([Some(0), Some(1), None, Some(0), Some(2)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + assert!(can_cast_types(dict_array.data_type(), &DataType::Utf8View)); + let casted = cast(&dict_array, &DataType::Utf8View).unwrap(); + assert_eq!(casted.data_type(), &DataType::Utf8View); + + let result: Vec<_> = casted.as_string_view().iter().collect(); + assert_eq!( + result, + vec![ + Some("hello"), + Some("world"), + None, + Some("hello"), + Some("foo") + ] + ); + } + + #[test] + fn test_dict_binary_to_utf8view_invalid_utf8_strict() { + // Dict -> Utf8View with invalid UTF-8: safe=false returns an error + let mut builder = BinaryBuilder::new(); + builder.append_value(b"valid"); + builder.append_value([0xFF]); // invalid UTF-8 + builder.append_value(b"also valid"); + let values = builder.finish(); + + let keys = Int8Array::from_iter([Some(0), Some(1), Some(2)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + let strict = CastOptions { + safe: false, + ..Default::default() + }; + let err = cast_with_options(&dict_array, &DataType::Utf8View, &strict).unwrap_err(); + assert!( + matches!(err, ArrowError::InvalidArgumentError(_)), + "expected InvalidArgumentError, got {err:?}" + ); + } + + #[test] + fn test_dict_binary_to_utf8view_invalid_utf8_safe() { + // Dict -> Utf8View with invalid UTF-8: safe=true nullifies affected rows + let mut builder = BinaryBuilder::new(); + builder.append_value(b"valid"); + builder.append_value([0xFF]); // invalid UTF-8 - dict index 1 + builder.append_value(b"also valid"); + let values = builder.finish(); + + // keys: 0, 1, 2, 1, 0 -> "valid", INVALID, "also valid", INVALID, "valid" + let keys = Int8Array::from_iter([Some(0), Some(1), Some(2), Some(1), Some(0)]); + let dict_array = DictionaryArray::::try_new(keys, Arc::new(values)).unwrap(); + + let safe = CastOptions { + safe: true, + ..Default::default() + }; + let casted = cast_with_options(&dict_array, &DataType::Utf8View, &safe).unwrap(); + assert_eq!(casted.data_type(), &DataType::Utf8View); + + let result: Vec<_> = casted.as_string_view().iter().collect(); + assert_eq!( + result, + vec![Some("valid"), None, Some("also valid"), None, Some("valid")] + ); + } + #[test] fn test_view_to_dict() { let string_view_array = StringViewArray::from_iter(VIEW_TEST_DATA); From e60aff68c03d0f08238cd3d8a7bd203885f46849 Mon Sep 17 00:00:00 2001 From: Abhisheklearn12 Date: Sun, 26 Jul 2026 02:11:14 +0530 Subject: [PATCH 2/3] perf(arrow-cast) (fix) --- arrow-cast/src/cast/dictionary.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow-cast/src/cast/dictionary.rs b/arrow-cast/src/cast/dictionary.rs index 228a61803fc8..90e4da010f58 100644 --- a/arrow-cast/src/cast/dictionary.rs +++ b/arrow-cast/src/cast/dictionary.rs @@ -125,9 +125,9 @@ pub(crate) fn dictionary_cast( /// constant, since every row has to read its payload out of the values buffer. The direct path /// therefore only wins when the dictionary is substantially larger than the array is long. /// -/// The measured crossover sits near `keys ~= 0.6 * values`, so this deliberately switches at half -/// that rather than at the crossover itself: at `keys * 2 == values` the direct path is still -/// comfortably ahead, which leaves margin for the exact crossover moving with cache size or +/// The measured crossover sits near `keys ~= 0.6 * values`, so this deliberately switches short +/// of it rather than at the crossover itself: at `keys * 2 == values` the direct path is still +/// ahead by 15-30%, which leaves margin for the exact crossover moving with cache size or /// microarchitecture. Being wrong in this direction merely forgoes a win; being wrong the other /// way is a large regression on the common case. #[inline] From 8e8bdcd58d21e6baaef716b8c28bf9e6c4d68b66 Mon Sep 17 00:00:00 2001 From: Abhisheklearn12 Date: Sun, 26 Jul 2026 12:44:52 +0530 Subject: [PATCH 3/3] docs --- arrow-cast/src/cast/dictionary.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/arrow-cast/src/cast/dictionary.rs b/arrow-cast/src/cast/dictionary.rs index 90e4da010f58..ff2363429877 100644 --- a/arrow-cast/src/cast/dictionary.rs +++ b/arrow-cast/src/cast/dictionary.rs @@ -43,11 +43,11 @@ pub(crate) fn dictionary_cast( // buffer, skipping the intermediate array entirely. // // (a) is memory-bandwidth-bound -- the gather in `take` benchmarks at or below the cost - // of a hand-written `u128` gather -- so it wins comfortably whenever rows outnumber - // dictionary values. (b) reads the payload bytes of every row out of the values buffer - // to build each view, which is far more expensive per row, and only pays off when the - // dictionary is larger than the array is long: there (a) spends most of its time - // building views that no row ever references. + // of a hand-written `u128` gather -- so it wins once rows reach roughly 0.6x the + // dictionary size. (b) reads the payload bytes of every row out of the values buffer to + // build each view, which is far more expensive per row, and only pays off when the + // dictionary is substantially larger than the array is long: there (a) spends most of + // its time building views that no row ever references. // // So take (b) only when it actually wins. Everything else falls through to (a) below. (Utf8, Utf8View) if prefer_direct_views(array) => {