You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Copy file name to clipboardExpand all lines: Benchmark/README.md
+75-55Lines changed: 75 additions & 55 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,122 +2,141 @@
2
2
3
3
> Generated automatically by `Scripts/run.py`. Do not edit by hand.
4
4
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.
6
8
7
9
## Headline
8
10
9
11
Single alloc/free pair, 16 B:
10
12
11
13
| backend | time |
12
14
|---|---:|
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 |
19
24
20
25
## Timing
21
26
22
27
### Single alloc/free pair
23
28
24
29
One `alloc(size)` immediately followed by `free(ptr)`, repeated. Hot reuse — the same slot churns. Time per pair, lower is better.
| 8192 × 64 B |83.0|103.8|32.7|39.1|91.4|650.3| n/a | n/a| 168.6|
47
52
48
53
_Values in us._
49
54
50
55
### Alloc + write every byte + free
51
56
52
57
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.
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.
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.
_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._
105
112
106
113
## How to read
107
114
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:
109
116
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).
112
122
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.
114
124
115
125
## Reproduce
116
126
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.
Copy file name to clipboardExpand all lines: Benchmark/README.template.md
+24-9Lines changed: 24 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,9 @@
2
2
3
3
> Generated automatically by `Scripts/run.py`. Do not edit by hand.
4
4
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.
6
8
7
9
## Headline
8
10
@@ -48,25 +50,37 @@ Allocate N small (32 B) objects, then release them all. Arena does this as one O
48
50
49
51
## Fragmentation
50
52
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.
52
54
53
55
{{TABLE_FRAG}}
54
56
55
57
## How to read
56
58
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:
58
60
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).
61
66
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.
63
68
64
69
## Reproduce
65
70
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.
0 commit comments