static_embeddings 0.1.1 → 0.1.2
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 +121 -0
- data/README.md +167 -276
- data/docs/ARCHITECTURE.md +56 -6
- data/docs/MODEL_AUDIT.md +21 -8
- data/docs/PERFORMANCE.md +104 -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 +235 -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 +225 -744
- data/lib/static_embeddings/format.rb +22 -11
- data/lib/static_embeddings/version.rb +1 -1
- data/tools/benchmark.rb +13 -4
- metadata +4 -1
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -24,6 +24,15 @@ Everything expensive, fragile or security-sensitive about reading third-party
|
|
|
24
24
|
model files happens once, offline, in a language where it is easy to get
|
|
25
25
|
right. What remains in C is a bounds-checked mmap and three loops.
|
|
26
26
|
|
|
27
|
+
The C side is `se_format.c` (mmap and validation), `se_tokenizer.c`,
|
|
28
|
+
`se_embed.c`, `se_unicode.c`, `se_f16.c` (half-precision codec and kernels),
|
|
29
|
+
`se_topk.c`, `se_alloc_stats.c` (optional allocation counters), and
|
|
30
|
+
`static_embeddings.c` for the Ruby bindings. Shared size arithmetic,
|
|
31
|
+
`static_assert`s on the mmapped struct layouts and the big-endian rejection live
|
|
32
|
+
in `se_internal.h`: the header fields are decoded little-endian explicitly, but
|
|
33
|
+
the mmapped structures and the float matrix are read in native order, so a
|
|
34
|
+
big-endian host is refused at load rather than silently misread.
|
|
35
|
+
|
|
27
36
|
## `.semb` v2
|
|
28
37
|
|
|
29
38
|
Little-endian throughout. 320-byte header, then sections aligned to 64 bytes.
|
|
@@ -90,10 +99,23 @@ tries inspired by double-array trie libraries such as libdatrie:
|
|
|
90
99
|
- `root_trie` for tokens that can start a word;
|
|
91
100
|
- `continuation_trie` for `##token` entries stored without the `##` prefix.
|
|
92
101
|
|
|
93
|
-
At runtime `append_wordpiece`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
At runtime `append_wordpiece` first tries the hash table for the whole word,
|
|
103
|
+
which is the common case for in-vocabulary text, and falls back to walking the
|
|
104
|
+
relevant trie once, recording the longest terminal node. Both halves earn their
|
|
105
|
+
place: removing the hash pre-check made tokenization several times slower on
|
|
106
|
+
ordinary text, and replacing the trie with repeated hash lookups by decreasing
|
|
107
|
+
length made out-of-vocabulary words several times slower still.
|
|
108
|
+
|
|
109
|
+
Normalization has an ASCII fast path. Below `0x80` the CJK, NFD, combining-mark
|
|
110
|
+
and case-folding branches always resolve the same way, so those runs go through
|
|
111
|
+
a 128-entry classification table instead of the full
|
|
112
|
+
`emit_cleaned -> emit_stripped -> emit_lowered -> feed_token_cp` chain, and a
|
|
113
|
+
word reserves its codepoint buffer once rather than once per character. The
|
|
114
|
+
table is generated to agree with `is_control`, `se_is_ascii_whitespace` and
|
|
115
|
+
`se_is_ascii_punct`, and `test/ascii_parity_test.rb` sweeps every byte in
|
|
116
|
+
`0x00..0x7F` against the Ruby reference to keep it that way. That test exists
|
|
117
|
+
because `U+007F DEL` was misclassified through 0.1.1 and every sampled test in
|
|
118
|
+
the suite passed anyway.
|
|
97
119
|
|
|
98
120
|
Loading a model still performs **zero runtime insertions**. It is `mmap` plus a
|
|
99
121
|
validation pass over the hash table, trie node ranges and trie edge ordering.
|
|
@@ -224,6 +246,33 @@ not triple the cost.
|
|
|
224
246
|
With `max_tokens: false` there is nothing to truncate and the whole input is
|
|
225
247
|
copied.
|
|
226
248
|
|
|
249
|
+
The window bounds tokenizing, not the whole call. Ruby has to answer whether the
|
|
250
|
+
`String` is valid UTF-8, and `rb_enc_str_coderange` answers it for the whole
|
|
251
|
+
`String` — an O(bytes) scan when Ruby has not computed a coderange yet, running
|
|
252
|
+
before the prefix is even chosen. `validate_encoding: :prefix` skips that scan
|
|
253
|
+
and leans on the fact that the C side already validates what it reads:
|
|
254
|
+
`decode_one` rejects malformed UTF-8, and `se_prefix_boundary_len` returns 0
|
|
255
|
+
when it cannot decode, which degrades to a full copy rather than reading out of
|
|
256
|
+
bounds. So `:prefix` is memory-safe in every case; what it gives up is noticing
|
|
257
|
+
invalid bytes the tokenizer never reaches. A cached coderange is always
|
|
258
|
+
honoured, which keeps the cheap answer authoritative when Ruby already has one.
|
|
259
|
+
|
|
260
|
+
## Mapping the model file
|
|
261
|
+
|
|
262
|
+
POSIX uses `mmap` with `PROT_READ`/`MAP_PRIVATE`; Windows uses
|
|
263
|
+
`CreateFileMapping` plus `MapViewOfFile`. Both give the same three properties
|
|
264
|
+
the format was designed around: pages fault in lazily instead of being read up
|
|
265
|
+
front, the page cache is shared between forked or sibling processes, and the
|
|
266
|
+
runtime never owns a writable copy of the matrix. Windows previously read the
|
|
267
|
+
file into the heap, which cost every process a private copy of the model and a
|
|
268
|
+
full read before the first query.
|
|
269
|
+
|
|
270
|
+
Windows keeps the heap path as a fallback when mapping fails, since some network
|
|
271
|
+
filesystems refuse it, and `model->mapped` records which one was taken so
|
|
272
|
+
`se_model_close` unmaps or frees correctly. `verify` is separate from loading and
|
|
273
|
+
streams the file through SHA-256 in chunks, so checking an artifact never
|
|
274
|
+
materialises it either.
|
|
275
|
+
|
|
227
276
|
## Concurrency
|
|
228
277
|
|
|
229
278
|
Small calls run inline. Large `embed_batch` calls copy Ruby input into C-owned
|
|
@@ -239,8 +288,9 @@ job/application layer.
|
|
|
239
288
|
|
|
240
289
|
Cancellation is cooperative: the tokenizer checks a flag every 1024 codepoints
|
|
241
290
|
(counted by iteration, not by byte offset, so the interval does not depend on
|
|
242
|
-
how wide the input's codepoints are), the
|
|
243
|
-
|
|
291
|
+
how wide the input's codepoints are), and the ASCII fast path checks on the same
|
|
292
|
+
cadence in bytes so a long ASCII run is no less interruptible. The pooling loop
|
|
293
|
+
checks every 256 rows, and the top-k scan every 1024 rows. `unblock_cancel` sets that flag when Ruby
|
|
244
294
|
interrupts a GVL-free region.
|
|
245
295
|
|
|
246
296
|
No global mutable state exists in C. The model is immutable after load and
|
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
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** | required before release |
|
|
15
|
+
|
|
16
|
+
0.1.2 changed `is_control()`, which changes token ids for any input containing
|
|
17
|
+
`U+007F`. `StaticEmbeddings::Reference` cannot settle whether the new behaviour
|
|
18
|
+
matches HuggingFace, because it is an implementation twin of the C runtime
|
|
19
|
+
written in this repository. Only the oracle can, and the oracle now carries DEL
|
|
20
|
+
and control-character rows it did not have when the result below was recorded —
|
|
21
|
+
so `rows=28` would itself be evidence of a stale run.
|
|
10
22
|
|
|
11
23
|
Source model:
|
|
12
24
|
|
|
@@ -14,11 +26,10 @@ Source model:
|
|
|
14
26
|
- Oracle implementation: `model2vec.StaticModel.from_pretrained`
|
|
15
27
|
- Python package: `model2vec 0.9.0`
|
|
16
28
|
- Oracle file: `tmp/model2vec_oracle.json`
|
|
17
|
-
- Oracle rows: `28`
|
|
29
|
+
- Oracle rows in this recorded run: `28`
|
|
18
30
|
- Oracle dimension: `512`
|
|
19
31
|
- Oracle max length: `512`
|
|
20
|
-
- Runtime
|
|
21
|
-
- Runtime source note: re-run parity after changing tokenizer, normalizer, prefix-window, pooling, or output-normalization code.
|
|
32
|
+
- Runtime at time of this record: `static_embeddings 0.1.1`
|
|
22
33
|
|
|
23
34
|
Converted `.semb`:
|
|
24
35
|
|
|
@@ -58,4 +69,6 @@ Decision:
|
|
|
58
69
|
- Unknown-word behavior: pass
|
|
59
70
|
- Long input / truncation behavior: pass
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
Accepted for runtime and benchmark use **under 0.1.1**. The `.semb` file is
|
|
73
|
+
unchanged and its SHA256 still matches; what expired is the runtime half of the
|
|
74
|
+
record.
|
data/docs/PERFORMANCE.md
CHANGED
|
@@ -1,60 +1,128 @@
|
|
|
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.
|
|
26
58
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
because warmup reads one byte per page.
|
|
59
|
+
```bash
|
|
60
|
+
STATIC_EMBEDDINGS_ALLOC_STATS=1 samples/run_all.sh
|
|
61
|
+
```
|
|
31
62
|
|
|
32
|
-
|
|
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.
|
|
33
67
|
|
|
34
|
-
|
|
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.
|
|
35
74
|
|
|
36
75
|
## Benchmark hygiene
|
|
37
76
|
|
|
38
|
-
|
|
77
|
+
Three failure modes have bitten this repository already.
|
|
39
78
|
|
|
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.
|
|
79
|
+
**Logical vs processed bytes.** With truncation active a 3 MB document is
|
|
80
|
+
tokenized only until `max_tokens` is reached, so any "MB/s of input" figure
|
|
81
|
+
computed from the string length divides by bytes that were never read. Samples
|
|
82
|
+
print both `logical_input_mb_per_sec` and a measured
|
|
83
|
+
`processed_input_mb_per_sec`; quote the second. `tools/benchmark.rb` refuses to
|
|
84
|
+
print a ns/byte figure at all when truncation makes the two differ.
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
**Resident working sets.** Embedding the same text, or the same frozen id array,
|
|
87
|
+
in a loop keeps a tiny slice of the matrix in cache and overstates throughput.
|
|
88
|
+
`random_pooling_hot_path.rb` rotates through many id sets and prints
|
|
89
|
+
`touched_matrix_mb` so you can check it exceeds the last level cache.
|
|
54
90
|
|
|
55
|
-
|
|
91
|
+
**GC state.** `GC.disable` does not stop an already started incremental cycle
|
|
92
|
+
from sweeping, so zeroed GC counters used to appear next to profiles full of
|
|
93
|
+
`gc_sweep`. It is off by default in the harness; set `GC_DISABLE=1`
|
|
94
|
+
deliberately and read `gc_disabled=` before quoting `gc_delta`.
|
|
95
|
+
|
|
96
|
+
## Two costs that are not the tokenizer
|
|
97
|
+
|
|
98
|
+
`embed` cannot start until Ruby has confirmed the `String` is valid UTF-8, and
|
|
99
|
+
for a `String` whose coderange has not been computed that is an O(bytes) scan
|
|
100
|
+
which runs before the prefix window is chosen. On a 3 MB freshly read document
|
|
101
|
+
it dominated: 442 µs against 71 µs for the same string once Ruby had cached the
|
|
102
|
+
answer. Reuse the `String`, call `valid_encoding?` outside the hot path, or pass
|
|
103
|
+
`validate_encoding: :prefix` to bound validation the same way tokenizing is
|
|
104
|
+
bounded — 78 µs, at the cost of not inspecting bytes past the window.
|
|
105
|
+
|
|
106
|
+
`StaticEmbeddings.verify` hashes the artifact and is deliberately not on the
|
|
107
|
+
load path. It streams in 1 MiB chunks: on a 62 MB model, 249 ms and 30 MB of
|
|
108
|
+
peak RSS. Reading the file whole and duplicating it to zero the checksum field
|
|
109
|
+
cost 433 ms and 280 MB.
|
|
110
|
+
|
|
111
|
+
## Cold start
|
|
112
|
+
|
|
113
|
+
`samples/cold_start.rb` measures `load`, first embed and `warmup!` in one shot,
|
|
114
|
+
because each of those happens exactly once: `load` walks the vocabulary hash and
|
|
115
|
+
both tries, and `warmup!` faults pages in. Drop the page cache first
|
|
116
|
+
(`sudo purge` on macOS, `echo 3 > /proc/sys/vm/drop_caches` on Linux) or the
|
|
117
|
+
output labels itself a warm start.
|
|
118
|
+
|
|
119
|
+
Every other sample runs a hot loop and measures steady state. `warmup_hot_path.rb`
|
|
120
|
+
in particular re-touches resident pages; its `mapped_range_gb_per_sec` is
|
|
121
|
+
address-range coverage, not memory bandwidth, because warmup reads one byte per
|
|
122
|
+
page.
|
|
123
|
+
|
|
124
|
+
## CI
|
|
56
125
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
on random row lookups, and the compiler already vectorises it.
|
|
126
|
+
The benchmark job is informational. Absolute thresholds on shared runners
|
|
127
|
+
produce noise, not signal; a regression gate is only meaningful against a
|
|
128
|
+
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
|