11#include " barretenberg/vm2/tracegen/trace_container.hpp"
22
33#include < algorithm>
4- #include < mutex >
4+ #include < ranges >
55
6+ #include " barretenberg/common/assert.hpp"
7+ #include " barretenberg/common/compiler_hints.hpp"
68#include " barretenberg/common/log.hpp"
79#include " barretenberg/common/ref_vector.hpp"
810#include " barretenberg/vm2/common/field.hpp"
@@ -12,7 +14,19 @@ namespace bb::avm2::tracegen {
1214namespace {
1315
1416// We need a zero value to return (a reference to) when a value is not found.
15- static const FF zero = FF ::zero();
17+ const FF zero = FF ::zero();
18+
19+ // Writes each 64-bit limb of the field with a relaxed atomic store. Each limb is naturally aligned (FF is
20+ // alignas(32), 4x uint64_t), so this lowers to 4 plain `movq` stores on x86-64 — no lock, no libatomic call
21+ // (unlike a whole-field std::atomic_ref<FF>, whose 32 bytes exceed the lock-free width). Lets set() make a
22+ // same-cell concurrent write data-race-free when every writer stores the same value.
23+ inline void store_per_limb (FF & cell, const FF & value)
24+ {
25+ static_assert (sizeof (FF ) == 4 * sizeof (uint64_t ));
26+ for (size_t i = 0 ; i < 4 ; ++i) {
27+ std::atomic_ref<uint64_t >(cell.data [i]).store (value.data [i], std::memory_order_relaxed);
28+ }
29+ }
1630
1731} // namespace
1832
@@ -23,9 +37,15 @@ TraceContainer::TraceContainer()
2337const FF & TraceContainer::get (Column col, uint32_t row) const
2438{
2539 auto & column_data = (*trace)[static_cast <size_t >(col)];
26- std::shared_lock lock (column_data.mutex );
27- const auto it = column_data.rows .find (row);
28- return it == column_data.rows .end () ? zero : it->second ;
40+ const size_t shard_idx = row / INTERVAL_SIZE ;
41+ if (shard_idx >= NUM_SHARDS ) {
42+ return zero;
43+ }
44+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
45+ if (shard == nullptr ) {
46+ return zero;
47+ }
48+ return shard->rows [row % INTERVAL_SIZE ];
2949}
3050
3151const FF & TraceContainer::get_column_or_shift (ColumnAndShifts col, uint32_t row) const
@@ -36,19 +56,52 @@ const FF& TraceContainer::get_column_or_shift(ColumnAndShifts col, uint32_t row)
3656 return get (static_cast <Column>(col), row);
3757}
3858
39- void TraceContainer::set (Column col, uint32_t row, const FF & value)
59+ TraceContainer::ColumnInterval& TraceContainer::get_or_create_shard (SparseColumn& column_data, size_t shard_idx)
60+ {
61+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
62+ if (shard != nullptr ) {
63+ return *shard;
64+ }
65+ // Slow path: this slot has never been written. Construct a shard and install it with a single CAS.
66+ // Creators of different shards target different atomics and run fully in parallel with no lock. If we
67+ // lose the race for this slot (only possible when two chunks share a shard at a boundary), we discard
68+ // our spare copy and use the winner's.
69+ auto fresh = std::make_unique<ColumnInterval>();
70+ ColumnInterval* expected = nullptr ;
71+ if (column_data.slots [shard_idx].compare_exchange_strong (
72+ expected, fresh.get (), std::memory_order_acq_rel, std::memory_order_acquire)) {
73+ return *fresh.release ();
74+ }
75+ return *expected; // CAS failure loaded the winning pointer into `expected` (acquire).
76+ }
77+
78+ void TraceContainer::set (Column col, uint32_t row, const FF & value, bool use_atomic_limbs)
4079{
4180 auto & column_data = (*trace)[static_cast <size_t >(col)];
42- std::unique_lock lock (column_data.mutex );
81+ const size_t shard_idx = row / INTERVAL_SIZE ;
82+ BB_ASSERT_LT (shard_idx, NUM_SHARDS , " row exceeds the maximum trace size" );
83+ const uint32_t offset = row % INTERVAL_SIZE ;
84+
4385 if (!value.is_zero ()) {
44- column_data.rows .insert_or_assign (row, value);
45- column_data.max_row_number = std::max (column_data.max_row_number , static_cast <int64_t >(row));
86+ // Lock-free: a single atomic load finds the shard (created on first write), then we write our
87+ // own dense cell directly. Different rows are distinct array elements, so concurrent writers of
88+ // this column (or even of the same shard, at a chunk boundary) never race and never serialize.
89+ auto & cell = get_or_create_shard (column_data, shard_idx).rows [offset];
90+ if (BB_UNLIKELY (use_atomic_limbs)) {
91+ store_per_limb (cell, value);
92+ } else {
93+ cell = value;
94+ }
4695 } else {
47- auto num_erased = column_data.rows .erase (row);
48- if (column_data.max_row_number == row && num_erased > 0 ) {
49- // This shouldn't happen often. We delay recalculation of the max row number
50- // until someone actually needs it.
51- column_data.row_number_dirty = true ;
96+ // Zero value: clear if present. We never create a shard, so sparse (mostly-zero) columns are not
97+ // materialized (an unset cell already reads as zero).
98+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
99+ if (shard != nullptr ) {
100+ if (BB_UNLIKELY (use_atomic_limbs)) {
101+ store_per_limb (shard->rows [offset], zero);
102+ } else {
103+ shard->rows [offset] = FF::zero ();
104+ }
52105 }
53106 }
54107}
@@ -62,24 +115,39 @@ void TraceContainer::set(uint32_t row, std::span<const std::pair<Column, FF>> va
62115
63116void TraceContainer::reserve_column (Column col, size_t size)
64117{
118+ if (size == 0 ) {
119+ return ;
120+ }
65121 auto & column_data = (*trace)[static_cast <size_t >(col)];
66- std::unique_lock lock (column_data.mutex );
67- column_data.rows .reserve (size);
122+ const size_t num_shards = std::min ((size + INTERVAL_SIZE - 1 ) / INTERVAL_SIZE , NUM_SHARDS );
123+ // Each shard's dense row array is full size on creation, so reserving just materializes the shards up
124+ // front (e.g. for precomputed columns). Lock-free: get_or_create_shard installs each via CAS.
125+ for (size_t k = 0 ; k < num_shards; ++k) {
126+ get_or_create_shard (column_data, k);
127+ }
68128}
69129
70130uint32_t TraceContainer::get_column_rows (Column col) const
71131{
132+ // The number of rows is (highest non-zero absolute row + 1). We find it by scanning shards from the
133+ // top down and, within the first non-empty shard, scanning its rows from the top. Shards are
134+ // top-dense, so this terminates almost immediately. This is only called after the parallel fill
135+ // phase, so no lock is needed. Lower shards cannot hold a higher row, so the first hit is the answer.
72136 auto & column_data = (*trace)[static_cast <size_t >(col)];
73- std::unique_lock lock (column_data.mutex );
74- if (column_data.row_number_dirty ) {
75- // Trigger recalculation of max row number.
76- auto keys = std::views::keys (column_data.rows );
77- const auto it = std::ranges::max_element (keys);
78- // We use -1 to indicate that the column is empty.
79- column_data.max_row_number = it == keys.end () ? -1 : static_cast <int64_t >(*it);
80- column_data.row_number_dirty = false ;
81- }
82- return static_cast <uint32_t >(column_data.max_row_number + 1 );
137+ for (size_t k = NUM_SHARDS ; k-- > 0 ;) {
138+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
139+ if (shard_ptr == nullptr ) {
140+ continue ;
141+ }
142+ const auto & rows = shard_ptr->rows ;
143+ const uint32_t base = static_cast <uint32_t >(k) * INTERVAL_SIZE ;
144+ for (uint32_t off = INTERVAL_SIZE ; off-- > 0 ;) {
145+ if (!rows[off].is_zero ()) {
146+ return base + off + 1 ;
147+ }
148+ }
149+ }
150+ return 0 ;
83151}
84152
85153uint32_t TraceContainer::get_num_witness_rows () const
@@ -103,9 +171,18 @@ uint32_t TraceContainer::get_num_rows() const
103171void TraceContainer::visit_column (Column col, const std::function<void (uint32_t , const FF &)>& visitor) const
104172{
105173 auto & column_data = (*trace)[static_cast <size_t >(col)];
106- std::shared_lock lock (column_data.mutex );
107- for (const auto & [row, value] : column_data.rows ) {
108- visitor (row, value);
174+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
175+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
176+ if (shard_ptr == nullptr ) {
177+ continue ;
178+ }
179+ auto & shard = *shard_ptr;
180+ const uint32_t base = static_cast <uint32_t >(k) * INTERVAL_SIZE ;
181+ for (uint32_t off = 0 ; off < INTERVAL_SIZE ; ++off) {
182+ if (!shard.rows [off].is_zero ()) {
183+ visitor (base + off, shard.rows [off]);
184+ }
185+ }
109186 }
110187}
111188
@@ -120,20 +197,28 @@ void TraceContainer::invert_column(Column col)
120197{
121198 RefVector<FF > ff_vector;
122199 auto & column_data = (*trace)[static_cast <size_t >(col)];
123- std::unique_lock lock (column_data.mutex );
124- for (auto & [row, value] : column_data.rows ) {
125- ff_vector.push_back (value);
200+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
201+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
202+ if (shard_ptr == nullptr ) {
203+ continue ;
204+ }
205+ auto & shard = *shard_ptr;
206+ for (auto & value : shard.rows ) {
207+ if (!value.is_zero ()) {
208+ ff_vector.push_back (value);
209+ }
210+ }
126211 }
127212 FF ::batch_invert<RefVector<FF >>(ff_vector);
128213}
129214
130215void TraceContainer::clear_column (Column col)
131216{
217+ // Lock-free: exchange hands each non-null shard pointer to exactly one caller, which frees it.
132218 auto & column_data = (*trace)[static_cast <size_t >(col)];
133- std::unique_lock lock (column_data.mutex );
134- column_data.rows .clear ();
135- column_data.max_row_number = -1 ;
136- column_data.row_number_dirty = false ;
219+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
220+ delete column_data.slots [k].exchange (nullptr , std::memory_order_acq_rel);
221+ }
137222}
138223
139224} // namespace bb::avm2::tracegen
0 commit comments