Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ fully specialized, no type erasure — crossed with runtime parameter sweeps.

```cpp
#include <bench/all>
#include <tuple>

int main() {
bench::arg_x record_size{10'000, 100'000, 1'000'000};

bench::throughput(
bench::types<int, double, float>{}, // compile-time type axis
bench::throughput<std::tuple<int, double, float>>( // compile-time type axis
bench::name{"write"}, record_size, // named args, any order
bench::warmup{3}, bench::sample{10},
[&]<class T>(std::size_t idx, std::size_t n) -> double {
Expand All @@ -35,10 +35,11 @@ runtime and throughput.
the bytes it moved; the engine reports MiB/s. Order-independent named args:
`bench::name`, `bench::arg_x` (size sweep), `bench::warmup`, `bench::sample`,
and the untimed/timed hooks `bench::before_sample` / `bench::after_sample`.
- **Type-axis dispatch** — the headline feature. `bench::throughput(`
`bench::types<Ts...>{}, ...)` folds the *same* generic-lambda body over a
compile-time type list, each fully specialized, yielding one row per
`(type, size)` (rows named `"<name>/<typelabel>"`).
- **Type-axis dispatch** — the headline feature. `bench::throughput<`
`std::tuple<Ts...>>(...)` folds the *same* generic-lambda body over a
compile-time type list passed as an explicit template argument, each fully
specialized, yielding one row per `(type, size)` (rows named
`"<name>/<typelabel>"`).
- **Sinks** — results flow through a pluggable `bench::sink`. The default
`bench::stdout_sink` prints a table; opt-in `bench::csv_sink` and
`bench::json_sink` (in `<bench/sink/csv.hpp>` / `<bench/sink/json.hpp>`, not
Expand All @@ -57,7 +58,7 @@ default (toggle with `-DBENCH_BUILD_EXAMPLES=OFF`):
| Example | Shows |
| --- | --- |
| `memcpy_bandwidth.cpp` | scalar `throughput` over an `arg_x` size sweep; default stdout table |
| `type_dispatch.cpp` | `bench::types<...>` running one body across several element types, fed by `get_test_data<T>` |
| `type_dispatch.cpp` | `bench::throughput<std::tuple<...>>` running one body across several element types, fed by `get_test_data<T>` |
| `sinks.cpp` | emitting results to CSV and JSON via `csv_sink` / `json_sink` and `set_sink(...)` |

```sh
Expand Down
4 changes: 2 additions & 2 deletions examples/heatmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <vector>
#include <cstddef>
#include <memory>
#include <tuple>

int main(){
// colour each (type, size) cell by mean throughput; write report.svg.
Expand All @@ -22,8 +23,7 @@ int main(){

std::vector<unsigned char> buf(1'000'000 * sizeof(double), 1);

bench::throughput(
bench::types<int, float, double>{},
bench::throughput<std::tuple<int, float, double>>(
bench::name{"sum"},
sizes,
bench::warmup{2}, bench::sample{10},
Expand Down
54 changes: 22 additions & 32 deletions examples/memcpy_bandwidth.cpp
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
* MIT License — see LICENSE
*
* memcpy_bandwidth — the simplest possible bench: a memory-bandwidth sweep.
* memcpy_bandwidth — a type-aware memory-bandwidth sweep.
*
* Uses the *scalar* throughput driver. We sweep a set of buffer sizes via
* `bench::arg_x{...}` and, for each size, copy a source buffer into a
* destination buffer with std::memcpy. The body returns the number of bytes
* touched (read + written), which the engine divides by the measured time to
* report MiB/s. Output goes to the default stdout table sink.
* Uses the *type-axis* throughput driver: the same body is folded over a
* compile-time tuple of element widths, with a typed source/destination buffer
* per type. We sweep a set of buffer sizes via `bench::arg_x{...}` and, for each
* (type, size) point, copy a source buffer into a destination buffer with
* std::memcpy. The body returns the number of bytes touched (read + written),
* which the engine divides by the measured time to report MiB/s. Output goes to
* the default stdout table sink.
*
* Dependency-free: only the bench core + standard library.
*/
#include <bench/all>

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <tuple>
#include <vector>

int main() {
// x-axis: buffer sizes in bytes, small -> large (crosses cache levels).
bench::arg_x sizes{ 4 * 1024, 64 * 1024, 1 * 1024 * 1024, 16 * 1024 * 1024 };

// One source/destination pair, sized to the largest sweep point so the
// body never reallocates inside the timed region.
std::vector<char> src(16 * 1024 * 1024, 'x');
std::vector<char> dst(src.size());

bench::throughput(
bench::name{"memcpy"},
sizes,
bench::warmup{5}, bench::sample{50},
// idx = sweep index, n = the size in bytes for this point.
[&](std::size_t /*idx*/, std::size_t n) -> double {
std::memcpy(dst.data(), src.data(), n);
// Defeat dead-store elimination by reading one byte back.
volatile char sink = dst[n - 1];
(void)sink;
// Bytes moved across the bus: n read + n written.
return static_cast<double>(2 * n);
});

// Results print as a table when the global store flushes at program exit.
return 0;
using types = std::tuple<uint8_t, uint16_t, uint32_t, uint64_t>;
constexpr std::size_t max_bytes = 16 * 1024 * 1024;
bench::arg_x sizes{ 4*1024, 64*1024, 1*1024*1024, max_bytes };
bench::throughput<types>(
bench::name{"memcpy"}, sizes, bench::warmup{5}, bench::sample{50},
[&]<class T>(std::size_t /*idx*/, std::size_t n) -> double {
static std::vector<T> src(max_bytes/sizeof(T), T{1}), dst(src.size());
std::memcpy(dst.data(), src.data(), n);
volatile T sink = dst[n/sizeof(T) - 1]; (void)sink;
return static_cast<double>(2 * n);
});
return 0;
}
6 changes: 3 additions & 3 deletions examples/type_dispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@

#include <cstddef>
#include <cstdint>
#include <tuple>
#include <vector>

int main() {
// Element counts (NOT bytes) — the body multiplies by sizeof(T) itself,
// so each type reports the bytes it actually moves.
bench::arg_x counts{ 1'000, 100'000, 1'000'000 };

bench::throughput(
// the compile-time type axis: one row-group per type
bench::types<std::uint8_t, std::uint32_t, double>{},
// the compile-time type axis: one row-group per type
bench::throughput<std::tuple<std::uint8_t, std::uint32_t, double>>(
bench::name{"sum"},
counts,
bench::warmup{3}, bench::sample{20},
Expand Down
39 changes: 26 additions & 13 deletions include/bench/bench.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,22 @@ namespace bench::impl {
}) / static_cast<T>(data.size());
return std::pair<T,T>{mean, std::sqrt(var)};
}

// Detects whether `T` is a specialization of the class template `Tmpl`.
// Used to constrain the type-axis throughput overload to `std::tuple<...>`.
template <class T, template <class...> class Tmpl>
struct is_specialization_of : std::false_type {};
template <template <class...> class Tmpl, class... Us>
struct is_specialization_of<Tmpl<Us...>, Tmpl> : std::true_type {};

template <class T>
concept is_tuple = is_specialization_of<std::remove_cvref_t<T>, std::tuple>::value;
}

namespace bench {
constexpr std::size_t max_dims = 32;
namespace arg = bench::meta::arg;

// ---- compile-time type axis -------------------------------------------
// Marker carrying the list of types to fold the benchmark body over.
template <class... Ts> struct types {};

// ---- named, order-independent arguments -------------------------------
using name = impl::value_t<std::string, impl::tag::name_t>;
using warmup = impl::value_t<std::uint16_t, impl::tag::warmup_t>;
Expand Down Expand Up @@ -250,8 +256,8 @@ namespace bench {

// ---- the throughput driver --------------------------------------------
// body signature: double(std::size_t idx, std::size_t n) -> bytes moved.
// (The compile-time type-axis overload `throughput(types<Ts...>{}, ...)`
// is added by a separate lane.)
// The compile-time type-axis form is the explicit-tuple overload below
// (`throughput<std::tuple<Ts...>>(...)`).
template <class... args_t>
void throughput(args_t... args) {
using callback_t = std::function<double(std::size_t, std::size_t)>;
Expand Down Expand Up @@ -286,14 +292,22 @@ namespace bench {
}

// ---- the type-axis throughput driver ----------------------------------
// Folds the benchmark body over the compile-time typelist `types<Ts...>`.
// Folds the benchmark body over the compile-time typelist supplied as an
// explicit template argument, a `std::tuple<Ts...>`. Invoke as
// bench::throughput<std::tuple<A,B,C>>(bench::name{...}, ...);
// The leading template parameter `Tuple` is constrained (impl::is_tuple) to
// be a `std::tuple` specialization so this never collides with the scalar
// `throughput(args...)` overload — ordinary calls deduce nothing for `Tuple`
// and select the scalar form, while an explicit tuple selects this one.
//
// `body` is a C++20 generic lambda invoked once per type as
// body.template operator()<T>(std::size_t idx, std::size_t n) -> double
// returning bytes moved (same contract as the scalar driver). Honours the
// same named args/hooks; one result row per (type, x-point), the row name
// suffixed with the type label so types are distinguishable.
template <class... Ts, class... args_t>
void throughput(types<Ts...>, args_t... args) {
template <class Tuple, class... args_t>
requires impl::is_tuple<Tuple>
void throughput(args_t... args) {
using name_t = typename arg::tpos<impl::tag::name_t, args_t...>;
using x_t = typename arg::tpos<impl::tag::x_t, args_t...>;
using warmup_t = typename arg::tpos<impl::tag::warmup_t, args_t...>;
Expand All @@ -303,7 +317,7 @@ namespace bench {

static_assert( name_t::present, "benchmark name must be specified (bench::name{...})" );
static_assert( x_t::present, "x axis must be specified (bench::arg_x{...})" );
static_assert( sizeof...(Ts) > 0, "type axis must list at least one type" );
static_assert( std::tuple_size_v<Tuple> > 0, "type axis must list at least one type" );

auto tuple = std::forward_as_tuple(args...);
store_t& store = store_t::get();
Expand All @@ -320,9 +334,8 @@ namespace bench {
// the generic body is the only positional (un-tagged) argument
auto&& body = arg::getn<sizeof...(args_t) - 1>(args...);

using list_t = std::tuple<Ts...>;
meta::impl::static_for<list_t>([&](auto I){
using T = std::tuple_element_t<decltype(I)::value, list_t>;
meta::impl::static_for<Tuple>([&](auto I){
using T = std::tuple_element_t<decltype(I)::value, Tuple>;
std::string label = impl::type_name<T>();
if (label.empty()) label = "T" + std::to_string(decltype(I)::value);
const std::string row_name = nm.value + "/" + label;
Expand Down
7 changes: 3 additions & 4 deletions test/type_dispatch.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
* MIT License — see LICENSE
*
* Type-axis test: the compile-time `throughput(types<Ts...>{}, ...)` overload
* Type-axis test: the compile-time `throughput<std::tuple<Ts...>>(...)` overload
* folds a generic benchmark body over a typelist and emits one result row per
* (type, x-point), with the type label distinguishing the rows.
*/
Expand All @@ -21,9 +21,8 @@ int main() {

std::vector<unsigned char> buf(100'000 * sizeof(double), 1);

// args intentionally out of order; types<...> is the leading positional arg.
bench::throughput(
bench::types<int, double, float>{},
// args intentionally out of order; the type axis is an explicit tuple arg.
bench::throughput<std::tuple<int, double, float>>(
bench::sample{su}, sizes, bench::warmup{wu},
bench::before_sample{ [&]{ ++before_calls; } },
bench::after_sample{ [&]{ ++after_calls; } },
Expand Down
Loading