Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
pippenger.bench.cpp
Go to the documentation of this file.
1
13
14#include <benchmark/benchmark.h>
15
17
18#include <cstdint>
19#include <limits>
20#include <vector>
21
22using namespace benchmark;
23
27
28namespace {
29
30class PippengerBench : public benchmark::Fixture {
31 public:
32 static constexpr size_t MAX_POINTS = 1 << 21;
34 std::vector<Fr> scalars;
36
37 void SetUp([[maybe_unused]] const ::benchmark::State& state) override
38 {
40 srs = bb::srs::get_crs_factory<Curve>()->get_crs(MAX_POINTS);
41
42 scalars.resize(MAX_POINTS);
43 for (auto& x : scalars) {
44 x = Fr::random_element(&engine);
45 }
46 }
47};
48
49// ===================== Single MSM =====================
50
51BENCHMARK_DEFINE_F(PippengerBench, PippengerUnsafe)(benchmark::State& state)
52{
53 const size_t num_points = static_cast<size_t>(state.range(0));
54 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
55 std::span<Fr> span(&scalars[0], num_points);
56 bb::PolynomialSpan<Fr> poly_scalars(0, span);
57
58 for (auto _ : state) {
60 bb::scalar_multiplication::pippenger_unsafe<Curve>(poly_scalars, points);
61 }
62}
63
64// ===== Round-parallel MSM (A) — window-partitioned, independent of pippenger_unsafe =====
65
66BENCHMARK_DEFINE_F(PippengerBench, PippengerRoundParallel)(benchmark::State& state)
67{
68 const size_t num_threads = static_cast<size_t>(state.range(0));
69 const size_t num_points = static_cast<size_t>(state.range(1));
70 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
71 std::span<Fr> span(&scalars[0], num_points);
72 bb::PolynomialSpan<Fr> poly_scalars(0, span);
73
74 const size_t original_concurrency = bb::get_num_cpus();
76
77 for (auto _ : state) {
79 bb::scalar_multiplication::pippenger_round_parallel<Curve>(poly_scalars, points);
80 }
81
82 bb::set_parallel_for_concurrency(original_concurrency);
83}
84
85BENCHMARK_DEFINE_F(PippengerBench, PippengerUnsafeThreads)(benchmark::State& state)
86{
87 const size_t num_threads = static_cast<size_t>(state.range(0));
88 const size_t num_points = static_cast<size_t>(state.range(1));
89 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
90 std::span<Fr> span(&scalars[0], num_points);
91 bb::PolynomialSpan<Fr> poly_scalars(0, span);
92
93 const size_t original_concurrency = bb::get_num_cpus();
95
96 for (auto _ : state) {
98 bb::scalar_multiplication::pippenger_unsafe<Curve>(poly_scalars, points);
99 }
100
101 bb::set_parallel_for_concurrency(original_concurrency);
102}
103
104// ===== Per-stage scaling sweep (single MSM, native concurrency) =====
105// A single round-parallel MSM at the runtime's native concurrency (bb::get_num_cpus()
106// natively; HARDWARE_CONCURRENCY under wasmtime), swept over 2^15..2^19 — the MSM-size band
107// the pinned Chonk tx flows actually exercise (light flows peak ~2^16, storage_7 ~2^18).
108// 2^20/2^21 are an AVM/rollup workload and are intentionally not in this tx-flow sweep. Unlike
109// PippengerRoundParallel this fixes a single thread axis, so the per-stage BB_BENCH
110// breakdown is clean. Run with BB_BENCH=1 to dump the Stage1..Stage7 hierarchical timings
111// per size; comparing native vs wasmtime per-stage scaling isolates which stage fails to
112// scale natively. Under wasm the breakdown requires ENABLE_WASM_BENCH=ON at build time.
113BENCHMARK_DEFINE_F(PippengerBench, PippengerRoundParallelScaling)(benchmark::State& state)
114{
115 const size_t num_points = static_cast<size_t>(state.range(0));
116 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
117 std::span<Fr> span(&scalars[0], num_points);
118 bb::PolynomialSpan<Fr> poly_scalars(0, span);
119
120 for (auto _ : state) {
122 bb::scalar_multiplication::pippenger_round_parallel<Curve>(poly_scalars, points);
123 }
124}
125
126// Legacy counterpart of the scaling sweep: byte-identical merge-train MSM
127// (legacy::pippenger_unsafe) over the same points/sizes, with the legacy stage markers
128// (MSM::build_point_schedule / sort_point_schedule / batch_accumulate_into_buckets /
129// accumulate_buckets) so its BB_BENCH breakdown maps onto the rewrite's stages.
130BENCHMARK_DEFINE_F(PippengerBench, PippengerScalingLegacy)(benchmark::State& state)
131{
132 const size_t num_points = static_cast<size_t>(state.range(0));
133 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
134 std::span<Fr> span(&scalars[0], num_points);
135 bb::PolynomialSpan<Fr> poly_scalars(0, span);
136
137 for (auto _ : state) {
139 bb::scalar_multiplication::legacy::pippenger_unsafe<Curve>(poly_scalars, points);
140 }
141}
142
143// ===================== Batch MSM =====================
144
145BENCHMARK_DEFINE_F(PippengerBench, BatchMSM)(benchmark::State& state)
146{
147 const size_t num_polys = static_cast<size_t>(state.range(0));
148 const size_t poly_size = static_cast<size_t>(state.range(1));
149
150 std::vector<std::vector<Fr>> all_scalars(num_polys);
152 std::span<const G1> points = srs->get_monomial_points().subspan(0, poly_size);
153
154 for (size_t i = 0; i < num_polys; ++i) {
155 all_scalars[i].resize(poly_size);
156 for (auto& s : all_scalars[i]) {
158 }
159 scalar_spans.emplace_back(0, std::span<Fr>(all_scalars[i]));
160 }
161
162 for (auto _ : state) {
165 }
166}
167
168namespace {
169// Uniform [0,1) draw from the deterministic debug RNG (used by the sparsity bench).
170[[nodiscard]] double uniform01(bb::numeric::RNG& engine) noexcept
171{
172 return static_cast<double>(engine.get_random_uint32()) / static_cast<double>(std::numeric_limits<uint32_t>::max());
173}
174
175} // namespace
176
177// ===================== Sparsity-profile single MSM =====================
178//
179// Single-MSM pippenger_round_parallel across dyadic sizes 2^15..2^19 under two scalar
180// distributions, to A/B the thread-pool backend (new generation-counter pool vs the
181// merge-train pool) at the workload shapes that stress the round-parallel scaffolding
182// and the dedup pre-pass.
183//
184// Dense80 — 80% uniformly-random nonzero scalars, 20% zero. Exercises the main
185// bucket-accumulation pipeline with light sparsity. dedup_hint=false.
186// DupHeavy — 50% unique random, 25% all equal to one random scalar A, 5% all equal
187// to another random scalar B, 20% zero. Heavy duplication drives the
188// Phase A dedup pre-pass (the most thread-intensive stage), so this is
189// the case most sensitive to pool dispatch / oversubscription behavior.
190// dedup_hint=true.
191//
192// Scalars are drawn from the fixture's deterministic debug RNG so the A/B runs on the
193// two pool backends see identical inputs.
194namespace {
195enum class SparsityProfile : uint8_t { Dense80 = 0, DupHeavy = 1 };
196
197std::vector<Fr> build_sparsity_scalars(SparsityProfile profile, size_t n, bb::numeric::RNG& engine)
198{
199 std::vector<Fr> out(n);
200 if (profile == SparsityProfile::Dense80) {
201 for (size_t i = 0; i < n; ++i) {
202 out[i] = (uniform01(engine) < 0.20) ? Fr::zero() : Fr::random_element(&engine);
203 }
204 } else {
205 const Fr dup_a = Fr::random_element(&engine);
206 const Fr dup_b = Fr::random_element(&engine);
207 for (size_t i = 0; i < n; ++i) {
208 const double r = uniform01(engine);
209 if (r < 0.20) {
210 out[i] = Fr::zero(); // 20% zero
211 } else if (r < 0.45) {
212 out[i] = dup_a; // 25% duplicate of A
213 } else if (r < 0.50) {
214 out[i] = dup_b; // 5% duplicate of B
215 } else {
216 out[i] = Fr::random_element(&engine); // 50% unique random
217 }
218 }
219 }
220 return out;
221}
222} // namespace
223
224BENCHMARK_DEFINE_F(PippengerBench, PippengerSparsity)(benchmark::State& state)
225{
226 const auto profile = static_cast<SparsityProfile>(state.range(0));
227 const size_t num_points = static_cast<size_t>(state.range(1));
228 const bool dedup_hint = (profile == SparsityProfile::DupHeavy);
229 state.SetLabel(profile == SparsityProfile::Dense80 ? "Dense80" : "DupHeavy");
230
231 // Build the scalar set from a fresh RNG re-seeded deterministically per (profile, size)
232 // rather than from the shared advancing engine. Two reasons:
233 // 1. Every benchmark repetition (--benchmark_repetitions) reuses the SAME scalars, so the
234 // measured variance reflects pool/scheduler noise only, not input variation.
235 // 2. The input is independent of benchmark execution order, so a filtered subset or a
236 // pool-toggled A/B run sees byte-identical scalars — the comparison is properly paired.
237 // The scalar build is outside the timed `for (auto _ : state)` loop regardless, so RNG cost
238 // never enters the measurement.
239 const std::uint_fast64_t case_seed =
240 0xC0FFEEULL + (static_cast<std::uint_fast64_t>(profile) << 32) + static_cast<std::uint_fast64_t>(num_points);
241 bb::numeric::RNG& case_engine = bb::numeric::get_debug_randomness(/*reset=*/true, /*seed=*/case_seed);
242
243 std::vector<Fr> msm_scalars = build_sparsity_scalars(profile, num_points, case_engine);
244 std::span<const G1> points = srs->get_monomial_points().subspan(0, num_points);
245 bb::PolynomialSpan<const Fr> poly_scalars(0, std::span<const Fr>(msm_scalars.data(), num_points));
246
247 for (auto _ : state) {
249 (void)bb::scalar_multiplication::pippenger_round_parallel<Curve>(poly_scalars, points, dedup_hint);
250 }
251}
252
253// ===================== Registration =====================
254
255// Single MSM: 2^14 to 2^20
256BENCHMARK_REGISTER_F(PippengerBench, PippengerUnsafe)
257 ->Unit(benchmark::kMillisecond)
258 ->RangeMultiplier(4)
259 ->Range(1 << 14, 1 << 20);
260
261// Sparsity-profile single MSM: {profile (0=Dense80, 1=DupHeavy), size}, sizes 2^15..2^19.
262BENCHMARK_REGISTER_F(PippengerBench, PippengerSparsity)
263 ->Unit(benchmark::kMillisecond)
264 ->ArgsProduct({ { 0, 1 }, { 1 << 15, 1 << 16, 1 << 17, 1 << 18, 1 << 19 } });
265
266// Batch MSM: {num_polynomials, polynomial_size}
267// AVM-like: 32 polys of size 2^21 (one batch from ~2618 wire polys committed in batches of 32)
268BENCHMARK_REGISTER_F(PippengerBench, BatchMSM)
269 ->Unit(benchmark::kMillisecond)
270 ->Args({ 32, 1 << 19 })
271 ->Args({ 32, 1 << 21 });
272
273// Grid sweep for A vs B: {threads, size}. N covers 2^7..2^21 (with extra in-between
274// points around the GLV crossover).
275BENCHMARK_REGISTER_F(PippengerBench, PippengerRoundParallel)
276 ->Unit(benchmark::kMillisecond)
277 ->ArgsProduct({ { 1, 4, 8, 12, 16, 32, 64, 128 },
278 { 1 << 7,
279 1 << 8,
280 1 << 9,
281 1 << 10,
282 1 << 11,
283 1 << 12,
284 1 << 13,
285 1 << 14,
286 1 << 15,
287 3 << 14,
288 1 << 16,
289 3 << 15,
290 1 << 17,
291 3 << 16,
292 1 << 18,
293 1 << 19,
294 1 << 20,
295 1 << 21 } });
296
297// Focused per-stage scaling sweep at native concurrency. Run with BB_BENCH=1 for the
298// Stage1..Stage7 hierarchical breakdown; filter with --benchmark_filter=PippengerRoundParallelScaling.
299BENCHMARK_REGISTER_F(PippengerBench, PippengerRoundParallelScaling)
300 ->Unit(benchmark::kMillisecond)
301 ->Arg(1 << 15)
302 ->Arg(1 << 16)
303 ->Arg(1 << 17)
304 ->Arg(1 << 18)
305 ->Arg(1 << 19);
306
307BENCHMARK_REGISTER_F(PippengerBench, PippengerScalingLegacy)
308 ->Unit(benchmark::kMillisecond)
309 ->Arg(1 << 15)
310 ->Arg(1 << 16)
311 ->Arg(1 << 17)
312 ->Arg(1 << 18)
313 ->Arg(1 << 19);
314
315BENCHMARK_REGISTER_F(PippengerBench, PippengerUnsafeThreads)
316 ->Unit(benchmark::kMillisecond)
317 ->ArgsProduct({ { 1, 4, 8, 12, 16, 32, 64, 128 },
318 { 1 << 9,
319 1 << 10,
320 1 << 11,
321 1 << 12,
322 1 << 13,
323 1 << 14,
324 1 << 15,
325 1 << 16,
326 1 << 17,
327 1 << 18,
328 1 << 19,
329 1 << 20 } });
330
331} // namespace
332
typename Group::affine_element AffineElement
Definition bn254.hpp:22
bb::fr ScalarField
Definition bn254.hpp:18
static std::vector< AffineElement > batch_multi_scalar_mul(std::span< const AffineElement > points, std::span< PolynomialSpan< ScalarField > > scalars, bool handle_edge_cases=true, std::span< const uint32_t > dedup_infos={}) noexcept
numeric::RNG & engine
#define GOOGLE_BB_BENCH_REPORTER(state)
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
std::filesystem::path bb_crs_path()
void init_file_crs_factory(const std::filesystem::path &path)
size_t get_num_cpus()
Definition thread.cpp:34
void set_parallel_for_concurrency(size_t num_cores)
Definition thread.cpp:24
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
BENCHMARK_MAIN()
Curve::AffineElement G1
bb::VectorAffineElementPushSpan< BaseParams > out
static field random_element(numeric::RNG *engine=nullptr) noexcept
static constexpr field zero()