From bb8b091046600489afd3d885ac7bdda14e6b3018 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 23 Jul 2026 14:50:49 -0400 Subject: [PATCH 1/4] perf: hoist loop-invariant division out of reduce's cell loop Precompute the reduced axis extent once per axis instead of dividing per cell per axis, and skip the merge division when merge == 1. About 5-10% faster on large histograms (Apple clang -O3, M-series). Assisted-by: ClaudeCode:claude-fable-5 --- include/boost/histogram/algorithm/reduce.hpp | 15 ++++++++------- include/boost/histogram/detail/reduce_command.hpp | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/include/boost/histogram/algorithm/reduce.hpp b/include/boost/histogram/algorithm/reduce.hpp index c342d6aa..8d007529 100644 --- a/include/boost/histogram/algorithm/reduce.hpp +++ b/include/boost/histogram/algorithm/reduce.hpp @@ -405,6 +405,9 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { auto result = Histogram(std::move(axes), detail::make_default(unsafe_access::storage(hist))); + for (auto& o : opts) + o.reduced_end = (o.end.index - o.begin.index) / static_cast(o.merge); + auto idx = detail::make_stack_buffer(unsafe_access::axes(result)); for (auto&& x : indexed(hist, coverage::all)) { auto i = idx.begin(); @@ -417,14 +420,12 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { *i = -1; if (!o->use_underflow_bin) skip = true; } else { - if (*i >= 0) + if (*i < 0) + *i = o->reduced_end; + else if (o->merge > 1) *i /= static_cast(o->merge); - else - *i = o->end.index; - const auto reduced_axis_end = - (o->end.index - o->begin.index) / static_cast(o->merge); - if (*i >= reduced_axis_end) { - *i = reduced_axis_end; + if (*i >= o->reduced_end) { + *i = o->reduced_end; if (!o->use_overflow_bin) skip = true; } } diff --git a/include/boost/histogram/detail/reduce_command.hpp b/include/boost/histogram/detail/reduce_command.hpp index 3fb67b50..037b28f1 100644 --- a/include/boost/histogram/detail/reduce_command.hpp +++ b/include/boost/histogram/detail/reduce_command.hpp @@ -33,6 +33,7 @@ struct reduce_command { unsigned merge = 0; // default value indicates unset option bool crop = false; // for internal use by the reduce algorithm + axis::index_type reduced_end = 0; // (end - begin) / merge bool is_ordered = true; bool use_underflow_bin = true; bool use_overflow_bin = true; From a264d5fb1c96ef887a08b91bd09161b9dba29197 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 3 Aug 2026 13:45:19 -0400 Subject: [PATCH 2/4] perf: drop merge>1 branch, always divide by merge Benchmarks show the branch costs more than the division it skips: with the default unlimited_storage, shrink-only reduce was 5-9% slower than develop. Always dividing keeps the win from hoisting reduced_end and is never slower than develop in any measured case. Assisted-by: ClaudeCode:claude-fable-5 --- include/boost/histogram/algorithm/reduce.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/boost/histogram/algorithm/reduce.hpp b/include/boost/histogram/algorithm/reduce.hpp index 8d007529..bdeb977b 100644 --- a/include/boost/histogram/algorithm/reduce.hpp +++ b/include/boost/histogram/algorithm/reduce.hpp @@ -420,10 +420,10 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { *i = -1; if (!o->use_underflow_bin) skip = true; } else { - if (*i < 0) - *i = o->reduced_end; - else if (o->merge > 1) + if (*i >= 0) *i /= static_cast(o->merge); + else + *i = o->reduced_end; if (*i >= o->reduced_end) { *i = o->reduced_end; if (!o->use_overflow_bin) skip = true; From 6bc101b2222b6b6ee0b93dcea05742a2367de935 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 5 Aug 2026 12:27:12 -0400 Subject: [PATCH 3/4] bench: add reduce benchmark Covers rebin, shrink, and shrink_and_rebin for 1d-3d histograms with dense_storage and unlimited_storage, 4-256 bins per axis. Assisted-by: ClaudeCode:claude-fable-5 --- benchmark/CMakeLists.txt | 1 + benchmark/histogram_reduce.cpp | 112 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 benchmark/histogram_reduce.cpp diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 656e6f08..8c5cb3ab 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -38,6 +38,7 @@ add_benchmark(axis_size) add_benchmark(axis_index) add_benchmark(histogram_filling) add_benchmark(histogram_iteration) +add_benchmark(histogram_reduce) add_benchmark(detail_normal) find_package(Threads) diff --git a/benchmark/histogram_reduce.cpp b/benchmark/histogram_reduce.cpp new file mode 100644 index 00000000..5302cf9e --- /dev/null +++ b/benchmark/histogram_reduce.cpp @@ -0,0 +1,112 @@ +// Copyright 2026 Henry Schreiner +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../test/throw_exception.hpp" + +#include +struct assert_check { + assert_check() { + assert(false); // don't run with asserts enabled + } +} _; + +using namespace boost::histogram; + +struct dense {}; +struct unlimited {}; + +template +using Dim_t = boost::mp11::mp_int; + +auto make_storage(dense) { return dense_storage(); } +auto make_storage(unlimited) { return unlimited_storage<>(); } + +template +auto make_histogram(Tag, boost::mp11::mp_int, int n) { + std::vector> axes; + for (int d = 0; d < Dim; ++d) axes.emplace_back(n, 0.0, 1.0); + auto h = make_histogram_with(make_storage(Tag{}), std::move(axes)); + // cell values do not affect reduce speed, they only must be non-trivial + std::default_random_engine gen(1); + std::uniform_real_distribution dis(0, 10); + for (auto&& v : unsafe_access::storage(h)) v = dis(gen); + return h; +} + +template +void run_reduce(benchmark::State& state, const Histogram& h, + const std::vector& opts) { + for (auto _ : state) { + auto r = algorithm::reduce(h, opts); + benchmark::DoNotOptimize(r); + } + // report throughput in input cells (including flow bins) per second + state.SetItemsProcessed(state.iterations() * static_cast(h.size())); +} + +template +static void Rebin(benchmark::State& state, Tag, boost::mp11::mp_int d) { + auto h = make_histogram(Tag{}, d, static_cast(state.range(0))); + std::vector opts; + for (int i = 0; i < Dim; ++i) opts.push_back(algorithm::rebin(i, 2)); + run_reduce(state, h, opts); +} + +template +static void Shrink(benchmark::State& state, Tag, boost::mp11::mp_int d) { + auto h = make_histogram(Tag{}, d, static_cast(state.range(0))); + std::vector opts; + for (int i = 0; i < Dim; ++i) opts.push_back(algorithm::shrink(i, 0.2, 0.8)); + run_reduce(state, h, opts); +} + +template +static void ShrinkAndRebin(benchmark::State& state, Tag, boost::mp11::mp_int d) { + auto h = make_histogram(Tag{}, d, static_cast(state.range(0))); + std::vector opts; + for (int i = 0; i < Dim; ++i) + opts.push_back(algorithm::shrink_and_rebin(i, 0.2, 0.8, 2)); + run_reduce(state, h, opts); +} + +#define BENCH(Type, Tag, Dim) \ + BENCHMARK_CAPTURE(Type, (Tag, Dim), Tag{}, Dim_t{}) \ + ->RangeMultiplier(4) \ + ->Range(4, 256) + +BENCH(Rebin, dense, 1); +BENCH(Shrink, dense, 1); +BENCH(ShrinkAndRebin, dense, 1); + +BENCH(Rebin, dense, 2); +BENCH(Shrink, dense, 2); +BENCH(ShrinkAndRebin, dense, 2); + +BENCH(Rebin, dense, 3); +BENCH(Shrink, dense, 3); +BENCH(ShrinkAndRebin, dense, 3); + +BENCH(Rebin, unlimited, 1); +BENCH(Shrink, unlimited, 1); +BENCH(ShrinkAndRebin, unlimited, 1); + +BENCH(Rebin, unlimited, 2); +BENCH(Shrink, unlimited, 2); +BENCH(ShrinkAndRebin, unlimited, 2); + +BENCH(Rebin, unlimited, 3); +BENCH(Shrink, unlimited, 3); +BENCH(ShrinkAndRebin, unlimited, 3); From c5a941ce6b0891c536889a2777fb7a922af9e122 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 5 Aug 2026 14:16:19 -0400 Subject: [PATCH 4/4] perf: compute reduced_end during axes_transform Removes the extra per-axis pass before the cell loop; the values are now set in the axes_transform lambda, which already visits every axis. Assisted-by: ClaudeCode:claude-fable-5 --- benchmark/histogram_reduce.cpp | 4 ++-- include/boost/histogram/algorithm/reduce.hpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/benchmark/histogram_reduce.cpp b/benchmark/histogram_reduce.cpp index 5302cf9e..12e8c97b 100644 --- a/benchmark/histogram_reduce.cpp +++ b/benchmark/histogram_reduce.cpp @@ -82,9 +82,9 @@ static void ShrinkAndRebin(benchmark::State& state, Tag, boost::mp11::mp_int{}) \ - ->RangeMultiplier(4) \ + ->RangeMultiplier(4) \ ->Range(4, 256) BENCH(Rebin, dense, 1); diff --git a/include/boost/histogram/algorithm/reduce.hpp b/include/boost/histogram/algorithm/reduce.hpp index bdeb977b..7758c85c 100644 --- a/include/boost/histogram/algorithm/reduce.hpp +++ b/include/boost/histogram/algorithm/reduce.hpp @@ -382,6 +382,8 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { // example [1, 4] with merge = 2 is reduced to [1, 3] o.end.index -= (o.end.index - o.begin.index) % static_cast(o.merge); + o.reduced_end = + (o.end.index - o.begin.index) / static_cast(o.merge); using A = std::decay_t; return A(a_in, o.begin.index, o.end.index, o.merge); }, @@ -398,6 +400,7 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { o.merge = 1; o.begin.index = 0; o.end.index = a_in.size(); + o.reduced_end = a_in.size(); return a_in; } }); @@ -405,9 +408,6 @@ Histogram reduce(const Histogram& hist, const Iterable& options) { auto result = Histogram(std::move(axes), detail::make_default(unsafe_access::storage(hist))); - for (auto& o : opts) - o.reduced_end = (o.end.index - o.begin.index) / static_cast(o.merge); - auto idx = detail::make_stack_buffer(unsafe_access::axes(result)); for (auto&& x : indexed(hist, coverage::all)) { auto i = idx.begin();