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"
67#include " barretenberg/common/log.hpp"
78#include " barretenberg/common/ref_vector.hpp"
89#include " barretenberg/vm2/common/field.hpp"
@@ -23,9 +24,15 @@ TraceContainer::TraceContainer()
2324const FF & TraceContainer::get (Column col, uint32_t row) const
2425{
2526 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 ;
27+ const size_t shard_idx = row / INTERVAL_SIZE ;
28+ if (shard_idx >= NUM_SHARDS ) {
29+ return zero;
30+ }
31+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
32+ if (shard == nullptr ) {
33+ return zero;
34+ }
35+ return shard->rows [row % INTERVAL_SIZE ];
2936}
3037
3138const FF & TraceContainer::get_column_or_shift (ColumnAndShifts col, uint32_t row) const
@@ -36,19 +43,42 @@ const FF& TraceContainer::get_column_or_shift(ColumnAndShifts col, uint32_t row)
3643 return get (static_cast <Column>(col), row);
3744}
3845
46+ TraceContainer::ColumnInterval& TraceContainer::get_or_create_shard (SparseColumn& column_data, size_t shard_idx)
47+ {
48+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
49+ if (shard != nullptr ) {
50+ return *shard;
51+ }
52+ // Slow path: this slot has never been written. Construct a shard and install it with a single CAS.
53+ // Creators of different shards target different atomics and run fully in parallel with no lock. If we
54+ // lose the race for this slot (only possible when two chunks share a shard at a boundary), we discard
55+ // our spare copy and use the winner's.
56+ auto fresh = std::make_unique<ColumnInterval>();
57+ ColumnInterval* expected = nullptr ;
58+ if (column_data.slots [shard_idx].compare_exchange_strong (
59+ expected, fresh.get (), std::memory_order_acq_rel, std::memory_order_acquire)) {
60+ return *fresh.release ();
61+ }
62+ return *expected; // CAS failure loaded the winning pointer into `expected` (acquire).
63+ }
64+
3965void TraceContainer::set (Column col, uint32_t row, const FF & value)
4066{
4167 auto & column_data = (*trace)[static_cast <size_t >(col)];
42- std::unique_lock lock (column_data.mutex );
68+ const size_t shard_idx = row / INTERVAL_SIZE ;
69+ BB_ASSERT_LT (shard_idx, NUM_SHARDS , " row exceeds the maximum trace size" );
70+ const uint32_t offset = row % INTERVAL_SIZE ;
71+
4372 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));
73+ // Lock-free: a single atomic load finds the shard (created on first write), then we write our
74+ // own dense cell directly. Different rows are distinct array elements, so concurrent writers of
75+ // this column (or even of the same shard, at a chunk boundary) never race and never serialize.
76+ get_or_create_shard (column_data, shard_idx).rows [offset] = value;
4677 } 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 ;
78+ // Zero value: clear if present. We never create a shard (clearing an absent row is a no-op).
79+ ColumnInterval* shard = column_data.slots [shard_idx].load (std::memory_order_acquire);
80+ if (shard != nullptr ) {
81+ shard->rows [offset] = FF::zero ();
5282 }
5383 }
5484}
@@ -62,24 +92,39 @@ void TraceContainer::set(uint32_t row, std::span<const std::pair<Column, FF>> va
6292
6393void TraceContainer::reserve_column (Column col, size_t size)
6494{
95+ if (size == 0 ) {
96+ return ;
97+ }
6598 auto & column_data = (*trace)[static_cast <size_t >(col)];
66- std::unique_lock lock (column_data.mutex );
67- column_data.rows .reserve (size);
99+ const size_t num_shards = std::min ((size + INTERVAL_SIZE - 1 ) / INTERVAL_SIZE , NUM_SHARDS );
100+ // Each shard's dense row array is full size on creation, so reserving just materializes the shards up
101+ // front (e.g. for precomputed columns). Lock-free: get_or_create_shard installs each via CAS.
102+ for (size_t k = 0 ; k < num_shards; ++k) {
103+ get_or_create_shard (column_data, k);
104+ }
68105}
69106
70107uint32_t TraceContainer::get_column_rows (Column col) const
71108{
109+ // The number of rows is (highest non-zero absolute row + 1). We find it by scanning shards from the
110+ // top down and, within the first non-empty shard, scanning its rows from the top. Shards are
111+ // top-dense, so this terminates almost immediately. This is only called after the parallel fill
112+ // phase, so no lock is needed. Lower shards cannot hold a higher row, so the first hit is the answer.
72113 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 );
114+ for (size_t k = NUM_SHARDS ; k-- > 0 ;) {
115+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
116+ if (shard_ptr == nullptr ) {
117+ continue ;
118+ }
119+ const auto & rows = shard_ptr->rows ;
120+ const uint32_t base = static_cast <uint32_t >(k) * INTERVAL_SIZE ;
121+ for (uint32_t off = INTERVAL_SIZE ; off-- > 0 ;) {
122+ if (!rows[off].is_zero ()) {
123+ return base + off + 1 ;
124+ }
125+ }
126+ }
127+ return 0 ;
83128}
84129
85130uint32_t TraceContainer::get_num_witness_rows () const
@@ -103,9 +148,18 @@ uint32_t TraceContainer::get_num_rows() const
103148void TraceContainer::visit_column (Column col, const std::function<void (uint32_t , const FF &)>& visitor) const
104149{
105150 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);
151+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
152+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
153+ if (shard_ptr == nullptr ) {
154+ continue ;
155+ }
156+ auto & shard = *shard_ptr;
157+ const uint32_t base = static_cast <uint32_t >(k) * INTERVAL_SIZE ;
158+ for (uint32_t off = 0 ; off < INTERVAL_SIZE ; ++off) {
159+ if (!shard.rows [off].is_zero ()) {
160+ visitor (base + off, shard.rows [off]);
161+ }
162+ }
109163 }
110164}
111165
@@ -120,20 +174,28 @@ void TraceContainer::invert_column(Column col)
120174{
121175 RefVector<FF > ff_vector;
122176 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);
177+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
178+ ColumnInterval* shard_ptr = column_data.slots [k].load (std::memory_order_acquire);
179+ if (shard_ptr == nullptr ) {
180+ continue ;
181+ }
182+ auto & shard = *shard_ptr;
183+ for (auto & value : shard.rows ) {
184+ if (!value.is_zero ()) {
185+ ff_vector.push_back (value);
186+ }
187+ }
126188 }
127189 FF ::batch_invert<RefVector<FF >>(ff_vector);
128190}
129191
130192void TraceContainer::clear_column (Column col)
131193{
194+ // Lock-free: exchange hands each non-null shard pointer to exactly one caller, which frees it.
132195 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 ;
196+ for (size_t k = 0 ; k < NUM_SHARDS ; ++k) {
197+ delete column_data.slots [k].exchange (nullptr , std::memory_order_acq_rel);
198+ }
137199}
138200
139201} // namespace bb::avm2::tracegen
0 commit comments