Skip to content

perf(fill): vectorize the fill loop when arguments are variants - #451

Open
henryiii wants to merge 1 commit into
boostorg:developfrom
henryiii:fix/fill-variant-vectorization
Open

henryiii wants to merge 1 commit into
boostorg:developfrom
henryiii:fix/fill-variant-vectorization

Conversation

@henryiii

@henryiii henryiii commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

This trick allow the inner loop of a variant fill to be vectorized on Clang. On GCC, you have to pass --no-trapping-math to get it to optimize, doesn't seem to be a way around that. This provides a nice speedup on variants, better matching non-variant fills in performance.

For some reason, Claude didn't capture the full table below when making the PR from my session, so pasting it in here (includes the flag on GCC):

Results, 1D Regular(100), 1M doubles, ns/element

before after
macOS clang, Int64 3.3 0.9
macOS clang, Weight 3.6 1.1
macOS clang, Unlimited 4.1 1.9
macOS clang, 2D at 100k 5.5 3.8
Linux gcc 14 docker, PyPI 1.8.1 wheel vs source build 3.95 0.98

The GCC speedup for boost-histogram (Python) requires scikit-hep/boost-histogram#1203 too.

🤖 AI text below 🤖

When fill receives variant arguments (as boost-histogram always does from Python), index_visitor runs through variant2::visit and is not inlined. The loop then reads begin_, size_ and stride_ through this on every iteration, so clang treats it as uncountable and does not vectorize it. This PR copies the bounds to locals and splits the loop on stride_ == 1, which gives the first axis a compile-time stride.

Measured with a 1D regular<> axis, dense_storage<int64_t> and 1M normally distributed doubles (clang 17, Apple M5 Pro, ns/element):

argument before after
span<const double> 0.8 0.8
variant<double, span<const double>> 3.3 0.8

End to end in boost-histogram, h.fill(x) on a 1D Regular axis goes from 3.3 to 0.9 ns/element, and 2D fills also get faster.

gcc is not helped by this change because -ftrapping-math (its default) blocks if-conversion of regular::index; with -fno-trapping-math gcc 14 reaches the same 0.8 ns/element with this patch. A branch-free rewrite of regular::index did not help gcc, since it also treats the int/double conversions as trapping.

When the fill arguments are variants, index_visitor runs through
variant2::visit and is not inlined. The loop then reads its bounds and
stride through `this` on every iteration and the vectorizer gives up.
Copy the bounds to locals and give the first axis a compile-time stride.

Measured with a 1D regular axis and 1M doubles (clang, Apple M5 Pro):
variant argument 3.3 -> 0.8 ns/element, same as a plain span.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1
@henryiii henryiii changed the title perf: vectorize the fill loop when arguments are variants perf(fill): vectorize the fill loop when arguments are variants Nov 2, 2026
henryiii added a commit to henryiii/boost-histogram that referenced this pull request Nov 2, 2026
GCC defaults to -ftrapping-math, which blocks if-conversion of the
regular axis index computation, so the fill loop stays scalar and
branchy. Nothing in the extension relies on floating-point exceptions.

Linux (gcc 14, docker), 1D Regular axis, 1M doubles: 3.95 -> 0.98
ns/element together with boostorg/histogram#451.

Assisted-by: ClaudeCode:claude-fable-5-1
@henryiii
henryiii marked this pull request as ready for review November 3, 2026 00:20

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant