Skip to content

perf: allow cross-crate inlining of BitBuffer accessors - #9285

Merged
connortsui20 merged 1 commit into
developfrom
ct/inline-buffers
Aug 7, 2026
Merged

perf: allow cross-crate inlining of BitBuffer accessors#9285
connortsui20 merged 1 commit into
developfrom
ct/inline-buffers

Conversation

@connortsui20

@connortsui20 connortsui20 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Related: #9259

Rationale for this change

Buffer<T> and BufferMut<T> are generic, so their MIR travels in the rlib and a downstream crate inlines them without LTO. BitBuffer, BitBufferMut, BitBufferView and BitBufferMutView are concrete, so a method without #[inline] reaches a downstream crate as a declaration only. The thin ones cost a real call per invocation, and the caller loses the offset and length constants it needs to fold the surrounding code.

Note that #[inline] is not what enables inlining across codegen units inside a crate. MIR inlining runs before partitioning, and profile.bench sets lto = false, which is thin-local LTO rather than no LTO. Both already inline small functions across a codegen unit boundary within vortex-buffer. The attribute only matters across the crate boundary, which is where every measurement below was taken.

What changes are included in this PR?

#[inline] on the thin wrappers of those four types: constructors, iterator factories, and the methods that forward a slice, an offset and a length. Methods with a body worth outlining keep their current behavior, including append_buffer, whose bitvec fallback path is too large to justify inlining for the 1.6% it measured.

Marginal instructions per iteration, measured with callgrind against a probe crate that calls vortex-buffer across a crate boundary, built at the profile.bench settings:

kernel before after fat LTO
BitBuffer::slice 8972 6415 5390
BitBufferView::slice 3017 2186 1994

slice_vortex_buffer measures 1.904us to 1.829us of wall time, and stops intermittently landing on a slower 2.12us mode. The wall-clock gain is much smaller than the instruction-count gain because these paths are bound by refcount atomics rather than by instruction issue. CodSpeed measures instruction counts, so expect its numbers to sit closer to the table than to the wall time.

Measurement method, and the parts of the LTO win this does not reach

The probe is a separate crate that calls vortex-buffer over a real crate boundary, so the cross-crate path is the one under test. Each kernel runs at two iteration counts under valgrind --tool=callgrind and the totals are differenced, which cancels process startup, CPU feature warmup and setup allocations. This matches what CodSpeed's Simulation mode reports.

BitBuffer::set_indices goes 77702 to 69503 at codegen-units = 1. At codegen-units = 16 the baseline lands on the faster value about half the time depending on how thin-local LTO's import decisions fall, so the change makes a previously partition-dependent win reliable rather than producing a new one.

The remaining fat LTO gap on these benchmarks is not cross-crate inlining, and #[inline] cannot reach it:

  • set_slices is 1.68x, and it is arrow's BitSliceIterator. Ten #[inline] attributes on the BitSliceIterator and UnalignedBitChunk chain recover 13729 to 10026 with no LTO. That belongs upstream in arrow-rs.
  • from_iter and bitand_owned are 1.9x. Nightly -Zcross-crate-inline-threshold=always does not move either one, so no amount of MIR availability explains them. Fat LTO is partially rescuing a per-bit read-modify-write loop by unrolling it. The real fix is that BitBuffer::from_iter costs 5.19 instructions per bit while BitBufferMut::from(&[bool]) does the same job at 0.19 through the word-packing kernels in pack.rs. Follow-up.
  • value_vortex_buffer and value_arrow_buffer both reported +56.8% on Build benchmarks with whole-program optimization #9259. The probe measures both at exactly 147467 instructions in every profile. That row is divan overhead.

Thin LTO was measured as an alternative and rejected: 0 to 2.5% across these kernels for 2.5x the bench build time.

`vortex-buffer`'s `Buffer<T>` and `BufferMut<T>` are generic, so their MIR
is exported and downstream crates can inline them without LTO. `BitBuffer`,
`BitBufferMut`, `BitBufferView` and `BitBufferMutView` are concrete types,
so any method without `#[inline]` is only a symbol to a downstream crate.
The thin ones -- constructors, iterator factories, and the wrappers that do
nothing but forward a slice, an offset and a length -- become a real call
per invocation, and the caller loses the offset and length constants it
needs to fold the surrounding code.

This marks those wrappers `#[inline]`. It leaves alone anything with a body
worth outlining, including `append_buffer`, whose bitvec fallback path is
too large to justify inlining for the 1.6% it measured.

Measured with callgrind against a probe crate that calls `vortex-buffer`
across a crate boundary, at the `profile.bench` settings
(`codegen-units = 16`, no LTO), as marginal instructions per iteration:

    BitBuffer::slice        8972 -> 6415  (-28.5%)
    BitBufferView::slice    3017 -> 2186  (-27.5%)

At `codegen-units = 1`, which removes codegen-unit partitioning as a
variable, `BitBuffer::set_indices` goes 77702 -> 69503 (-10.6%); at
`codegen-units = 16` it already lands on the faster value about half the
time depending on how the partition falls, so the change makes a win that
was previously luck-dependent reliable instead.

`slice_vortex_buffer` measures 1.904us -> 1.829us of wall time, and stops
intermittently landing on a slower 2.12us mode. The wall-clock gain is much
smaller than the instruction-count gain because these paths are bound by
refcount atomics rather than by instruction issue.

Signed-off-by: "Connor Tsui" <connor@spiraldb.com>
@connortsui20 connortsui20 added the changelog/performance A performance improvement label Aug 7, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 7 improved benchmarks
❌ 2 regressed benchmarks
✅ 1928 untouched benchmarks
⏩ 51 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation take[small_m/shuffled/primitive/nonnull/chunks=16384/indices=16] 1.2 ms 1.3 ms -10.5%
Simulation chunked_into_canonical[(1000, 50, 8, 64)] 17.8 ms 19.8 ms -10.08%
Simulation slice_vortex_buffer 14.4 µs 10.9 µs +32.96%
Simulation decompress[u32, (10000, 256)] 100.4 µs 82.9 µs +21.08%
Simulation set_indices_vortex_buffer[128] 2.2 µs 1.9 µs +18.28%
Simulation bitwise_not_vortex_buffer[1024] 7.4 µs 6.3 µs +16.34%
Simulation bitwise_not_vortex_buffer[2048] 7.8 µs 6.8 µs +15.32%
Simulation bitwise_not_vortex_buffer[128] 9.1 µs 8.1 µs +11.92%
Simulation iter_vortex_buffer[128] 2 µs 1.8 µs +11.46%

Tip

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


Comparing ct/inline-buffers (bc1c689) with develop (66c447e)

Open in CodSpeed

Footnotes

  1. 51 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

Copy link
Copy Markdown
Member Author

im pretty sure that the regressions are the typical ones that are noisy

@connortsui20
connortsui20 merged commit 19f771f into develop Aug 7, 2026
93 of 95 checks passed
@connortsui20
connortsui20 deleted the ct/inline-buffers branch August 7, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/performance A performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants