static_embeddings 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/MODEL_AUDIT.md CHANGED
@@ -1,24 +1,37 @@
1
1
  # Model Audit
2
2
 
3
- This document records the trust and parity status of converted `.semb` models.
4
- A converted production model is accepted only after parity against the upstream
5
- Python `model2vec.StaticModel` implementation is recorded here.
3
+ A converted model is trusted only after parity against upstream
4
+ `model2vec.StaticModel` is recorded here. An audit record covers one `.semb`
5
+ file **and** one runtime version: changing the tokenizer, normalizer, prefix
6
+ window, pooling, output normalization or the oracle's corpus invalidates the
7
+ runtime half of it even though the file bytes are untouched.
6
8
 
7
9
  ## potion-retrieval-32m
8
10
 
9
- Status: audited / accepted
11
+ | runtime | parity | note |
12
+ |---|---|---|
13
+ | 0.1.1 | `parity OK`, recorded below | superseded |
14
+ | 0.1.2 | **not re-run** | superseded before release |
15
+ | 0.1.3 | `parity OK`, recorded below | current |
16
+
17
+ 0.1.2 changed `is_control()`, which changes token ids for any input containing
18
+ `U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
19
+ matches HuggingFace, because it is an implementation twin of the C runtime
20
+ written in this repository. The 0.1.3 release candidate was checked against a
21
+ fresh upstream `model2vec.StaticModel` oracle that includes DEL, control
22
+ characters, Unicode, OOV, long-word and truncation rows.
10
23
 
11
24
  Source model:
12
25
 
13
26
  - Hugging Face repository: `minishlab/potion-retrieval-32M`
27
+ - Hugging Face snapshot: `6fc8051fab2a1e0ee76689cf08c853792ac285e7`
14
28
  - Oracle implementation: `model2vec.StaticModel.from_pretrained`
15
29
  - Python package: `model2vec 0.9.0`
16
30
  - Oracle file: `tmp/model2vec_oracle.json`
17
- - Oracle rows: `28`
31
+ - Oracle rows in this recorded run: `31`
18
32
  - Oracle dimension: `512`
19
33
  - Oracle max length: `512`
20
- - Runtime checked with: `static_embeddings 0.1.1`
21
- - Runtime source note: re-run parity after changing tokenizer, normalizer, prefix-window, pooling, or output-normalization code.
34
+ - Runtime at time of this record: `static_embeddings 0.1.3`
22
35
 
23
36
  Converted `.semb`:
24
37
 
@@ -31,6 +44,9 @@ Converted `.semb`:
31
44
  Parity command:
32
45
 
33
46
  ```bash
47
+ python tools/model2vec_oracle.py minishlab/potion-retrieval-32M \
48
+ --out tmp/model2vec_oracle.json
49
+
34
50
  bundle exec rake parity \
35
51
  MODEL="$HOME/.cache/static_embeddings/models/potion-retrieval-32m.semb" \
36
52
  ORACLE=tmp/model2vec_oracle.json
@@ -39,8 +55,8 @@ bundle exec rake parity \
39
55
  Parity result:
40
56
 
41
57
  ```text
42
- rows=28
43
- id_rows_checked=28
58
+ rows=31
59
+ id_rows_checked=31
44
60
  min_cosine=0.9999999999989528
45
61
  max_abs_all=2.980232238769531e-07
46
62
  token_id_failures=[]
@@ -57,5 +73,8 @@ Decision:
57
73
  - Unicode normalization behavior: pass
58
74
  - Unknown-word behavior: pass
59
75
  - Long input / truncation behavior: pass
76
+ - DEL / control-character behavior: pass
60
77
 
61
- This converted model is accepted for runtime and benchmark use.
78
+ Accepted for runtime and benchmark use **under 0.1.3**. The `.semb` file is
79
+ unchanged and its SHA256 still matches; the runtime half of the record was
80
+ refreshed after the DEL fix.
data/docs/PERFORMANCE.md CHANGED
@@ -1,60 +1,136 @@
1
1
  # Performance budget
2
2
 
3
- Absolute latency numbers are not comparable across models: `potion-base-2M`
4
- and `potion-retrieval-32M` differ by roughly 8x in row width, so swapping the
5
- model would read as a regression. The budget is therefore normalised.
3
+ Absolute latency is not comparable across models: `potion-base-2M` and
4
+ `potion-retrieval-32M` differ by roughly 8x in row width, so swapping the model
5
+ would read as a regression. The budget is therefore normalised.
6
6
 
7
- Run `ruby tools/benchmark.rb [model.semb]`.
7
+ ```bash
8
+ ruby tools/benchmark.rb [model.semb]
9
+ ```
8
10
 
9
11
  ## Metrics tracked
10
12
 
11
13
  | Metric | Unit | Why |
12
14
  |---|---|---|
13
- | tokenization | ns / input byte | independent of `dim`; catches normalizer regressions |
15
+ | tokenization | ns / processed byte | independent of `dim`; catches normalizer regressions |
14
16
  | pooling | ns / token / 100 dims | comparable across models |
15
17
  | single embed | µs / call | the latency a query pays |
16
18
  | batch throughput | texts/s, tokens/s | indexing capacity |
17
19
  | RSS overhead | bytes beyond the mapped file | must stay O(scratch), not O(model) |
18
- | cold vs warm | first query after a cold page cache | mmap means the first pass faults in the matrix |
19
20
  | time to first query | ms from `load` to first vector | what a Puma `before_fork` or a serverless cold start pays |
21
+ | C allocation stats | bytes and counts by native category | optional build, explains heap churn |
20
22
 
21
- `samples/cold_start.rb` measures the last row. It is single shot on purpose:
22
- `load` walks the whole vocabulary hash table and both tries exactly once, and
23
- `warmup!` only faults pages in once. Drop the page cache first
24
- (`sudo purge` on macOS, `echo 3 > /proc/sys/vm/drop_caches` on Linux) or the
25
- output will label itself a warm start.
23
+ ## Where the time actually goes
24
+
25
+ Tokenization dominates; pooling is a handful of row reads and adds. That is why
26
+ the tokenizer is the file to profile, and why SIMD in the pooling loop is not
27
+ where the wins are that loop is memory-bound on random row lookups and the
28
+ compiler already vectorises it.
29
+
30
+ Out-of-vocabulary text is the main cliff. In-vocabulary words hit the
31
+ vocabulary hash directly; everything else falls through to trie-driven subword
32
+ splitting, which on a synthetic all-OOV corpus measured several times the
33
+ per-text cost. Non-English input on an English model pays this on top of the
34
+ `[UNK]` quality problem.
35
+
36
+ ## `f16` is a storage trade-off
37
+
38
+ `format: :f16` halves the bytes a top-k scan streams and doubles the decode
39
+ work. Which one wins is a property of the machine, and this repository's own
40
+ sample runs disagree with each other — same corpus, same `k`, native decode
41
+ kernel in both cases:
42
+
43
+ ```text
44
+ f32 f16
45
+ x86-64, f16c 3.00 ms 1.65 ms f16 1.8x faster
46
+ M1 Pro, neon-fp16 2.29 ms 2.88 ms f16 1.3x slower
47
+ ```
48
+
49
+ Neither ratio transfers. Confirm `StaticEmbeddings.simd_backend`, then measure
50
+ on the hardware you will run on. On the lookup-table fallback `f16` is usually
51
+ slower than `f32`.
52
+
53
+ ## C allocation counters
54
+
55
+ Compiling with `STATIC_EMBEDDINGS_ALLOC_STATS=1` wraps the runtime's own
56
+ allocations and records bytes and counts per category. Default builds do not
57
+ define the internal methods and do not pay for the counters.
58
+
59
+ ```bash
60
+ STATIC_EMBEDDINGS_ALLOC_STATS=1 samples/run_all.sh
61
+ ```
26
62
 
27
- Every other sample under `samples/` runs a hot loop and measures steady state.
28
- `warmup_hot_path.rb` in particular re-touches pages that are already resident;
29
- its `mapped_range_gb_per_sec` is address-range coverage, not memory bandwidth,
30
- because warmup reads one byte per page.
63
+ The harness forces `rake clobber compile` in this mode, because an existing
64
+ mkmf Makefile otherwise keeps the previous `DEFS`, and it fails the run if the
65
+ instrumented methods did not load. Samples then print
66
+ `c_alloc.<category>.<metric>` lines.
31
67
 
32
- ## CI status
68
+ Two limits worth stating. The counters cover only allocations made through the
69
+ instrumented wrappers, so Ruby object heap, allocator fragmentation and mmap
70
+ residency are all outside them — use Instruments, heaptrack, Massif or `vmmap`
71
+ for those. And the instrumentation is not free: about 13% on `tokenize`, 3% on
72
+ `embed`, within noise on `embed_batch`. A run captured with it is not
73
+ comparable to one captured without it.
33
74
 
34
- The CI benchmark job is informational. Treat regression gates as meaningful only when a checked-in baseline is compared on the same runner class. Absolute thresholds on shared CI hardware produce noise, not signal.
75
+ ## Running the benchmarks
76
+
77
+ `bundle exec rake benchmark`, or one at a time with `rake benchmark:core_paths`
78
+ and `rake benchmark:gvl_threshold`. Generated text inputs are
79
+ `valid_encoding?`-primed before timing, so these are cached-coderange hot-path
80
+ numbers. Every row carries `spread_pct`, and a difference smaller than that is
81
+ not a result. See `docs/BENCHMARKING.md`.
35
82
 
36
83
  ## Benchmark hygiene
37
84
 
38
- Two failure modes have bitten this repository already and are worth stating:
85
+ Three failure modes have bitten this repository already.
39
86
 
40
- - **Logical vs processed bytes.** With truncation active, a 3 MB document is
41
- tokenized only until `max_tokens` is reached. Any "MB/s of input" figure
42
- divides by bytes that were never read. Samples that still print one label it
43
- `logical_input_mb_per_sec` and set `truncation_active`.
44
- - **Resident working sets.** Embedding the same text, or the same frozen id
45
- array, in a loop keeps a tiny slice of the matrix in cache and overstates
46
- throughput. `random_pooling_hot_path.rb` rotates through many id sets and
47
- prints `touched_matrix_mb` so the reader can check it exceeds the last level
48
- cache.
87
+ **Logical vs processed bytes.** With truncation active a 3 MB document is
88
+ tokenized only until `max_tokens` is reached, so any "MB/s of input" figure
89
+ computed from the string length divides by bytes that were never read. Samples
90
+ print both `logical_input_mb_per_sec` and a measured
91
+ `processed_input_mb_per_sec`; quote the second. `tools/benchmark.rb` refuses to
92
+ print a ns/byte figure at all when truncation makes the two differ.
49
93
 
50
- `GC.disable` is off by default in the sample harness: it does not stop an
51
- already started incremental cycle from sweeping, so zeroed GC counters used to
52
- appear next to profiles full of `gc_sweep`. Set `GC_DISABLE=1` deliberately and
53
- read `gc_disabled=` before quoting `gc_delta`.
94
+ **Resident working sets.** Embedding the same text, or the same frozen id array,
95
+ in a loop keeps a tiny slice of the matrix in cache and overstates throughput.
96
+ `random_pooling_hot_path.rb` rotates through many id sets and prints
97
+ `touched_matrix_mb` so you can check it exceeds the last level cache.
54
98
 
55
- ## Where the time actually goes
99
+ **GC state.** `GC.disable` does not stop an already started incremental cycle
100
+ from sweeping, so zeroed GC counters used to appear next to profiles full of
101
+ `gc_sweep`. It is off by default in the harness; set `GC_DISABLE=1`
102
+ deliberately and read `gc_disabled=` before quoting `gc_delta`.
103
+
104
+ ## Two costs that are not the tokenizer
105
+
106
+ `embed` cannot start until Ruby has confirmed the `String` is valid UTF-8, and
107
+ for a `String` whose coderange has not been computed that is an O(bytes) scan
108
+ which runs before the prefix window is chosen. On a 3 MB freshly read document
109
+ it dominated: 442 µs against 71 µs for the same string once Ruby had cached the
110
+ answer. Reuse the `String`, call `valid_encoding?` outside the hot path, or pass
111
+ `validate_encoding: :prefix` to bound validation the same way tokenizing is
112
+ bounded — 78 µs, at the cost of not inspecting bytes past the window.
113
+
114
+ `StaticEmbeddings.verify` hashes the artifact and is deliberately not on the
115
+ load path. It streams in 1 MiB chunks: on a 62 MB model, 249 ms and 30 MB of
116
+ peak RSS. Reading the file whole and duplicating it to zero the checksum field
117
+ cost 433 ms and 280 MB.
118
+
119
+ ## Cold start
120
+
121
+ `samples/cold_start.rb` measures `load`, first embed and `warmup!` in one shot,
122
+ because each of those happens exactly once: `load` walks the vocabulary hash and
123
+ both tries, and `warmup!` faults pages in. Drop the page cache first
124
+ (`sudo purge` on macOS, `echo 3 > /proc/sys/vm/drop_caches` on Linux) or the
125
+ output labels itself a warm start.
126
+
127
+ Every other sample runs a hot loop and measures steady state. `warmup_hot_path.rb`
128
+ in particular re-touches resident pages; its `mapped_range_gb_per_sec` is
129
+ address-range coverage, not memory bandwidth, because warmup reads one byte per
130
+ page.
131
+
132
+ ## CI
56
133
 
57
- For static embeddings, tokenization dominates: pooling is a handful of row
58
- reads and adds. That is why the tokenizer is the file to profile, and why
59
- SIMD in the pooling loop is not where the wins are — the loop is memory-bound
60
- on random row lookups, and the compiler already vectorises it.
134
+ The benchmark job is informational. Absolute thresholds on shared runners
135
+ produce noise, not signal; a regression gate is only meaningful against a
136
+ checked-in baseline on the same runner class.
@@ -24,6 +24,9 @@ end
24
24
 
25
25
  $srcs = %w[
26
26
  static_embeddings.c
27
+ se_alloc_stats.c
28
+ se_f16.c
29
+ se_topk.c
27
30
  se_format.c
28
31
  se_unicode.c
29
32
  se_tokenizer.c
@@ -40,5 +43,6 @@ end
40
43
 
41
44
  $CFLAGS += " -O3 -std=gnu99 -fvisibility=hidden"
42
45
  $CFLAGS += " -Wall -Wextra -Wno-unused-parameter"
46
+ $defs << "-DSE_ENABLE_ALLOC_STATS=1" if ENV["STATIC_EMBEDDINGS_ALLOC_STATS"] == "1"
43
47
 
44
48
  create_makefile("static_embeddings/static_embeddings")
@@ -0,0 +1,244 @@
1
+ #include "se_internal.h"
2
+
3
+ #include <stdint.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+
7
+ static const char *const se_alloc_category_names[SE_ALLOC_CATEGORY_COUNT] = {
8
+ "unknown", "scratch", "batch_input", "batch_output",
9
+ "batch_stats", "batch_index", "token_ids", "topk_query",
10
+ "topk_best", "topk_matrix_copy", "format_validate", "model_file"};
11
+
12
+ const char *se_alloc_category_name(se_alloc_category_t category) {
13
+ if ((unsigned)category >= SE_ALLOC_CATEGORY_COUNT)
14
+ return se_alloc_category_names[SE_ALLOC_UNKNOWN];
15
+ return se_alloc_category_names[category];
16
+ }
17
+
18
+ #if SE_ENABLE_ALLOC_STATS
19
+
20
+ #define SE_ALLOC_STATS_MAGIC 0x5EA110C5u
21
+
22
+ typedef union {
23
+ struct {
24
+ uint32_t magic;
25
+ uint32_t category;
26
+ size_t size;
27
+ } h;
28
+ long double align_long_double;
29
+ void *align_ptr;
30
+ uint64_t align_u64;
31
+ } se_alloc_header_t;
32
+
33
+ static se_alloc_stats_t se_alloc_stats[SE_ALLOC_CATEGORY_COUNT];
34
+
35
+ static se_alloc_category_t normalize_category(se_alloc_category_t category) {
36
+ if ((unsigned)category >= SE_ALLOC_CATEGORY_COUNT)
37
+ return SE_ALLOC_UNKNOWN;
38
+ return category;
39
+ }
40
+
41
+ #if defined(__GNUC__) || defined(__clang__)
42
+ static size_t atomic_add_size(size_t *ptr, size_t value) {
43
+ return __sync_add_and_fetch(ptr, value);
44
+ }
45
+
46
+ static size_t atomic_sub_size(size_t *ptr, size_t value) {
47
+ return __sync_sub_and_fetch(ptr, value);
48
+ }
49
+
50
+ static void atomic_inc_size(size_t *ptr) {
51
+ __sync_fetch_and_add(ptr, (size_t)1);
52
+ }
53
+
54
+ static size_t atomic_load_size(const size_t *ptr) {
55
+ return __sync_fetch_and_add((size_t *)ptr, (size_t)0);
56
+ }
57
+
58
+ static void atomic_store_size(size_t *ptr, size_t value) {
59
+ __sync_lock_test_and_set(ptr, value);
60
+ }
61
+
62
+ static int atomic_cas_size(size_t *ptr, size_t old_value, size_t new_value) {
63
+ return __sync_bool_compare_and_swap(ptr, old_value, new_value);
64
+ }
65
+ #else
66
+ static size_t atomic_add_size(size_t *ptr, size_t value) {
67
+ *ptr += value;
68
+ return *ptr;
69
+ }
70
+
71
+ static size_t atomic_sub_size(size_t *ptr, size_t value) {
72
+ *ptr -= value;
73
+ return *ptr;
74
+ }
75
+
76
+ static void atomic_inc_size(size_t *ptr) {
77
+ (*ptr)++;
78
+ }
79
+
80
+ static size_t atomic_load_size(const size_t *ptr) {
81
+ return *ptr;
82
+ }
83
+
84
+ static void atomic_store_size(size_t *ptr, size_t value) {
85
+ *ptr = value;
86
+ }
87
+
88
+ static int atomic_cas_size(size_t *ptr, size_t old_value, size_t new_value) {
89
+ if (*ptr != old_value)
90
+ return 0;
91
+ *ptr = new_value;
92
+ return 1;
93
+ }
94
+ #endif
95
+
96
+ static void record_peak(se_alloc_category_t category, size_t current) {
97
+ size_t peak = atomic_load_size(&se_alloc_stats[category].peak_bytes);
98
+ while (current > peak) {
99
+ if (atomic_cas_size(&se_alloc_stats[category].peak_bytes, peak, current))
100
+ return;
101
+ peak = atomic_load_size(&se_alloc_stats[category].peak_bytes);
102
+ }
103
+ }
104
+
105
+ static void record_alloc(se_alloc_category_t category, size_t bytes) {
106
+ se_alloc_stats_t *stats = &se_alloc_stats[category];
107
+ size_t current = atomic_add_size(&stats->current_bytes, bytes);
108
+ atomic_add_size(&stats->total_allocated_bytes, bytes);
109
+ atomic_inc_size(&stats->alloc_count);
110
+ record_peak(category, current);
111
+ }
112
+
113
+ static void record_realloc(se_alloc_category_t old_category, size_t old_bytes,
114
+ se_alloc_category_t new_category, size_t new_bytes) {
115
+ se_alloc_stats_t *old_stats = &se_alloc_stats[old_category];
116
+ se_alloc_stats_t *new_stats = &se_alloc_stats[new_category];
117
+ atomic_sub_size(&old_stats->current_bytes, old_bytes);
118
+ atomic_add_size(&old_stats->total_freed_bytes, old_bytes);
119
+ atomic_inc_size(&old_stats->free_count);
120
+
121
+ size_t current = atomic_add_size(&new_stats->current_bytes, new_bytes);
122
+ atomic_add_size(&new_stats->total_allocated_bytes, new_bytes);
123
+ atomic_inc_size(&new_stats->alloc_count);
124
+ atomic_inc_size(&new_stats->realloc_count);
125
+ record_peak(new_category, current);
126
+ }
127
+
128
+ static void record_free(se_alloc_category_t category, size_t bytes) {
129
+ se_alloc_stats_t *stats = &se_alloc_stats[category];
130
+ atomic_sub_size(&stats->current_bytes, bytes);
131
+ atomic_add_size(&stats->total_freed_bytes, bytes);
132
+ atomic_inc_size(&stats->free_count);
133
+ }
134
+
135
+ static se_alloc_header_t *header_from_ptr(void *ptr) {
136
+ return ((se_alloc_header_t *)ptr) - 1;
137
+ }
138
+
139
+ void *se_alloc_stats_malloc(se_alloc_category_t category, size_t bytes) {
140
+ size_t total = 0;
141
+ if (!se_checked_add_size(sizeof(se_alloc_header_t), bytes, &total))
142
+ return NULL;
143
+
144
+ se_alloc_header_t *header = (se_alloc_header_t *)malloc(total);
145
+ if (!header)
146
+ return NULL;
147
+
148
+ category = normalize_category(category);
149
+ header->h.magic = SE_ALLOC_STATS_MAGIC;
150
+ header->h.category = (uint32_t)category;
151
+ header->h.size = bytes;
152
+ record_alloc(category, bytes);
153
+ return (void *)(header + 1);
154
+ }
155
+
156
+ void *se_alloc_stats_calloc(se_alloc_category_t category, size_t count, size_t elem_size) {
157
+ size_t bytes = 0;
158
+ if (!se_checked_mul_size(count, elem_size, &bytes))
159
+ return NULL;
160
+
161
+ void *ptr = se_alloc_stats_malloc(category, bytes);
162
+ if (ptr)
163
+ memset(ptr, 0, bytes);
164
+ return ptr;
165
+ }
166
+
167
+ void *se_alloc_stats_realloc(se_alloc_category_t category, void *ptr, size_t bytes) {
168
+ if (!ptr)
169
+ return se_alloc_stats_malloc(category, bytes);
170
+
171
+ size_t total = 0;
172
+ if (!se_checked_add_size(sizeof(se_alloc_header_t), bytes, &total))
173
+ return NULL;
174
+
175
+ se_alloc_header_t *old_header = header_from_ptr(ptr);
176
+ if (old_header->h.magic != SE_ALLOC_STATS_MAGIC)
177
+ abort();
178
+
179
+ se_alloc_category_t old_category = normalize_category((se_alloc_category_t)old_header->h.category);
180
+ size_t old_bytes = old_header->h.size;
181
+ se_alloc_header_t *new_header = (se_alloc_header_t *)realloc(old_header, total);
182
+ if (!new_header)
183
+ return NULL;
184
+
185
+ category = normalize_category(category);
186
+ new_header->h.magic = SE_ALLOC_STATS_MAGIC;
187
+ new_header->h.category = (uint32_t)category;
188
+ new_header->h.size = bytes;
189
+ record_realloc(old_category, old_bytes, category, bytes);
190
+ return (void *)(new_header + 1);
191
+ }
192
+
193
+ void se_alloc_stats_free(void *ptr) {
194
+ if (!ptr)
195
+ return;
196
+
197
+ se_alloc_header_t *header = header_from_ptr(ptr);
198
+ if (header->h.magic != SE_ALLOC_STATS_MAGIC)
199
+ abort();
200
+
201
+ se_alloc_category_t stored_category =
202
+ normalize_category((se_alloc_category_t)header->h.category);
203
+ size_t bytes = header->h.size;
204
+ header->h.magic = 0;
205
+ record_free(stored_category, bytes);
206
+ free(header);
207
+ }
208
+
209
+ void se_alloc_stats_reset(void) {
210
+ for (size_t i = 0; i < SE_ALLOC_CATEGORY_COUNT; i++) {
211
+ se_alloc_stats_t *stats = &se_alloc_stats[i];
212
+ size_t current = atomic_load_size(&stats->current_bytes);
213
+ atomic_store_size(&stats->peak_bytes, current);
214
+ atomic_store_size(&stats->total_allocated_bytes, 0);
215
+ atomic_store_size(&stats->total_freed_bytes, 0);
216
+ atomic_store_size(&stats->alloc_count, 0);
217
+ atomic_store_size(&stats->realloc_count, 0);
218
+ atomic_store_size(&stats->free_count, 0);
219
+ }
220
+ }
221
+
222
+ void se_alloc_stats_snapshot(se_alloc_stats_t out[SE_ALLOC_CATEGORY_COUNT]) {
223
+ for (size_t i = 0; i < SE_ALLOC_CATEGORY_COUNT; i++) {
224
+ out[i].current_bytes = atomic_load_size(&se_alloc_stats[i].current_bytes);
225
+ out[i].peak_bytes = atomic_load_size(&se_alloc_stats[i].peak_bytes);
226
+ out[i].total_allocated_bytes =
227
+ atomic_load_size(&se_alloc_stats[i].total_allocated_bytes);
228
+ out[i].total_freed_bytes = atomic_load_size(&se_alloc_stats[i].total_freed_bytes);
229
+ out[i].alloc_count = atomic_load_size(&se_alloc_stats[i].alloc_count);
230
+ out[i].realloc_count = atomic_load_size(&se_alloc_stats[i].realloc_count);
231
+ out[i].free_count = atomic_load_size(&se_alloc_stats[i].free_count);
232
+ }
233
+ }
234
+
235
+ #else
236
+
237
+ void se_alloc_stats_reset(void) {
238
+ }
239
+
240
+ void se_alloc_stats_snapshot(se_alloc_stats_t out[SE_ALLOC_CATEGORY_COUNT]) {
241
+ memset(out, 0, sizeof(se_alloc_stats_t) * SE_ALLOC_CATEGORY_COUNT);
242
+ }
243
+
244
+ #endif