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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -0
- data/README.md +172 -275
- data/Rakefile +95 -0
- data/benchmark/_support.rb +196 -0
- data/benchmark/core_paths.rb +57 -0
- data/benchmark/gvl_threshold.rb +48 -0
- data/docs/ARCHITECTURE.md +56 -6
- data/docs/BENCHMARKING.md +87 -0
- data/docs/LIMITATIONS.md +91 -0
- data/docs/MODEL_AUDIT.md +29 -10
- data/docs/PERFORMANCE.md +112 -36
- data/ext/static_embeddings/extconf.rb +4 -0
- data/ext/static_embeddings/se_alloc_stats.c +244 -0
- data/ext/static_embeddings/se_f16.c +378 -0
- data/ext/static_embeddings/se_format.c +238 -148
- data/ext/static_embeddings/se_internal.h +159 -0
- data/ext/static_embeddings/se_tokenizer.c +144 -41
- data/ext/static_embeddings/se_topk.c +236 -0
- data/ext/static_embeddings/static_embeddings.c +277 -744
- data/lib/static_embeddings/format.rb +22 -11
- data/lib/static_embeddings/version.rb +1 -1
- data/static_embeddings.gemspec +2 -0
- data/tools/benchmark.rb +13 -4
- metadata +10 -1
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
# Model Audit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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: `
|
|
31
|
+
- Oracle rows in this recorded run: `31`
|
|
18
32
|
- Oracle dimension: `512`
|
|
19
33
|
- Oracle max length: `512`
|
|
20
|
-
- Runtime
|
|
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=
|
|
43
|
-
id_rows_checked=
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
Three failure modes have bitten this repository already.
|
|
39
86
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|