Skip to content

feat: merge dictionary values for view types, dedup exactly on overflow - #10927

Open
okhsunrog wants to merge 2 commits into
apache:mainfrom
okhsunrog:dict-merge-view-types
Open

feat: merge dictionary values for view types, dedup exactly on overflow#10927
okhsunrog wants to merge 2 commits into
apache:mainfrom
okhsunrog:dict-merge-view-types

Conversation

@okhsunrog

@okhsunrog okhsunrog commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Combining DictionaryArrays whose dictionaries were built independently has to merge their values. merge_dictionary_values deduplicates, so the merged dictionary only holds the distinct referenced values; the MutableArrayData fallback in concat/interleave concatenates them and can therefore exceed what the key type addresses even when the distinct values fit it comfortably.

Two gaps kept that merge from happening:

  • should_merge_dictionary_values returned false for any value type that is neither primitive nor an offset-based byte array, so Dictionary(_, Utf8View) and Dictionary(_, BinaryView) always took the non-deduplicating fallback. Reaching the merge path would then have hit unimplemented!() in get_masked_values, which has no arm for the view layouts either. The two are indistinguishable to a caller: identical data merges as Utf8 and fails as Utf8View.

  • The Interner backing the merge is best-effort by design: a hash collision evicts the previous occupant, so one value can be handed several keys. Merging 4 dictionaries of 60k distinct values under a UInt16 key left ~40% duplicates and overflowed anyway. This affects Utf8 dictionaries too, it is simply less visible there.

What changes are included in this PR?

All of the logic is in arrow-select/src/dictionary.rs; concat.rs and interleave.rs gain tests only.

  • should_merge_dictionary_values: compare Utf8View/BinaryView values through ArrayData::ptr_eq, which covers the views buffer and the data buffers behind it.
  • get_masked_values: extract masked values for both view layouts, through a new masked_byte_views.
  • merge_dictionary_values: keep the interner fast path, and on DictionaryKeyOverflowError retry the key assignment with exact deduplication, which allocates exactly one key per distinct value. The shared loop moves into compute_key_mappings, parameterised by the key-assignment closure.

With both, merging 16 dictionaries of 60k distinct values under a UInt16 key yields a 60k-value dictionary instead of failing.

Are these changes tested?

Yes, five new tests:

  • concat_string_view_dictionary_merges_duplicate_values and test_interleave_string_view_dictionary_merges_duplicate_values cover the first gap: two Dictionary(UInt8, Utf8View) arrays over the same 200 distinct values, which fail to combine without the fix. Both check that every key still resolves to the value it started out with.
  • concat_binary_view_dictionary_merges_duplicate_values covers the other view layout.
  • merge_string_view_dictionaries_deduplicates_exactly covers the second gap deterministically, at a cardinality where the interner alone cannot succeed: four Dictionary(UInt8, Utf8View) arrays over 200 distinct values plus an empty string and a null. It asserts exactly one key per distinct value, that null and empty string stay apart, and that every mapping preserves its value.
  • concat_dictionary_merges_values_of_many_arrays covers four dictionaries whose concatenated values would need 800 keys under a UInt8 key.

The existing *_overflow_returns_err tests continue to pin the genuine-overflow behaviour.

Are there any user-facing changes?

No API changes. Cases that previously returned ArrowError::DictionaryKeyOverflowError may now succeed.

A genuine overflow, where there really are more distinct values than the key type can address, still errors — but only after the retry pass has run in full. Dictionaries make no uniqueness guarantee, so there is no cheap way to tell the two apart in advance.

okhsunrog pushed a commit to tarantool/arrow-rs that referenced this pull request Aug 31, 2026
Combining `DictionaryArray`s whose dictionaries are built independently
(one per partition, per shard, or per record batch) has to merge their
values. `merge_dictionary_values` deduplicates, so the merged dictionary
only ever holds the distinct referenced values; the `MutableArrayData`
fallback in `concat`/`interleave` simply concatenates them and can
therefore exceed what the key type addresses even when the distinct
values comfortably fit.

Two gaps kept that merge from happening:

- `should_merge_dictionary_values` returned `false` for any value type
  that is neither primitive nor an offset-based byte array, so
  `Dictionary(_, Utf8View)` and `Dictionary(_, BinaryView)` always took
  the non-deduplicating fallback. Reaching the merge path would then have
  hit `unimplemented!()` in `get_masked_values`, which has no arm for the
  view layouts either. Add pointer comparison and masked-value extraction
  for both view types.

- The `Interner` backing the merge is best-effort by design: a hash
  collision evicts the previous occupant, so one value can be handed
  several keys. Merging 4 dictionaries of 60k distinct values under a
  `UInt16` key left ~40% duplicates and still overflowed. Keep the fast
  path, and on overflow retry the mapping with exact deduplication, which
  allocates exactly one key per distinct value.

With both, merging 16 dictionaries of 60k distinct values under a
`UInt16` key yields a 60k-value dictionary instead of failing.

Upstream: apache#10927
@github-actions github-actions Bot added arrow Changes to the arrow crate arrow-select labels Aug 31, 2026
Combining `DictionaryArray`s whose dictionaries were built independently
has to merge their values. `merge_dictionary_values` deduplicates, so the
merged dictionary only holds the distinct referenced values; the
`MutableArrayData` fallback in `concat`/`interleave` concatenates them and
can therefore exceed what the key type addresses even when the distinct
values fit it comfortably.

Two gaps kept that merge from happening:

- `should_merge_dictionary_values` returned `false` for any value type
  that is neither primitive nor an offset-based byte array, so
  `Dictionary(_, Utf8View)` and `Dictionary(_, BinaryView)` always took
  the non-deduplicating fallback. Reaching the merge path would then have
  hit `unimplemented!()` in `get_masked_values`, which has no arm for the
  view layouts either. Add pointer comparison and masked-value extraction
  for both view types.

- The `Interner` backing the merge is best-effort by design: a hash
  collision evicts the previous occupant, so one value can be handed
  several keys. Merging 4 dictionaries of 60k distinct values under a
  `UInt16` key left ~40% duplicates and overflowed anyway. Keep the fast
  path, and on overflow retry the mapping with exact deduplication, which
  allocates exactly one key per distinct value.

With both, merging 16 dictionaries of 60k distinct values under a
`UInt16` key yields a 60k-value dictionary instead of failing.
okhsunrog added a commit to tarantool/arrow-rs that referenced this pull request Aug 31, 2026
Combining `DictionaryArray`s whose dictionaries are built independently
(one per partition, per shard, or per record batch) has to merge their
values. `merge_dictionary_values` deduplicates, so the merged dictionary
only ever holds the distinct referenced values; the `MutableArrayData`
fallback in `concat`/`interleave` simply concatenates them and can
therefore exceed what the key type addresses even when the distinct
values comfortably fit.

Two gaps kept that merge from happening:

- `should_merge_dictionary_values` returned `false` for any value type
  that is neither primitive nor an offset-based byte array, so
  `Dictionary(_, Utf8View)` and `Dictionary(_, BinaryView)` always took
  the non-deduplicating fallback. Reaching the merge path would then have
  hit `unimplemented!()` in `get_masked_values`, which has no arm for the
  view layouts either. Add pointer comparison and masked-value extraction
  for both view types.

- The `Interner` backing the merge is best-effort by design: a hash
  collision evicts the previous occupant, so one value can be handed
  several keys. Merging 4 dictionaries of 60k distinct values under a
  `UInt16` key left ~40% duplicates and still overflowed. Keep the fast
  path, and on overflow retry the mapping with exact deduplication, which
  allocates exactly one key per distinct value.

With both, merging 16 dictionaries of 60k distinct values under a
`UInt16` key yields a 60k-value dictionary instead of failing.

Upstream: apache#10927
@okhsunrog
okhsunrog force-pushed the dict-merge-view-types branch from b0d906e to 47f48a3 Compare August 31, 2026 09:47
@Rich-T-kid

Copy link
Copy Markdown
Contributor

@okhsunrog I think this is more of a feature than a fix.

@okhsunrog

Copy link
Copy Markdown
Contributor Author

@okhsunrog I think this is more of a feature than a fix.

I guess we can call it a feature, it depends. What do you suggest? Rename the PR?

@Rich-T-kid

Copy link
Copy Markdown
Contributor

@okhsunrog I think this is more of a feature than a fix.

I guess we can call it a feature, it depends. What do you suggest? Rename the PR?

feature: support de-duplicating view types in interleave_dictionaries/concat

or something like that, just an idea

@Rich-T-kid

Copy link
Copy Markdown
Contributor

run benchmark interleave_kernels

@Rich-T-kid

Copy link
Copy Markdown
Contributor

run benchmark concatenate_kernel

@adriangbot

This comment was marked as duplicate.

@adriangbot

This comment was marked as duplicate.

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing dict-merge-view-types (47f48a3) to 0aece99 (merge-base) diff

Run configuration
run benchmark concatenate_kernel
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                          dict-merge-view-types                  main
-----                                                          ---------------------                  ----
concat 1024 arrays boolean 4                                   1.01     12.5±0.04µs        ? ?/sec    1.00     12.4±0.03µs        ? ?/sec
concat 1024 arrays i32 4                                       1.01      7.2±0.02µs        ? ?/sec    1.00      7.1±0.04µs        ? ?/sec
concat 1024 arrays str 4                                       1.05     23.4±0.07µs        ? ?/sec    1.00     22.3±0.09µs        ? ?/sec
concat boolean 1024                                            1.05    246.6±7.65ns        ? ?/sec    1.00    234.4±4.26ns        ? ?/sec
concat boolean 8192 over 100 arrays                            1.00      3.8±0.35µs        ? ?/sec    1.00      3.8±0.36µs        ? ?/sec
concat boolean nulls 1024                                      1.05    393.0±7.11ns        ? ?/sec    1.00   374.3±10.91ns        ? ?/sec
concat boolean nulls 8192 over 100 arrays                      1.01      9.5±0.11µs        ? ?/sec    1.00      9.5±0.08µs        ? ?/sec
concat fixed size lists                                        1.00    232.7±1.19µs        ? ?/sec    1.00    232.6±0.75µs        ? ?/sec
concat i32 1024                                                1.00    313.6±3.69ns        ? ?/sec    1.01    316.5±6.55ns        ? ?/sec
concat i32 8192 over 100 arrays                                1.00     96.6±0.99µs        ? ?/sec    1.02     98.6±0.55µs        ? ?/sec
concat i32 nulls 1024                                          1.01    518.5±4.84ns        ? ?/sec    1.00    514.3±4.37ns        ? ?/sec
concat i32 nulls 8192 over 100 arrays                          1.00    104.6±0.91µs        ? ?/sec    1.01    105.6±0.56µs        ? ?/sec
concat str 1024                                                1.06      6.3±0.01µs        ? ?/sec    1.00      6.0±0.01µs        ? ?/sec
concat str 8192 over 100 arrays                                1.00     47.4±0.19ms        ? ?/sec    1.05     49.7±0.18ms        ? ?/sec
concat str nulls 1024                                          1.00      3.2±0.00µs        ? ?/sec    1.01      3.2±0.00µs        ? ?/sec
concat str nulls 8192 over 100 arrays                          1.00     24.0±0.14ms        ? ?/sec    1.00     24.1±0.11ms        ? ?/sec
concat str_dict 1024                                           1.01      2.1±0.01µs        ? ?/sec    1.00      2.1±0.01µs        ? ?/sec
concat str_dict_sparse 1024                                    1.01      8.6±0.02µs        ? ?/sec    1.00      8.5±0.02µs        ? ?/sec
concat struct with int32 and dicts size=1024 count=2           1.01      4.9±0.02µs        ? ?/sec    1.00      4.9±0.02µs        ? ?/sec
concat utf8_view  max_str_len=128 null_density=0               1.00     58.4±0.08µs        ? ?/sec    1.00     58.6±0.05µs        ? ?/sec
concat utf8_view  max_str_len=128 null_density=0.2             1.00     59.0±0.20µs        ? ?/sec    1.00     59.2±0.05µs        ? ?/sec
concat utf8_view  max_str_len=20 null_density=0                1.00     59.0±0.09µs        ? ?/sec    1.00     59.0±0.05µs        ? ?/sec
concat utf8_view  max_str_len=20 null_density=0.2              1.00     59.7±0.06µs        ? ?/sec    1.00     59.5±0.05µs        ? ?/sec
concat utf8_view all_inline max_str_len=12 null_density=0      1.03     17.9±0.03µs        ? ?/sec    1.00     17.4±0.07µs        ? ?/sec
concat utf8_view all_inline max_str_len=12 null_density=0.2    1.02     18.5±0.29µs        ? ?/sec    1.00     18.2±0.12µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 245.1s
Peak memory 312.4 MiB
Avg memory 61.6 MiB
CPU user 220.2s
CPU sys 20.8s
Peak spill 0 B

branch

Metric Value
Wall time 245.1s
Peak memory 319.9 MiB
Avg memory 34.7 MiB
CPU user 218.6s
CPU sys 22.7s
Peak spill 0 B

File an issue against this benchmark runner

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a first pass. this is looking good!

Ill take a closer look at merge_dictionary_values when I take a second look.

Comment thread arrow-select/src/concat.rs Outdated

assert_eq!(combined.len(), 400);
assert_eq!(combined.values().data_type(), &DataType::Utf8View);
assert!(combined.values().len() < 400);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert!(combined.values().len() < 400);
assert_eq!(combined.values().len(),200);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is 251 here, not 200. With only two dictionaries the best-effort interner has enough buckets to do the merge on its own, so the exact retry never runs and 51 duplicates survive. The property worth asserting is that the result fits the key type, so I went with assert!(u8::try_from(combined.values().len()).is_ok()) and said why in a comment. The test still checks every key resolves to its original value, which is the real guarantee.

Comment thread arrow-select/src/concat.rs Outdated
Comment on lines +1713 to +1718
// Two independently-built `Dictionary<UInt8, Utf8View>` arrays holding the
// same 200 distinct values. Naively concatenating their dictionaries yields
// 400 entries, which overflows the u8 key range, but the distinct values do
// fit -- so the values must be merged and deduplicated instead. This mirrors
// a `Dictionary<UInt16, Utf8View>` column read in several partitions, each
// building its own dictionary, and then combined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should trim this. we dont need the

This mirrors a Dictionary<UInt16, Utf8View> column read in several partitions, each building its own dictionary, and then combined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +1754 to +1757
#[test]
fn concat_binary_view_dictionary_merges_duplicate_values() {
// Same as `concat_string_view_dictionary_merges_duplicate_values`, for the
// other view-typed dictionary value layout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think this is needed. both stringview/binaryview are pretty much the exact same structure. 1 test should be fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layouts are the same, but get_masked_values dispatches them through separate arms with separate downcasts:

DataType::Utf8View   => masked_byte_views(array.as_string_view(), mask),
DataType::BinaryView => masked_byte_views(array.as_binary_view(), mask),

Swap those and as_binary_view() panics on a Utf8View array, which nothing else would catch. The test is cheap, so I would rather keep it, but happy to drop it if you disagree.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    /// Downcast this to a [`StringViewArray`]
    ///
    /// # Panics
    ///
    /// Panics if this is not a [`StringViewArray`]
    fn as_string_view(&self) -> &StringViewArray {
        self.as_byte_view_opt().expect("string view array")
    }

 /// Downcast this to a [`BinaryViewArray`]
    ///
    /// # Panics
    ///
    /// Panics if this is not a [`BinaryViewArray`]
    fn as_binary_view(&self) -> &BinaryViewArray {
        self.as_byte_view_opt().expect("binary view array")
    }

these just perform a a downcast so that masked_byte_views() receives a GenericByteViewArray the code paths are identical

pub type BinaryViewArray = GenericByteViewArray<BinaryViewType>;

pub type StringViewArray = GenericByteViewArray<StringViewType>;

this isn't too important but i'm in favor or removing it. would be nice to get a third opinion

Comment thread arrow-select/src/concat.rs Outdated
Comment on lines +1804 to +1806
// Every key is addressable; how far below 200 the merge gets depends on
// whether the best-effort interner sufficed or the exact retry ran
assert!(u8::try_from(combined.values().len()).is_ok());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im confused, this always de-dupe the values right? is it possible for only some of the values to be deduplicated? if not

Suggested change
// Every key is addressable; how far below 200 the merge gets depends on
// whether the best-effort interner sufficed or the exact retry ran
assert!(u8::try_from(combined.values().len()).is_ok());
// Every key is addressable; how far below 200 the merge gets depends on
// whether the best-effort interner sufficed or the exact retry ran
assert!(u8::try_from(combined.values().len()).is_ok());
assert_eq!(total_values_len,200)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this did read badly, the comment was wrong: the merged values can never go below 200, there are exactly that many distinct ones. Four dictionaries do overflow the interner, so the exact retry runs and the count comes out at exactly 200. Asserting that now, with the comment rewritten.

Comment on lines +145 to +147
/// For each referenced value of a dictionary, its index within that dictionary's
/// values and its bytes (`None` for a null value)
type MaskedValues<'a> = Vec<(usize, Option<&'a [u8]>)>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this is very neat and makes the code easier to follow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :-)

Comment thread arrow-select/src/dictionary.rs Outdated
Comment on lines +471 to +479
#[cfg_attr(miri, ignore)] // Takes too long
fn merge_string_view_dictionaries_deduplicates_exactly() {
// Four dictionaries over the same values: 60000 distinct strings, an
// empty string and a null. Concatenating them would need 240008 keys,
// far past the UInt16 range, while the distinct values leave room to
// spare -- so the merge has to deduplicate them exactly. At this
// cardinality the best-effort interner alone leaves thousands of
// duplicates behind and overflows, which forces the exact retry.
const DISTINCT: usize = 60000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make this test smaller by using u8 key type and having far less distinct values. The same things are being tested but this way this test requires less compute and we dont need to skip the miri check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done. u8 keys over 200 distinct values still leaves the interner with more duplicates than 256 keys can address, so the exact retry is still the path under test. The miri skip is gone and the test runs in about 14 seconds there.

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing dict-merge-view-types (47f48a3) to 0aece99 (merge-base) diff

Run configuration
run benchmark interleave_kernels
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                                        dict-merge-view-types                  main
-----                                                                                        ---------------------                  ----
interleave dict(20, 0.0) 100 [0..100, 100..230, 450..1000]                                   1.00    644.3±4.02ns        ? ?/sec    1.01    651.1±3.81ns        ? ?/sec
interleave dict(20, 0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]                         1.00   1871.5±7.16ns        ? ?/sec    1.00   1878.9±8.52ns        ? ?/sec
interleave dict(20, 0.0) 1024 [0..100, 100..230, 450..1000]                                  1.00   1808.0±4.41ns        ? ?/sec    1.02  1839.9±13.63ns        ? ?/sec
interleave dict(20, 0.0) 400 [0..100, 100..230, 450..1000]                                   1.00   1021.1±3.85ns        ? ?/sec    1.03   1054.2±3.17ns        ? ?/sec
interleave dict_distinct 100                                                                 1.03      2.2±0.01µs        ? ?/sec    1.00      2.1±0.01µs        ? ?/sec
interleave dict_distinct 1024                                                                1.03      2.2±0.01µs        ? ?/sec    1.00      2.1±0.01µs        ? ?/sec
interleave dict_distinct 2048                                                                1.03      2.2±0.01µs        ? ?/sec    1.00      2.1±0.01µs        ? ?/sec
interleave dict_sparse(20, 0.0) 100 [0..100, 100..230, 450..1000]                            1.00   1537.0±4.71ns        ? ?/sec    1.02   1561.8±6.30ns        ? ?/sec
interleave dict_sparse(20, 0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]                  1.00      3.1±0.01µs        ? ?/sec    1.00      3.1±0.01µs        ? ?/sec
interleave dict_sparse(20, 0.0) 1024 [0..100, 100..230, 450..1000]                           1.00      2.7±0.01µs        ? ?/sec    1.01      2.8±0.01µs        ? ?/sec
interleave dict_sparse(20, 0.0) 400 [0..100, 100..230, 450..1000]                            1.00   1958.3±9.35ns        ? ?/sec    1.01   1972.6±7.44ns        ? ?/sec
interleave fixed_size_list<i64,5>(0.0,0.0) 100 [0..100, 100..230, 450..1000]                 1.05    515.2±1.93ns        ? ?/sec    1.00    489.7±1.53ns        ? ?/sec
interleave fixed_size_list<i64,5>(0.0,0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]       1.01      3.0±0.01µs        ? ?/sec    1.00      3.0±0.00µs        ? ?/sec
interleave fixed_size_list<i64,5>(0.0,0.0) 1024 [0..100, 100..230, 450..1000]                1.00      2.9±0.00µs        ? ?/sec    1.00      3.0±0.00µs        ? ?/sec
interleave fixed_size_list<i64,5>(0.0,0.0) 400 [0..100, 100..230, 450..1000]                 1.00   1261.1±2.13ns        ? ?/sec    1.00   1263.9±2.57ns        ? ?/sec
interleave fixed_size_list<i64,5>(0.1,0.1) 100 [0..100, 100..230, 450..1000]                 1.11      2.3±0.03µs        ? ?/sec    1.00      2.1±0.03µs        ? ?/sec
interleave fixed_size_list<i64,5>(0.1,0.1) 1024 [0..100, 100..230, 450..1000, 0..1000]       1.14     19.2±0.28µs        ? ?/sec    1.00     16.9±0.36µs        ? ?/sec
interleave fixed_size_list<i64,5>(0.1,0.1) 1024 [0..100, 100..230, 450..1000]                1.13     19.1±0.29µs        ? ?/sec    1.00     16.9±0.36µs        ? ?/sec
interleave fixed_size_list<i64,5>(0.1,0.1) 400 [0..100, 100..230, 450..1000]                 1.12      7.8±0.12µs        ? ?/sec    1.00      6.9±0.14µs        ? ?/sec
interleave i32(0.0) 100 [0..100, 100..230, 450..1000]                                        1.00    212.9±2.08ns        ? ?/sec    1.00    213.1±1.76ns        ? ?/sec
interleave i32(0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]                              1.00    959.1±2.88ns        ? ?/sec    1.00    963.3±2.44ns        ? ?/sec
interleave i32(0.0) 1024 [0..100, 100..230, 450..1000]                                       1.00   1014.8±2.63ns        ? ?/sec    1.00   1019.4±2.92ns        ? ?/sec
interleave i32(0.0) 400 [0..100, 100..230, 450..1000]                                        1.00    464.8±2.13ns        ? ?/sec    1.14    527.8±2.45ns        ? ?/sec
interleave i32(0.5) 100 [0..100, 100..230, 450..1000]                                        1.00    450.0±2.92ns        ? ?/sec    1.00    449.1±2.07ns        ? ?/sec
interleave i32(0.5) 1024 [0..100, 100..230, 450..1000, 0..1000]                              1.00      2.9±0.01µs        ? ?/sec    1.01      2.9±0.01µs        ? ?/sec
interleave i32(0.5) 1024 [0..100, 100..230, 450..1000]                                       1.00      3.0±0.02µs        ? ?/sec    1.01      3.0±0.02µs        ? ?/sec
interleave i32(0.5) 400 [0..100, 100..230, 450..1000]                                        1.05   1340.2±3.01ns        ? ?/sec    1.00   1281.0±8.20ns        ? ?/sec
interleave list<i64>(0.0,0.0,20) 100 [0..100, 100..230, 450..1000]                           1.00    848.4±2.83ns        ? ?/sec    1.00    846.5±2.81ns        ? ?/sec
interleave list<i64>(0.0,0.0,20) 1024 [0..100, 100..230, 450..1000, 0..1000]                 1.00      6.6±0.01µs        ? ?/sec    1.00      6.6±0.01µs        ? ?/sec
interleave list<i64>(0.0,0.0,20) 1024 [0..100, 100..230, 450..1000]                          1.01      6.6±0.02µs        ? ?/sec    1.00      6.5±0.01µs        ? ?/sec
interleave list<i64>(0.0,0.0,20) 400 [0..100, 100..230, 450..1000]                           1.00      2.7±0.01µs        ? ?/sec    1.01      2.8±0.00µs        ? ?/sec
interleave list<i64>(0.1,0.1,20) 100 [0..100, 100..230, 450..1000]                           1.03      2.5±0.01µs        ? ?/sec    1.00      2.4±0.03µs        ? ?/sec
interleave list<i64>(0.1,0.1,20) 1024 [0..100, 100..230, 450..1000, 0..1000]                 1.07     20.5±0.17µs        ? ?/sec    1.00     19.3±0.31µs        ? ?/sec
interleave list<i64>(0.1,0.1,20) 1024 [0..100, 100..230, 450..1000]                          1.07     20.7±0.14µs        ? ?/sec    1.00     19.4±0.31µs        ? ?/sec
interleave list<i64>(0.1,0.1,20) 400 [0..100, 100..230, 450..1000]                           1.04      8.3±0.04µs        ? ?/sec    1.00      8.0±0.12µs        ? ?/sec
interleave list_view<i64>(0.0,0.0,20) 100 [0..100, 100..230, 450..1000]                      1.00      2.3±0.01µs        ? ?/sec    1.03      2.4±0.01µs        ? ?/sec
interleave list_view<i64>(0.0,0.0,20) 1024 [0..100, 100..230, 450..1000, 0..1000]            1.00     14.7±0.03µs        ? ?/sec    1.01     14.9±0.02µs        ? ?/sec
interleave list_view<i64>(0.0,0.0,20) 1024 [0..100, 100..230, 450..1000]                     1.00     14.6±0.02µs        ? ?/sec    1.01     14.7±0.03µs        ? ?/sec
interleave list_view<i64>(0.0,0.0,20) 400 [0..100, 100..230, 450..1000]                      1.00      6.5±0.01µs        ? ?/sec    1.00      6.5±0.01µs        ? ?/sec
interleave list_view<i64>(0.1,0.1,20) 100 [0..100, 100..230, 450..1000]                      1.00      3.7±0.01µs        ? ?/sec    1.00      3.7±0.01µs        ? ?/sec
interleave list_view<i64>(0.1,0.1,20) 1024 [0..100, 100..230, 450..1000, 0..1000]            1.00     25.2±0.16µs        ? ?/sec    1.00     25.2±0.20µs        ? ?/sec
interleave list_view<i64>(0.1,0.1,20) 1024 [0..100, 100..230, 450..1000]                     1.00     25.3±0.16µs        ? ?/sec    1.00     25.3±0.22µs        ? ?/sec
interleave list_view<i64>(0.1,0.1,20) 400 [0..100, 100..230, 450..1000]                      1.00     10.8±0.05µs        ? ?/sec    1.00     10.9±0.07µs        ? ?/sec
interleave list_view_overlapping<i64>(80x,20) 100 [0..100, 100..230, 450..1000]              1.00      2.5±0.01µs        ? ?/sec    1.03      2.5±0.01µs        ? ?/sec
interleave list_view_overlapping<i64>(80x,20) 1024 [0..100, 100..230, 450..1000, 0..1000]    1.00      6.2±0.04µs        ? ?/sec    1.00      6.2±0.02µs        ? ?/sec
interleave list_view_overlapping<i64>(80x,20) 1024 [0..100, 100..230, 450..1000]             1.00      6.0±0.02µs        ? ?/sec    1.01      6.0±0.02µs        ? ?/sec
interleave list_view_overlapping<i64>(80x,20) 400 [0..100, 100..230, 450..1000]              1.00      3.0±0.01µs        ? ?/sec    1.00      3.0±0.00µs        ? ?/sec
interleave map<utf8,i64>(0.0,5,8) 100 [0..100, 100..230, 450..1000]                          1.00      2.5±0.01µs        ? ?/sec    1.01      2.5±0.01µs        ? ?/sec
interleave map<utf8,i64>(0.0,5,8) 1024 [0..100, 100..230, 450..1000, 0..1000]                1.00     19.0±0.05µs        ? ?/sec    1.01     19.1±0.04µs        ? ?/sec
interleave map<utf8,i64>(0.0,5,8) 1024 [0..100, 100..230, 450..1000]                         1.00     18.8±0.04µs        ? ?/sec    1.00     18.8±0.03µs        ? ?/sec
interleave map<utf8,i64>(0.0,5,8) 400 [0..100, 100..230, 450..1000]                          1.00      7.8±0.02µs        ? ?/sec    1.01      7.9±0.02µs        ? ?/sec
interleave map<utf8,i64>(0.1,5,8) 100 [0..100, 100..230, 450..1000]                          1.00      2.6±0.01µs        ? ?/sec    1.00      2.6±0.01µs        ? ?/sec
interleave map<utf8,i64>(0.1,5,8) 1024 [0..100, 100..230, 450..1000, 0..1000]                1.00     19.3±0.04µs        ? ?/sec    1.00     19.3±0.04µs        ? ?/sec
interleave map<utf8,i64>(0.1,5,8) 1024 [0..100, 100..230, 450..1000]                         1.00     19.0±0.05µs        ? ?/sec    1.00     18.9±0.06µs        ? ?/sec
interleave map<utf8,i64>(0.1,5,8) 400 [0..100, 100..230, 450..1000]                          1.00      7.9±0.02µs        ? ?/sec    1.00      7.9±0.02µs        ? ?/sec
interleave ree_i32<dict<u32,utf8>>(64 runs) 100 [0..100, 100..230, 450..1000]                1.00      4.2±0.01µs        ? ?/sec    1.03      4.4±0.02µs        ? ?/sec
interleave ree_i32<dict<u32,utf8>>(64 runs) 1024 [0..100, 100..230, 450..1000, 0..1000]      1.01     20.8±0.09µs        ? ?/sec    1.00     20.7±0.10µs        ? ?/sec
interleave ree_i32<dict<u32,utf8>>(64 runs) 1024 [0..100, 100..230, 450..1000]               1.00     20.3±0.07µs        ? ?/sec    1.00     20.4±0.08µs        ? ?/sec
interleave ree_i32<dict<u32,utf8>>(64 runs) 400 [0..100, 100..230, 450..1000]                1.01      9.5±0.03µs        ? ?/sec    1.00      9.5±0.03µs        ? ?/sec
interleave ree_i32<i64>(64 runs) 100 [0..100, 100..230, 450..1000]                           1.00      3.3±0.01µs        ? ?/sec    1.02      3.4±0.01µs        ? ?/sec
interleave ree_i32<i64>(64 runs) 1024 [0..100, 100..230, 450..1000, 0..1000]                 1.00     19.0±0.08µs        ? ?/sec    1.02     19.3±0.09µs        ? ?/sec
interleave ree_i32<i64>(64 runs) 1024 [0..100, 100..230, 450..1000]                          1.00     18.8±0.07µs        ? ?/sec    1.00     18.9±0.09µs        ? ?/sec
interleave ree_i32<i64>(64 runs) 400 [0..100, 100..230, 450..1000]                           1.01      8.4±0.03µs        ? ?/sec    1.00      8.3±0.03µs        ? ?/sec
interleave str(20, 0.0) 100 [0..100, 100..230, 450..1000]                                    1.00    601.7±2.17ns        ? ?/sec    1.00    603.6±3.09ns        ? ?/sec
interleave str(20, 0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]                          1.00      4.6±0.01µs        ? ?/sec    1.00      4.6±0.01µs        ? ?/sec
interleave str(20, 0.0) 1024 [0..100, 100..230, 450..1000]                                   1.00      4.6±0.01µs        ? ?/sec    1.00      4.6±0.01µs        ? ?/sec
interleave str(20, 0.0) 400 [0..100, 100..230, 450..1000]                                    1.01   1901.3±8.93ns        ? ?/sec    1.00   1889.1±7.25ns        ? ?/sec
interleave str(20, 0.5) 100 [0..100, 100..230, 450..1000]                                    1.00    749.9±2.17ns        ? ?/sec    1.00    748.8±3.37ns        ? ?/sec
interleave str(20, 0.5) 1024 [0..100, 100..230, 450..1000, 0..1000]                          1.00      6.0±0.01µs        ? ?/sec    1.00      6.0±0.02µs        ? ?/sec
interleave str(20, 0.5) 1024 [0..100, 100..230, 450..1000]                                   1.00      5.9±0.02µs        ? ?/sec    1.00      5.9±0.02µs        ? ?/sec
interleave str(20, 0.5) 400 [0..100, 100..230, 450..1000]                                    1.00      2.5±0.01µs        ? ?/sec    1.00      2.5±0.01µs        ? ?/sec
interleave str_view(0.0) 100 [0..100, 100..230, 450..1000]                                   1.00    545.2±2.91ns        ? ?/sec    1.03    563.4±3.36ns        ? ?/sec
interleave str_view(0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]                         1.00      2.6±0.01µs        ? ?/sec    1.02      2.6±0.01µs        ? ?/sec
interleave str_view(0.0) 1024 [0..100, 100..230, 450..1000]                                  1.00      2.5±0.01µs        ? ?/sec    1.00      2.5±0.01µs        ? ?/sec
interleave str_view(0.0) 400 [0..100, 100..230, 450..1000]                                   1.01   1228.7±3.96ns        ? ?/sec    1.00   1220.3±2.45ns        ? ?/sec
interleave struct(i32(0.0), i32(0.0) 100 [0..100, 100..230, 450..1000]                       1.00    649.3±3.68ns        ? ?/sec    1.02    659.6±8.55ns        ? ?/sec
interleave struct(i32(0.0), i32(0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]             1.00      2.2±0.01µs        ? ?/sec    1.00      2.2±0.01µs        ? ?/sec
interleave struct(i32(0.0), i32(0.0) 1024 [0..100, 100..230, 450..1000]                      1.00      2.1±0.01µs        ? ?/sec    1.05      2.2±0.01µs        ? ?/sec
interleave struct(i32(0.0), i32(0.0) 400 [0..100, 100..230, 450..1000]                       1.00   1145.6±3.35ns        ? ?/sec    1.01   1154.1±4.86ns        ? ?/sec
interleave struct(i32(0.0), str(20, 0.0) 100 [0..100, 100..230, 450..1000]                   1.01   1049.5±7.03ns        ? ?/sec    1.00   1037.4±7.84ns        ? ?/sec
interleave struct(i32(0.0), str(20, 0.0) 1024 [0..100, 100..230, 450..1000, 0..1000]         1.00      5.9±0.02µs        ? ?/sec    1.00      5.9±0.01µs        ? ?/sec
interleave struct(i32(0.0), str(20, 0.0) 1024 [0..100, 100..230, 450..1000]                  1.01      5.9±0.02µs        ? ?/sec    1.00      5.8±0.01µs        ? ?/sec
interleave struct(i32(0.0), str(20, 0.0) 400 [0..100, 100..230, 450..1000]                   1.00      2.6±0.01µs        ? ?/sec    1.00      2.6±0.01µs        ? ?/sec
interleave struct(str(20, 0.0), str(20, 0.0)) 100 [0..100, 100..230, 450..1000]              1.00   1411.0±7.94ns        ? ?/sec    1.01   1419.6±5.77ns        ? ?/sec
interleave struct(str(20, 0.0), str(20, 0.0)) 1024 [0..100, 100..230, 450..1000, 0..1000]    1.08     10.4±0.10µs        ? ?/sec    1.00      9.6±0.02µs        ? ?/sec
interleave struct(str(20, 0.0), str(20, 0.0)) 1024 [0..100, 100..230, 450..1000]             1.00      9.5±0.03µs        ? ?/sec    1.00      9.5±0.02µs        ? ?/sec
interleave struct(str(20, 0.0), str(20, 0.0)) 400 [0..100, 100..230, 450..1000]              1.00      4.1±0.01µs        ? ?/sec    1.00      4.1±0.01µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 840.2s
Peak memory 16.6 MiB
Avg memory 16.2 MiB
CPU user 837.6s
CPU sys 0.1s
Peak spill 0 B

branch

Metric Value
Wall time 845.2s
Peak memory 17.1 MiB
Avg memory 16.3 MiB
CPU user 839.0s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@okhsunrog okhsunrog changed the title fix: merge dictionary values for view types, dedup exactly on overflow feat: merge dictionary values for view types, dedup exactly on overflow Sep 1, 2026
Address review feedback on the tests:

- Drop the sentence motivating the view-type case from a downstream
  reader's point of view; the test stands on its own.
- Two dictionaries stay within reach of the best-effort interner, so a
  few duplicates survive the merge and the value count is not exactly
  200 there. Assert what actually holds, that the merged values fit the
  key type.
- Four dictionaries leave the interner with more duplicates than the key
  type can address, so the exact retry runs and the merged values come
  out one per distinct value. Assert that count exactly.
- Shrink `merge_string_view_dictionaries_deduplicates_exactly` to `u8`
  keys over 200 distinct values. That still overflows the interner and
  forces the exact retry, while running fast enough under miri that the
  test no longer has to be skipped there.
@okhsunrog

Copy link
Copy Markdown
Contributor Author

Renamed the PR

@okhsunrog
okhsunrog requested a review from Rich-T-kid September 1, 2026 16:05
@Rich-T-kid

Copy link
Copy Markdown
Contributor

will take another look in the morning 👍

@Jefffrey Jefffrey added the enhancement Any new improvement worthy of a entry in the changelog label Sep 2, 2026
@okhsunrog

Copy link
Copy Markdown
Contributor Author

@Rich-T-kid could you take a look, please?

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this PR makes sense to me, but I think we should split this up into two PR because currently its doing two things

  1. its introducing support for view value types (utf8View,BinaryView)
  2. its introduces de-duplication to merge_dictionary_values which it goes about in not the most optimal way.

in terms of support for view types the PR good to go.
As for deduplication I think we need a separate discussion as to why it'd be worth to do when merge_dictionary_values goes out of its way to not do it, as well as benchmarks to gauge any performance degradation.

would be happy to hear your thoughts on this @okhsunrog

Comment on lines +1754 to +1757
#[test]
fn concat_binary_view_dictionary_merges_duplicate_values() {
// Same as `concat_string_view_dictionary_merges_duplicate_values`, for the
// other view-typed dictionary value layout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    /// Downcast this to a [`StringViewArray`]
    ///
    /// # Panics
    ///
    /// Panics if this is not a [`StringViewArray`]
    fn as_string_view(&self) -> &StringViewArray {
        self.as_byte_view_opt().expect("string view array")
    }

 /// Downcast this to a [`BinaryViewArray`]
    ///
    /// # Panics
    ///
    /// Panics if this is not a [`BinaryViewArray`]
    fn as_binary_view(&self) -> &BinaryViewArray {
        self.as_byte_view_opt().expect("binary view array")
    }

these just perform a a downcast so that masked_byte_views() receives a GenericByteViewArray the code paths are identical

pub type BinaryViewArray = GenericByteViewArray<BinaryViewType>;

pub type StringViewArray = GenericByteViewArray<StringViewType>;

this isn't too important but i'm in favor or removing it. would be nice to get a third opinion

Comment on lines +293 to +296
// The duplicates left behind by the interner's hash collisions can push
// the output past what the key type can address even though the distinct
// values would have fit. Retry with exact deduplication, which allocates
// exactly one key per distinct value at the cost of a hash map.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit unrelated to this PR but I think it'd be useful to add some of this info to the Internern::intern(). its easier to understand why we retry on DictionaryKeyOverFlowError if the Internern::intern() was a bit more clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate arrow-select enhancement Any new improvement worthy of a entry in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dictionary values are not merged for view types, and the merge interner can overflow the key type on its own

4 participants