Skip to content

[STACKED] perf(fill): branch-free fill for axes without flow bins - #453

Draft
henryiii wants to merge 5 commits into
boostorg:developfrom
henryiii:perf/branchless-optional-index
Draft

henryiii wants to merge 5 commits into
boostorg:developfrom
henryiii:perf/branchless-optional-index

Conversation

@henryiii

@henryiii henryiii commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Build on #452; the first commit is that PR. This fixes the case when you disable underflow or overflow bins; before this PR they are much slower (and is a pretty common case in boost-histogram, as this includes the the "numpy" axis type).

🤖 AI text below 🤖

Axes without underflow or overflow bins use optional_index, and on data that is partly out of range the validity checks mispredict. Making only linearize select-based did not help, because clang turns the selects back into branches; the disassembly of the fill loop showed three mispredicting branches per element. This PR:

  • detail/linearize.hpp: the optional_index overload computes validity with non-short-circuit boolean ops and applies an all-ones mask instead of branching. All-ones is also invalid_index, so an invalid input stays invalid.
  • detail/fill_n.hpp: after the index pass, a branch-free compaction moves the valid indices to the front, so the storage loop has no data-dependent branch. This runs only when no weight or sample arrays are passed and the index type is optional_index.
  • axis/regular.hpp: the "upper edge of the last bin is inclusive" select becomes a subtraction, since clang also turned that select into a branch.

Measured with a 1D regular<..., option::none_t> axis of 100 bins, dense_storage<int64_t>, 1M normally distributed doubles on [-1, 1] (32% out of range), ns/element (Apple M5 Pro, aarch64):

develop this PR
clang, span argument 4.8 1.2
clang, variant<double, span> argument 4.8 1.2
clang, all data in range 1.0 1.3
gcc 14, default flags 4.9 3.5
gcc 14, -fno-trapping-math 4.9 1.3

The all-in-range case pays about 0.2 ns/element for the compaction pass. The gcc default-flags row is limited by the trapping-math issue described in #452. Fills with weights or samples keep the branch in the storage loop and only get the linearize improvement; carrying positions through the compaction would extend it to them.

Random data mispredicts the range checks in the hot loop. Compute the
comparisons first and select the result, with the value clamped so the
conversion to int is in range for any input, including NaN. The result
is identical to the previous code for all inputs.

1D regular axis, 1M normal doubles, ns/element:
clang, variant argument 3.4 -> 1.1; gcc 14 default flags 3.6 -> 2.2.

Assisted-by: ClaudeCode:claude-fable-5-1
@henryiii henryiii changed the title perf: branch-free fill for axes without flow bins perf(fill): branch-free fill for axes without flow bins Nov 2, 2026
Replaces the nested ternary clamp with fmax(fmin(z, 1), 0). This maps
to min/max instructions on clang and on gcc with -fno-trapping-math,
and halves most of the remaining regressions vs develop.

Assisted-by: ClaudeCode:claude-fable-5-1
The 128 kB buffer spills out of L1 on recent CPUs. With 32 kB, bulk
fills where every value lands in the same bin run 2x faster on Apple M5
with clang and gcc; other cases are unchanged. Stack usage drops as well.

Assisted-by: ClaudeCode:claude-fable-5-1
Axes without underflow or overflow bins use optional_index. The validity
checks in linearize and in the storage loop mispredict on data that is
partly out of range. Replace the checks with mask arithmetic, and move
the valid indices to the front before the storage loop when no weight
or sample arrays are passed.

1D regular axis without flow bins, 1M normal doubles, ns/element:
clang 4.8 -> 1.2; gcc 14 with -fno-trapping-math 4.9 -> 1.3.

Assisted-by: ClaudeCode:claude-fable-5-1
The single-value fill path keeps the branching version. Its checks are
predictable there, and the mask version cost 10 to 30 percent on gcc for
axes without flow bins.

Assisted-by: ClaudeCode:claude-fable-5-1
@henryiii
henryiii force-pushed the perf/branchless-optional-index branch from be4b4f2 to 3d36eec Compare November 3, 2026 16:26
@henryiii henryiii changed the title perf(fill): branch-free fill for axes without flow bins [STACKED] perf(fill): branch-free fill for axes without flow bins Nov 3, 2026

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