Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
vectorized_for.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <array>
7#include <cstddef>
8#include <type_traits>
9
10namespace bb {
11
12// Compile-time trait: is a SIMD VectorField<Fr::Params> operator* body actually compiled on this
13// target? Delegates to `simd_available_v` (vector_field.hpp), which is target-aware — false on a
14// non-SIMD build and false for any Fr without a body (e.g. stdlib::field_t today). Because it is
15// target-aware, callers branch on it with a plain `if constexpr` and need no `#if __wasm_simd128__`.
16template <typename Fr> inline constexpr bool simd_supported_v = simd_available_v<typename Fr::Params>;
17
18// Number of field elements per ContiguousVectorIndex<N> / VectorIndex<N>
19// token, equal to VectorField's q1s1 lane count (4 SIMD lanes + 1 scalar
20// field on the integer pipe). Naming the constant lets call sites be
21// explicit about *why* the literal 5 appears, but it does NOT by itself
22// make the surrounding code width-agnostic — the actual width is baked
23// into:
24// * VectorField::gather / scatter signatures (std::array<size_t, 5>)
25// * VectorField's load/store-array helpers (std::array<Field, 5>)
26// * VectorWriteProxyT::idx in polynomial.hpp
27// * VectorField's q1s1 Mont-mul kernel itself
28// Changing the value requires touching all of the above; do not assume
29// the templates carry it through transparently. The vectorized_for /
30// vectorized_for_if loop functions and the operator[] dispatch on tokens
31// are the only pieces that are genuinely parameterized on N today.
32inline constexpr size_t VECTOR_FIELD_WIDTH = 5;
33
35 size_t i;
36};
37
38template <size_t N> struct VectorIndex {
39 std::array<size_t, N> idx;
40};
41
42// ContiguousVectorIndex<N> marks an N-wide block of consecutive indices
43// [base, base+N). The bulk path of vectorized_for<N> emits this so kernels
44// can route through Polynomial's contiguous-load overloads, which fuse the
45// N scalar loads into raw SIMD loads of the underlying Fr limb bytes.
46//
47// vectorized_for_if<N> still emits VectorIndex<N> because its lane indices
48// are not consecutive.
49template <size_t N> struct ContiguousVectorIndex {
50 size_t base;
51};
52
53constexpr ScalarIndex shift(ScalarIndex ctx, size_t d)
54{
55 return ScalarIndex{ ctx.i + d };
56}
57
58template <size_t N> constexpr VectorIndex<N> shift(VectorIndex<N> ctx, size_t d)
59{
61 for (size_t k = 0; k < N; ++k) {
62 out.idx[k] = ctx.idx[k] + d;
63 }
64 return out;
65}
66
67template <size_t N> constexpr ContiguousVectorIndex<N> shift(ContiguousVectorIndex<N> ctx, size_t d)
68{
69 return ContiguousVectorIndex<N>{ ctx.base + d };
70}
71
72// Design note (native vs WASM):
73//
74// The point of emitting ContiguousVectorIndex<N> tokens in the bulk is to
75// route the kernel through Polynomial's vector operator[], which on WASM
76// resolves to VectorField's q1s1 SIMD primitives (the win).
77//
78// On native, VectorField has no SIMD — its fallback stores `Field elts[N]`
79// and loops over `elts` scalar-by-scalar inside every operator. The bulk
80// path therefore costs (i) loading N Fr's into the struct, (ii) doing N
81// scalar ops on the struct, (iii) writing N Fr's back. For cheap ops
82// (+, -) this round-trip costs more than the work it saves; we measured a
83// ~13% slowdown on `+=`/`-=` at 2^16 elements vs a plain scalar loop.
84//
85// So on native, we degenerate to plain scalar: every iteration emits a
86// ScalarIndex. The kernel still compiles uniformly (its generic lambda
87// just gets one fewer instantiation on this target). This recovers full
88// scalar-loop performance on native without forcing kernels to grow
89// `if constexpr (is_wasm)` branches at every call site.
90//
91// Perf cliff to be aware of: this only degrades the IMPLICIT path through
92// `vectorized_for<N>`. Direct user code that mints
93// `ContiguousVectorIndex<N>{...}` tokens by hand still hits VectorField's
94// native fallback and pays the round-trip. That's intentional — opting
95// into the SIMD shape explicitly is a "you know what you're doing" signal,
96// and we'd rather keep the abstraction surface honest than silently
97// rewrite explicit token use under the hood.
98template <size_t N, typename Fr, typename K>
99[[gnu::always_inline]] inline void vectorized_for(size_t start, size_t end, K&& kernel)
100{
101 // simd_supported_v<Fr> is target-aware, so on native (and for any Fr without a SIMD body) it is
102 // false and the packed branch below is discarded by `if constexpr` — no `#if __wasm_simd128__`.
103 if constexpr (simd_supported_v<Fr>) {
104 size_t i = start;
105 // Bulk: emit ContiguousVectorIndex<N> so the kernel routes through the
106 // fast contiguous-load path.
107 //
108 // The kernel call sites are tagged with `BB_INLINE_STMT` (see compiler_hints.hpp)
109 // so that under -Oz the generic lambda's `operator()` is inlined into the bulk
110 // loop instead of being emitted as a standalone WASM function and called once per
111 // N-wide block. This keeps the kernel body resident in the caller's TurboFan
112 // compilation unit so V8 can register-allocate the SIMD lanes across blocks.
113 while (i + N <= end) {
115 i += N;
116 }
117 // Tail
118 while (i < end) {
119 BB_INLINE_STMT kernel(ScalarIndex{ i });
120 ++i;
121 }
122 } else {
123 // Scalar fallback: native (avoids the round-trip through the VectorField scalar fallback), or an
124 // Fr without a SIMD body (e.g. bb::fq under ECCVM/Translator) so the kernel never instantiates
125 // against an undefined VectorField<Params>::operator*.
126 for (size_t i = start; i < end; ++i) {
127 BB_INLINE_STMT kernel(ScalarIndex{ i });
128 }
129 }
130}
131
132// Sparse variant of vectorized_for. Walks [start, end), invokes `predicate(i)` for each i,
133// and gathers indices that pass into a VectorIndex<N> buffer; once full, dispatches one
134// kernel call with the gather token. Leftover indices at the end run scalar-by-scalar.
135//
136// Same V8/TurboFan perf cliff applies as in vectorized_for above: both the predicate lambda
137// and the kernel lambda need to inline through this template. The bulk call site is tagged
138// here only for the kernel — the predicate body should be inline-friendly by construction
139// (a small function-of-i, not a polymorphic dispatcher).
140//
141// VectorIndex<N> routes through VectorField::gather (random-access scalar reads), so the
142// per-bulk savings come from amortizing kernel call overhead, not from contiguous-SIMD
143// loads. For dense ranges where every i passes the predicate, prefer vectorized_for.
144template <size_t N, typename Fr, typename P, typename K>
145void vectorized_for_if(size_t start, size_t end, P&& predicate, K&& kernel)
146{
147 // Target-aware via simd_supported_v (see vectorized_for): the gather path is discarded by
148 // `if constexpr` on native and for any Fr without a SIMD body — no `#if __wasm_simd128__` needed.
149 if constexpr (simd_supported_v<Fr>) {
150 VectorIndex<N> buf{};
151 size_t count = 0;
152 for (size_t i = start; i < end; ++i) {
153 if (predicate(i)) {
154 buf.idx[count++] = i;
155 if (count == N) {
156 kernel(buf);
157 count = 0;
158 }
159 }
160 }
161 // Drain leftovers scalar-by-scalar
162 for (size_t k = 0; k < count; ++k) {
163 kernel(ScalarIndex{ buf.idx[k] });
164 }
165 } else {
166 // Scalar fallback: native (VectorIndex<N> would otherwise round-trip through VectorField::gather)
167 // or an Fr without a SIMD body.
168 for (size_t i = start; i < end; ++i) {
169 if (predicate(i)) {
170 kernel(ScalarIndex{ i });
171 }
172 }
173 }
174}
175
176} // namespace bb
constexpr size_t N
#define BB_INLINE_STMT
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
void vectorized_for(size_t start, size_t end, K &&kernel)
constexpr size_t VECTOR_FIELD_WIDTH
constexpr bool simd_supported_v
constexpr ScalarIndex shift(ScalarIndex ctx, size_t d)
void vectorized_for_if(size_t start, size_t end, P &&predicate, K &&kernel)
bb::VectorAffineElementPushSpan< BaseParams > out
std::array< size_t, N > idx