Repository navigation
perf(array): extend small-table take SIMD - #10317
joseph-isaacs wants to merge 3 commits into
Conversation
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Merging this PR will improve performance by 18.08%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | decode_primitives[u8, (2000, 32)] |
43.3 µs | 34.3 µs | +26.22% |
| ⚡ | Simulation | decode_primitives[u8, (1000, 32)] |
35.9 µs | 32.5 µs | +10.46% |
| 🆕 | Simulation | allocate_and_copy |
N/A | 2 ms | N/A |
| 🆕 | Simulation | u8[16] |
N/A | 2 ms | N/A |
| 🆕 | Simulation | u8[2] |
N/A | 2 ms | N/A |
| 🆕 | Simulation | u8[32] |
N/A | 2 ms | N/A |
| 🆕 | Simulation | u8[4] |
N/A | 2 ms | N/A |
| 🆕 | Simulation | u8[8] |
N/A | 2 ms | N/A |
| 🆕 | Simulation | gather_u16[128] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[16] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[2] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[32] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[4] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[64] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u16[8] |
N/A | 8.8 ms | N/A |
| 🆕 | Simulation | gather_u32[16] |
N/A | 7 ms | N/A |
| 🆕 | Simulation | gather_u32[2] |
N/A | 7 ms | N/A |
| 🆕 | Simulation | gather_u32[32] |
N/A | 7 ms | N/A |
| 🆕 | Simulation | gather_u32[4] |
N/A | 7 ms | N/A |
| 🆕 | Simulation | gather_u32[64] |
N/A | 7 ms | N/A |
| ... | ... | ... | ... | ... | ... |
ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/small-table-take-vbmi (b3d8fab) with develop (c6e51ba)
Footnotes
-
518 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. ↩
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Too many benchmarks in a single uploadThe performance report could not be generated because there were too many benchmarks in a single upload to CodSpeed. We recommend sharding your benchmarks into smaller uploads, with max 1000 benchmarks per upload. See the documentation for more information. |
Summary
Small dictionary take falls back to general lookup once byte tables exceed 16 entries, and wider values miss the register-table optimization. Extend the existing AVX2 and NEON kernels using u8 codes, selecting only the value widths that showed wins on each measured architecture.
Arbitrary cardinalities within each limit are supported. NEON requires at least 64 output rows, or 1,024 rows for eight-byte values. Wider codes and unsupported widths/cardinalities retain existing fallbacks. Physical widths cover signed/unsigned integers, floats, and corresponding decimal storage; variable-length strings and bit-packed booleans are outside this path.
Implementation
VPSHUFBand blends. Runtime-gated AVX-512F/BW/VBMI usesVPERMB/VPERMT2B. Two-byte lookup supports the limits above.TBL, combining 64-byte banks for larger tables. Independent maximum-code accumulators shorten the bounds-check dependency chain.ST2/ST4.try_takedispatch and allocator behavior. Enable four-/eight-byte small-table dispatch only on little-endian AArch64; the existing x86 implementations and tests are unchanged by consolidation.fearless_simd, and opt-in nightlystd::simdbenchmarks. Add the full dictionary benchmark across all primitive types and the standalone NEON/generic comparison from [codex] perf: extend small dictionary take across NEON value widths #10315.x86 performance
Intel Xeon 6975P-C, 1,048,576 indices:
The final u8 kernel measured ~29.95 µs at 32 values versus ~30.77 µs for the stable generic
fearless_simdcomparison in the same run. The raw allocation controls measured ~19.81 µs for reusable memcpy and ~21.48 µs for a fresh Vortex buffer plus copy.Profiling showed the apparent ~30% Vortex gap was primarily checked-kernel instruction cost plus an allocation mismatch in the benchmark, not framework dispatch. Replacing per-vector subtract/OR bounds checks with max reduction removed those hotspots; framework/execution accounted for roughly 6% of the profiled sample.
On x86, u32/u64 register-table variants were benchmarked but intentionally not selected in production because they did not beat the existing memory-bound fallback.
These x86 measurements were reported on the pre-consolidation branch ending at b3d8fab. The consolidation does not alter those x86 kernels.
NEON — Apple M3 Max
End-to-end dictionary execution, including allocation, one million uniform non-null u8 codes. Times are microseconds. Rust 1.98.0, native aarch64 macOS, no CPU pinning.
Baseline is 3c0a33e with the expanded benchmark source. It already has the earlier PR's 32-entry byte fast path; larger byte tables and wider values use the scalar default. Candidate is cac5eca from #10315, before consolidation. These are historical measurements of the imported kernels; the combined PR head has not been benchmarked.
Entries are medians of three run medians, with 31 samples of four iterations. Run order was baseline/candidate, candidate/baseline, baseline/candidate. All 1,408 cases ran in each executable: 11 primitive types × 16 cardinalities × four lengths × two distributions.
The host showed scheduling variation, including in unchanged fallback cases. For example, u64/32 candidate run medians were 166.5, 156.4, and 325.3 µs. Small differences at 64 rows should not be treated as stable wins. These measurements establish gains on this M3 Max; they are not measurements of other NEON implementations.
Native NEON versus generic fearless_simd
Kernel-only comparison on the same M3 Max with Rust 1.98.0, one million uniform codes. Times are microseconds. Output is preallocated; table preparation, dispatch, bounds checks, lookup, and tails are timed. The generator differs from the end-to-end benchmark, so compare columns within this table.
The 16-/32-byte native entries are experimental kernels, not enabled production paths. Most enabled cases are effectively tied between native and generic SIMD. The byte/16 native loop uses four independent accumulators while the generic version uses a single 16-lane vector; this comparison applies to the supplied kernels, not to the libraries in general.
Each entry is the median of three run medians. Each run rotates the three variants over 31 samples, with
clamp(2_000_000 / output_bytes, 1, 256)iterations per sample. All 504 datasets and setup correctness checks ran successfully.Reproduction
For the end-to-end NEON comparison, keep
take_small_dict.rsidentical in the baseline and candidate and save each emitted executable:For the standalone generic comparison on AArch64:
cargo run --release --manifest-path benchmarks/take-small-dict/Cargo.toml > results.jsonlUse
cargo bench -p vortex-array --bench dict_take_smallfor the existing direct-kernel, fallback, allocation, and generic SIMD comparisons.NEON comparison methodology
Validation
The combined revision 04f4c4e was inspected for dispatch compatibility and preservation of the x86 source and existing tests. Builds, tests, linting, formatting, and benchmarks were not rerun during consolidation; the results below belong to the earlier revisions.
Earlier x86 revision b3d8fab
cargo nextest run -p vortex-array— 4,004 passed, 1 skippedcargo clippy -p vortex-array --all-targets --all-features -- -D warningscargo check -p vortex-array --lib --target aarch64-unknown-linux-gnuRUSTFLAGS='--cfg vortex_nightly_portable_simd -C target-cpu=native' cargo +nightly check -p vortex-array --bench dict_take_smallcargo +nightly fmt --all -- --checkgit diff --checkImported NEON revision cac5eca