Skip to content

Avoid truncating dictionary fill codes - #10745

Merged
connortsui20 merged 1 commit into
developfrom
ct/dict-fill-code
Oct 26, 2026
Merged

connortsui20 merged 1 commit into
developfrom
ct/dict-fill-code

Conversation

@connortsui20

Copy link
Copy Markdown
Member

Summary

Fixes #10623.

Dict::fill_null converted the index of the matching dictionary value into the codes type with an unchecked as cast. A dictionary can hold more values than its codes type can index, so an unreferenced fill value could truncate to the wrong code (with u8 codes, filling with value 300 returned value 44) or panic for signed codes.

Changes

The kernel now checks the index against the range of the codes type. When the index does not fit, the kernel declines and the generic fill_null path fills the decoded array, which is slower but correct. Non-nullable codes need no fill code, so they keep the fast path. The new tests cover u8 and i8 codes at and past their maximum, and non-nullable codes over nullable values. The three cases past the maximum fail without the fix.

The Narrow work (#10733) depends on this fix, because a rule there makes narrow dictionary codes common.

🤖 Generated with Claude Code

@connortsui20 connortsui20 added the changelog/fix A bug fix label Oct 23, 2026
@codspeed

codspeed Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.18%

4 warnings, 1 note

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

⚠️ Unable to measure the execution of 2 benchmarks

These benchmarks measured no execution time, so their results are not comparable and count as unchanged. This usually happens when the compiler removes the code under test.

Prevent compiler optimizations

See affected benchmarks

⚠️ 16 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ 40 benchmarks have no flamegraph

The flamegraph wasn't generated: the symbols of the profiling trace could not be resolved, because no information about the language frames was collected.

See affected benchmarks

⚡ 4 improved benchmarks
❌ 4 regressed benchmarks
✅ 2400 untouched benchmarks
⏩ 12 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Walltime filter_neon[8] 879 ns 1,408 ns -37.57%
❌ Walltime filter_neon[64] 1.7 µs 2.2 µs -21.99%
❌ Walltime filter_neon[80] 2.4 µs 2.9 µs -16.41%
❌ Walltime bitpack_blocked_compress_avx2 6.7 µs 7.6 µs -12.25%
⚡ CPU Simulation test_scan_filter[int] 3.7 ms 3 ms +22.74%
⚡ CPU Simulation test_repeated_scan_scalar_at[int] 275.1 µs 235.4 µs +16.85%
⚡ Walltime threshold_avx512[80] 2.9 µs 2.5 µs +15.87%
⚡ Walltime deferred_bool_avx512[16384, (Columns, NullOnlyFailure)] 296.3 µs 269.2 µs +10.08%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ct/dict-fill-code (5c0ae8e) with develop (bbb562a)

Open in CodSpeed

Footnotes

  1. 12 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@connortsui20
connortsui20 marked this pull request as ready for review October 23, 2026 20:34
Comment on lines +63 to +65
if fill_index > codes_dtype.as_ptype().max_value_as_u64() {
return Ok(None);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

should we widen instead of just giving up here?

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.

we could widen here, since it just copies the codes. Can make this a follow up

The dictionary fill_null kernel converted the index of the fill value into the codes type with an unchecked cast. A dictionary can hold more values than its codes type can index, so an unreferenced fill value could truncate to the wrong code, or panic for signed codes. The kernel now checks the index against the range of the codes and declines when it does not fit, so the generic path fills the decoded array. Non-nullable codes need no fill code, so they keep the fast path.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20
connortsui20 merged commit dcb4f50 into develop Oct 26, 2026
79 of 81 checks passed
@connortsui20
connortsui20 deleted the ct/dict-fill-code branch October 26, 2026 14:55
robert3005 pushed a commit that referenced this pull request Oct 26, 2026
## Summary

- Tracking Issue: #10804

Follow-up to #10745, as suggested in review there. When the fill value
already exists in the dictionary values but its index does not fit the
codes type (e.g. `u8` codes and the fill value at index 300), the `Dict`
`fill_null` kernel returned `None`, so the generic path decoded the
whole array before filling. The kernel only rewrites the null codes, so
it can widen them instead and keep the result dictionary-encoded.

## Changes

- For nullable codes whose type cannot hold the fill index, cast the
codes to `PType::min_unsigned_ptype_for_value(fill_index)` (kept
nullable) and then fill the nulls with the index. Codes are never
negative, so the cast keeps every code. Codes that already fit and
non-nullable codes take the same path as before.
- New test `fill_unreferenced_value_widens_codes` calls the kernel
directly and checks that it returns a `Dict` with widened, non-nullable
codes and the right values, for `u8 -> u16` (256, 300), `i8 -> u8`
(128), `i8 -> u16` (256) and `u16 -> u32` (65536). All five cases fail
on `develop`, where the kernel returns `None`.

Tested with `cargo test -p vortex-array --lib` (4437 passed), `cargo
clippy -p vortex-array --all-targets --all-features -- -D warnings`, and
`cargo +nightly-2026-09-10 fmt -p vortex-array`.

AI assistance: I used an AI coding assistant to help write this change
and the description; I reviewed the code and ran the tests above myself.

Signed-off-by: Vincent <1729065730@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dict fill_null can truncate indices of unreferenced dictionary values

2 participants