Skip to content

Commit c0c5159

Browse files
bench: overhaul allocator benchmark
One column per MisraStdC allocator type instead of a smart-router picking the "correct" tool per workload. Each allocator runs only the workloads its contract honestly supports; everything else reads n/a. The reader decides which column is the right comparison for their workload, not the harness. Columns: glibc / jemalloc / mimalloc / tcmalloc (process malloc) misra-heap (HeapAllocator) misra-slab (SlabAllocator) misra-arena (ArenaAllocator) misra-page (PageAllocator) misra-budget (BudgetAllocator) DebugAllocator is intentionally not benchmarked -- it's a diagnostic wrapper, not a production allocator; the column would measure diagnostic overhead, not allocator design. Two builds, one bench script: build -Dalloc_stats=false -> timing rows (no per-call stats accounting overhead) build-frag -Dalloc_stats=true -> fragmentation rows (misra columns can report live bytes via AllocatorBytesInUse) Scripts/run.py picks the matching build per row group and merges the JSON. The reproduce section documents both setups. Fragmentation table cells now show "live / footprint" per backend, both sourced from the backend's OWN introspection API: glibc mallinfo2().uordblks / .arena+.hblkhd jemalloc stats.allocated / stats.mapped mimalloc no working API -> n/a / n/a tcmalloc generic.current_allocated_bytes / generic.heap_size misra AllocatorBytesInUse(a) / AllocatorFootprintBytes(a) No harness-side sum_live() bookkeeping covers for a missing API. mimalloc 3.3.0's stats are broken; the column honestly reads n/a instead of a synthesised fallback. Removed the std::vector<Alloc> tracking; collapsed Alloc to plain void *. Convention pass against CODING-CONVENTIONS.md: - dropped redundant g_heap_typed sentinel; matches g_<x>_live pattern in arena/page/budget - inlined budget's structural-no-op tear_current() - dropped unused <Misra/Std/Allocator/Page.h> includes from heap.c and arena.c - BENCH_MISRA_VARIANT_NAME -> BENCH_BACKEND_NAME everywhere; the MISRA_ prefix is reserved for include guards - trimmed multi-paragraph docstring file headers - documented why the bench backends use process-singleton globals (Google Benchmark's BENCHMARK signature offers no user-data slot) so a reader doesn't model new code on that shape Library code, tests, and the typed-allocator API are untouched. Tested: Linux x86_64 / gcc 15.2.0 / 105/105 tests pass macOS aarch64 / Apple clang 17 / 100/100 tests pass
1 parent a1ccb4d commit c0c5159

14 files changed

Lines changed: 799 additions & 638 deletions

Benchmark/README.md

Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,122 +2,141 @@
22

33
> Generated automatically by `Scripts/run.py`. Do not edit by hand.
44
5-
Compares MisraStdC's `HeapAllocator` and `SlabAllocator` against four production allocators: glibc, jemalloc, mimalloc, tcmalloc.
5+
One column per allocator. The libc-shape columns (`glibc`, `jemalloc`, `mimalloc`, `tcmalloc`) sit next to one column for each MisraStdC allocator type (`misra-heap`, `misra-slab`, `misra-arena`, `misra-page`, `misra-budget`). Rows that fall outside an allocator's contract -- mixed sizes for a fixed-slot allocator, sub-page requests for `PageAllocator`, many-live-allocs for `ArenaAllocator`, and so on -- read `n/a`. The benchmark does not decide which allocator is the "right tool" for a workload; the reader does.
6+
7+
`DebugAllocator` is intentionally not benchmarked. It's a diagnostic wrapper (leak / canary / trace tracking) for tests and fuzz, not a production allocator -- the column would measure diagnostic overhead, not allocator design.
68

79
## Headline
810

911
Single alloc/free pair, 16 B:
1012

1113
| backend | time |
1214
|---|---:|
13-
| tcmalloc | 3.4 ns |
14-
| glibc | 7.3 ns |
15-
| jemalloc | 3.5 ns |
16-
| mimalloc | 3.0 ns |
17-
| misra (Heap) | 14.8 ns |
18-
| misra-correct (Slab) | 14.0 ns |
15+
| glibc | 5.3 ns |
16+
| jemalloc | 3.7 ns |
17+
| mimalloc | 3.0 ns |
18+
| tcmalloc | 3.7 ns |
19+
| misra-heap | 13.3 ns |
20+
| misra-slab | 8.3 ns |
21+
| misra-arena | 6.9 ns |
22+
| misra-page | n/a |
23+
| misra-budget | 11.2 ns |
1924

2025
## Timing
2126

2227
### Single alloc/free pair
2328

2429
One `alloc(size)` immediately followed by `free(ptr)`, repeated. Hot reuse — the same slot churns. Time per pair, lower is better.
2530

26-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
27-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
28-
| 16 B | 7.3 | 3.5 | 3.0 | 3.4 | 14.8 | 14.0 | 12.0 | n/a |
29-
| 64 B | 7.2 | 3.6 | 3.2 | 3.5 | 15.0 | 14.0 | 12.2 | n/a |
30-
| 256 B | 7.2 | 3.9 | 13.7 | 3.4 | 15.0 | 13.9 | 12.4 | n/a |
31-
| 1 KiB | 19.3 | 5.5 | 15.1 | 3.4 | 15.0 | 13.9 | 12.2 | n/a |
32-
| 4 KiB | 15.1 | 10.4 | 10.6 | 3.6 | 19.1 | 13.9 | 12.2 | 19.8 |
33-
| 16 KiB | 16.2 | 18.8 | 19.6 | 3.6 | 19.2 | 19.3 | 12.3 | 19.7 |
34-
| 64 KiB | 17.2 | 267.0 | 19.6 | 3.6 | 19.2 | 19.2 | 12.3 | 20.0 |
31+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
32+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
33+
| 16 B | 5.3 | 3.7 | 3.0 | 3.7 | 13.3 | 8.3 | 6.9 | n/a | 11.2 |
34+
| 64 B | 5.1 | 3.8 | 3.2 | 3.7 | 13.0 | 8.1 | 6.9 | n/a | 11.2 |
35+
| 256 B | 5.1 | 4.3 | 13.9 | 3.6 | 13.1 | 8.1 | 6.9 | n/a | 11.2 |
36+
| 1 KiB | 5.0 | 5.7 | 15.3 | 3.5 | 13.0 | 8.1 | 6.9 | n/a | 11.2 |
37+
| 4 KiB | 16.8 | 10.4 | 11.0 | 3.6 | 17.6 | 8.1 | 6.9 | 11.5 | 11.2 |
38+
| 16 KiB | 17.0 | 20.6 | 11.0 | 3.6 | 17.6 | n/a | 6.9 | 11.5 | 11.2 |
39+
| 64 KiB | 17.1 | 168.4 | 19.9 | 3.7 | 17.6 | n/a | 6.9 | 11.6 | 10.0 |
3540

3641
_Values in ns._
3742

3843
### Batch alloc-N-then-free-N
3944

4045
`N` allocs of fixed size, then `N` frees, then repeat. Holds `N` allocations live at peak. Time per batch, lower is better.
4146

42-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
43-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
44-
| 128 × 64 B | 1.4 | 0.5 | 0.4 | 0.5 | 1.6 | 1.9 | n/a | n/a |
45-
| 1024 × 64 B | 10.9 | 10.8 | 3.6 | 3.9 | 12.4 | 18.9 | n/a | n/a |
46-
| 8192 × 64 B | 87.3 | 88.7 | 33.2 | 38.9 | 99.6 | 399.5 | n/a | n/a |
47+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
48+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
49+
| 128 × 64 B | 1.5 | 0.5 | 0.4 | 0.5 | 1.4 | 1.2 | n/a | n/a | 1.5 |
50+
| 1024 × 64 B | 10.7 | 11.9 | 3.6 | 3.9 | 11.5 | 21.3 | n/a | n/a | 12.8 |
51+
| 8192 × 64 B | 83.0 | 103.8 | 32.7 | 39.1 | 91.4 | 650.3 | n/a | n/a | 168.6 |
4752

4853
_Values in us._
4954

5055
### Alloc + write every byte + free
5156

5257
Same shape as the pair test but writes every byte of the allocation before freeing. Catches page-fault cost that pure alloc/free hides. Time per item, lower is better.
5358

54-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
55-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
56-
| 64 B | 7.8 | 4.4 | 3.2 | 4.4 | 15.0 | 16.6 | 14.7 | n/a |
57-
| 4 KiB | 32.1 | 23.3 | 24.0 | 18.9 | 38.1 | 33.6 | 55.3 | 38.7 |
58-
| 64 KiB | 955.9 | 1061.8 | 863.4 | 840.3 | 852.3 | 850.4 | 916.3 | 851.1 |
59+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
60+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
61+
| 64 B | 6.0 | 4.7 | 3.3 | 4.4 | 13.7 | 10.6 | 9.4 | n/a | 13.3 |
62+
| 4 KiB | 41.2 | 23.9 | 24.7 | 19.1 | 36.0 | 26.6 | 50.7 | 30.7 | 31.0 |
63+
| 64 KiB | 861.3 | 1063.8 | 870.3 | 840.3 | 848.3 | n/a | 907.5 | 843.9 | 841.6 |
5964

6065
_Values in ns._
6166

6267
### Mixed-size Pareto
6368

6469
512-allocation batch with sizes drawn from a Pareto(α=1.16, xm=24) distribution capped at 256 KiB. Time per batch, lower is better.
6570

66-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
67-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
68-
| Pareto(1.16, 24) | 30.5 | 11.0 | 10.8 | 11.4 | 24.9 | 25.3 | n/a | n/a |
71+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
72+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
73+
| Pareto(1.16, 24) | 27.8 | 11.3 | 11.1 | 11.6 | 24.6 | n/a | n/a | n/a | n/a |
6974

7075
_Values in us._
7176

7277
### Realloc growth
7378

7479
Geometric realloc ladder from 8 B up to 1 MiB. Time per full ladder, lower is better.
7580

76-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
77-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
78-
| 8 B → 1 MiB | 2851.4 | 3883.1 | 18015.4 | 15131.8 | 16677.8 | 15854.8 | 135.0 | n/a |
81+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
82+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
83+
| 8 B → 1 MiB | 5721.1 | 5296.2 | 17982.3 | 15251.9 | 16539.7 | n/a | 93.3 | n/a | n/a |
7984

8085
_Values in ns._
8186

8287
### Arena bump + bulk reset
8388

8489
Allocate N small (32 B) objects, then release them all. Arena does this as one O(1) reset; every other backend has to free each pointer individually. Time per batch, lower is better.
8590

86-
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
87-
|---|---:|---:|---:|---:|---:|---:|---:|---:|
88-
| 128 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 0.8 | n/a |
89-
| 1024 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 6.5 | n/a |
90-
| 8192 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 52.4 | n/a |
91+
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra-heap | misra-slab | misra-arena | misra-page | misra-budget |
92+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
93+
| 128 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 0.5 | n/a | n/a |
94+
| 1024 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 3.9 | n/a | n/a |
95+
| 8192 × 32 B | n/a | n/a | n/a | n/a | n/a | n/a | 31.0 | n/a | n/a |
9196

9297
_Values in us._
9398

9499
## Fragmentation
95100

96-
Each allocator's own introspection API reports committed bytes after the workload runs. Lower committed-bytes for the same live-bytes is better.
101+
Each cell shows `live / footprint` for one backend, both numbers as the backend's OWN introspection API reports them. `live` is what's currently outstanding to the user; `footprint` is what the allocator pulled from the OS. Lower footprint at the same live = less fragmentation. `n/a` means the backend's stats API didn't expose the number -- the harness does not synthesize a value to fill the cell.
97102

98-
| benchmark | live MB | glibc MB | jemalloc MB | mimalloc MB | tcmalloc MB | misra MB | misra-correct MB | misra-arena MB | misra-page MB |
103+
| benchmark | glibc (live / fp) | jemalloc (live / fp) | mimalloc (live / fp) | tcmalloc (live / fp) | misra-heap (live / fp) | misra-slab (live / fp) | misra-arena (live / fp) | misra-page (live / fp) | misra-budget (live / fp) |
99104
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
100-
| Checkerboard (4 K small) | 0.6 | 2.2 | 9.1 | n/a | 7.0 | 0.8 | 0.8 | n/a | n/a |
101-
| Checkerboard (16 K small) | 2.5 | 4.1 | 11.7 | n/a | 7.0 | 3.2 | 3.1 | n/a | n/a |
102-
| Checkerboard (64 K small) | 10.0 | 15.3 | 22.0 | n/a | 16.0 | 12.5 | 12.5 | n/a | n/a |
103-
| Lifetime mix | 4.0 | 5.0 | 20.1 | n/a | 16.0 | 4.6 | 4.7 | n/a | n/a |
104-
| Page overhang | 18.2 | 20.5 | 32.8 | n/a | 29.0 | 38.4 | 38.4 | n/a | n/a |
105+
| Checkerboard (4 K small) | 0.8 / 1.1 | 0.8 / 9.1 | n/a | 0.8 / 2.0 | 0.6 / 0.8 | n/a | n/a | n/a | n/a |
106+
| Checkerboard (16 K small) | 3.0 / 3.7 | 2.8 / 11.7 | n/a | 2.8 / 4.0 | 2.5 / 3.1 | n/a | n/a | n/a | n/a |
107+
| Checkerboard (64 K small) | 11.8 / 14.4 | 10.9 / 21.1 | n/a | 10.8 / 14.0 | 10.0 / 12.5 | n/a | n/a | n/a | n/a |
108+
| Lifetime mix | 4.4 / 4.7 | 4.3 / 14.7 | n/a | 4.1 / 14.0 | 3.9 / 4.6 | n/a | n/a | n/a | n/a |
109+
| Page overhang | 19.7 / 19.8 | 23.5 / 31.7 | n/a | 21.3 / 27.0 | 36.3 / 38.4 | n/a | n/a | n/a | n/a |
110+
111+
_Per backend: live (allocator's reported outstanding bytes) / footprint (committed bytes from OS). MB. Lower footprint at the same live = less fragmentation. `n/a` means that backend's stats API didn't expose the number._
105112

106113
## How to read
107114

108-
MisraStdC's typed-allocator family shows up as several columns:
115+
Each `misra-*` column is one MisraStdC allocator type, not a choice between "fast" and "correct". The libc-shape columns are general-purpose by design and run every row; the MisraStdC columns vary by contract:
109116

110-
- **`misra` / `misra-correct`**`HeapAllocator` everywhere vs the right tool per workload (`SlabAllocator(slot_size)` for fixed-size benches, `HeapAllocator` for mixed). The latter is what a real MisraStdC caller would do.
111-
- **`misra-arena` / `misra-page`** — specialised backends (bump+bulk-reset, mmap-per-alloc). `n/a` rows mean the workload doesn't fit the backend's contract (e.g. arena can't handle many-live-allocs; page wastes whole pages on sub-page requests).
117+
- **`misra-heap`**`HeapAllocator`. General-purpose binned heap; runs every workload. The baseline for an apples-to-apples comparison against glibc / jemalloc / mimalloc / tcmalloc.
118+
- **`misra-slab`**`SlabAllocator(slot)`. Fixed slot, power of two in [16, 4096]. Runs the fixed-size rows whose slot fits the cap (`AllocFreePair/16..4096`, all `BatchAllocFree`, `AllocTouchFree/64,4096`). `n/a` everywhere else: super-page slots, mixed sizes, and any realloc that crosses the slot.
119+
- **`misra-arena`**`ArenaAllocator`. Bump-pointer with single-deep LIFO rewind on free and one bulk O(1) `Reset`. Runs the pair-style rows (last-ptr rewind keeps growth flat), `ReallocGrow` (last-ptr remap fast-path), and `ArenaBumpReset`. `n/a` on many-live-allocs and mixed sizes -- the workload would grow unbounded.
120+
- **`misra-page`**`PageAllocator`. One mmap per alloc, page-rounded. Restricted to page-aligned sizes (4 KiB and up) so the row reflects mmap dispatch and not rounding waste from sub-page requests. `n/a` everywhere else.
121+
- **`misra-budget`**`BudgetAllocator`. Caller-buffer fixed-budget pool, no growth. Runs the fixed-size rows that fit the 16 MiB backing buffer. `n/a` on mixed sizes and realloc (one slot for life).
112122

113-
Where tcmalloc beats `misra-correct` on small AllocFreePair, the gap is MisraStdC's structural safety (double-free, misalignment, magic-tag dispatch, cross-class invariants) that production allocators skip even in their fast paths.
123+
`n/a` is a deliberate signal that the row falls outside the allocator's contract. The reader picks the workload-matched column instead of relying on the benchmark to pick one for them.
114124

115125
## Reproduce
116126

127+
Two builds: timing rows want `-Dalloc_stats=false` so per-allocator counters don't bias the hot path; fragmentation rows want `-Dalloc_stats=true` so the misra columns can report live bytes. `run.py` runs each row group against the matching binary.
128+
117129
```sh
118-
meson setup build -Dbenchmark=true -Dbuildtype=release -Doptimization=3
119-
ninja -C build
130+
# perf build (timing rows)
131+
meson setup build -Dbenchmark=true -Dbuildtype=release -Doptimization=3 -Dalloc_stats=false
132+
ninja -C build
133+
134+
# frag build (fragmentation rows)
135+
meson setup build-frag -Dbenchmark=true -Dbuildtype=release -Doptimization=3 -Dalloc_stats=true
136+
ninja -C build-frag
137+
120138
python3 Benchmark/Scripts/run.py build
139+
# --frag-builddir defaults to <perf-builddir>-frag (i.e. "build-frag" here).
121140
```
122141

123142
Regenerates this file with measurements from the host.
@@ -126,10 +145,11 @@ Regenerates this file with measurements from the host.
126145

127146
| | |
128147
|---|---|
129-
| timestamp | 2026-05-29 12:59:55 UTC |
130-
| git rev | e837a85ac468 (feat/heap-class-shrink-xl-retention) |
148+
| timestamp | 2026-06-04 08:41:34 UTC |
149+
| git rev | e55e50d6f84e (master) |
131150
| host CPU | Intel(R) Core(TM) Ultra 7 165U |
132-
| kernel | Linux 6.18.28 |
133-
| compiler | gcc 14.3.0 |
134-
| build | buildtype=release optimization=3 b_lto=False b_sanitize=[] alloc_debug=True |
151+
| kernel | Linux 6.18.32 |
152+
| compiler | gcc 15.2.0 |
153+
| build (perf) | buildtype=release optimization=3 b_sanitize=[] b_lto=False alloc_stats=False alloc_debug=True |
154+
| build (frag) | buildtype=release optimization=3 b_sanitize=[] b_lto=False alloc_stats=True alloc_debug=True |
135155
| reps | 10 per row (median reported) |

Benchmark/README.template.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
> Generated automatically by `Scripts/run.py`. Do not edit by hand.
44
5-
Compares MisraStdC's `HeapAllocator` and `SlabAllocator` against four production allocators: glibc, jemalloc, mimalloc, tcmalloc.
5+
One column per allocator. The libc-shape columns (`glibc`, `jemalloc`, `mimalloc`, `tcmalloc`) sit next to one column for each MisraStdC allocator type (`misra-heap`, `misra-slab`, `misra-arena`, `misra-page`, `misra-budget`). Rows that fall outside an allocator's contract -- mixed sizes for a fixed-slot allocator, sub-page requests for `PageAllocator`, many-live-allocs for `ArenaAllocator`, and so on -- read `n/a`. The benchmark does not decide which allocator is the "right tool" for a workload; the reader does.
6+
7+
`DebugAllocator` is intentionally not benchmarked. It's a diagnostic wrapper (leak / canary / trace tracking) for tests and fuzz, not a production allocator -- the column would measure diagnostic overhead, not allocator design.
68

79
## Headline
810

@@ -48,25 +50,37 @@ Allocate N small (32 B) objects, then release them all. Arena does this as one O
4850

4951
## Fragmentation
5052

51-
Each allocator's own introspection API reports committed bytes after the workload runs. Lower committed-bytes for the same live-bytes is better.
53+
Each cell shows `live / footprint` for one backend, both numbers as the backend's OWN introspection API reports them. `live` is what's currently outstanding to the user; `footprint` is what the allocator pulled from the OS. Lower footprint at the same live = less fragmentation. `n/a` means the backend's stats API didn't expose the number -- the harness does not synthesize a value to fill the cell.
5254

5355
{{TABLE_FRAG}}
5456

5557
## How to read
5658

57-
MisraStdC's typed-allocator family shows up as several columns:
59+
Each `misra-*` column is one MisraStdC allocator type, not a choice between "fast" and "correct". The libc-shape columns are general-purpose by design and run every row; the MisraStdC columns vary by contract:
5860

59-
- **`misra` / `misra-correct`**`HeapAllocator` everywhere vs the right tool per workload (`SlabAllocator(slot_size)` for fixed-size benches, `HeapAllocator` for mixed). The latter is what a real MisraStdC caller would do.
60-
- **`misra-arena` / `misra-page`** — specialised backends (bump+bulk-reset, mmap-per-alloc). `n/a` rows mean the workload doesn't fit the backend's contract (e.g. arena can't handle many-live-allocs; page wastes whole pages on sub-page requests).
61+
- **`misra-heap`**`HeapAllocator`. General-purpose binned heap; runs every workload. The baseline for an apples-to-apples comparison against glibc / jemalloc / mimalloc / tcmalloc.
62+
- **`misra-slab`**`SlabAllocator(slot)`. Fixed slot, power of two in [16, 4096]. Runs the fixed-size rows whose slot fits the cap (`AllocFreePair/16..4096`, all `BatchAllocFree`, `AllocTouchFree/64,4096`). `n/a` everywhere else: super-page slots, mixed sizes, and any realloc that crosses the slot.
63+
- **`misra-arena`**`ArenaAllocator`. Bump-pointer with single-deep LIFO rewind on free and one bulk O(1) `Reset`. Runs the pair-style rows (last-ptr rewind keeps growth flat), `ReallocGrow` (last-ptr remap fast-path), and `ArenaBumpReset`. `n/a` on many-live-allocs and mixed sizes -- the workload would grow unbounded.
64+
- **`misra-page`**`PageAllocator`. One mmap per alloc, page-rounded. Restricted to page-aligned sizes (4 KiB and up) so the row reflects mmap dispatch and not rounding waste from sub-page requests. `n/a` everywhere else.
65+
- **`misra-budget`**`BudgetAllocator`. Caller-buffer fixed-budget pool, no growth. Runs the fixed-size rows that fit the 16 MiB backing buffer. `n/a` on mixed sizes and realloc (one slot for life).
6166

62-
Where tcmalloc beats `misra-correct` on small AllocFreePair, the gap is MisraStdC's structural safety (double-free, misalignment, magic-tag dispatch, cross-class invariants) that production allocators skip even in their fast paths.
67+
`n/a` is a deliberate signal that the row falls outside the allocator's contract. The reader picks the workload-matched column instead of relying on the benchmark to pick one for them.
6368

6469
## Reproduce
6570

71+
Two builds: timing rows want `-Dalloc_stats=false` so per-allocator counters don't bias the hot path; fragmentation rows want `-Dalloc_stats=true` so the misra columns can report live bytes. `run.py` runs each row group against the matching binary.
72+
6673
```sh
67-
meson setup build -Dbenchmark=true -Dbuildtype=release -Doptimization=3
68-
ninja -C build
74+
# perf build (timing rows)
75+
meson setup build -Dbenchmark=true -Dbuildtype=release -Doptimization=3 -Dalloc_stats=false
76+
ninja -C build
77+
78+
# frag build (fragmentation rows)
79+
meson setup build-frag -Dbenchmark=true -Dbuildtype=release -Doptimization=3 -Dalloc_stats=true
80+
ninja -C build-frag
81+
6982
python3 Benchmark/Scripts/run.py build
83+
# --frag-builddir defaults to <perf-builddir>-frag (i.e. "build-frag" here).
7084
```
7185

7286
Regenerates this file with measurements from the host.
@@ -80,5 +94,6 @@ Regenerates this file with measurements from the host.
8094
| host CPU | {{CPU}} |
8195
| kernel | {{KERNEL}} |
8296
| compiler | {{COMPILER}} |
83-
| build | {{BUILD_OPTIONS}} |
97+
| build (perf) | {{BUILD_OPTIONS}} |
98+
| build (frag) | {{BUILD_OPTIONS_FRAG}} |
8499
| reps | {{REPS}} per row (median reported) |

0 commit comments

Comments
 (0)