Skip to content

Commit c855e0e

Browse files
audit-round-1 fixes: drop MISRA_ on bench-local macro; tighten misc
Applied findings from the audit-loop round 1: Allocator_misra_correct.c: MISRA_BENCH_SLAB_MAX_SLOT -> BENCH_SLAB_MAX_SLOT (MISRA_ prefix reserved for FEATURE_/MAGIC/guards; bench-local cap is none). bench_realloc(MODE_SLAB) comment: rewritten to admit the soft NULL is intentional (avoid aborting the whole bench process on a routing mismatch), instead of the contradictory 'Fail loudly'. enum BenchMode -> bench_mode (file-local enum; convention uses snake_case for private types). Allocator_misra.c, Allocator_misra_correct.c: Drop dead <stdlib.h> include. Allocator_misra_arena.c, Allocator_misra_page.c: int g_*_live -> bool. Allocator.h: Document the reset asymmetry in bench_use_fixed_size/general -- misra backends tear down and reinit between benches; libc-shape backends keep process malloc state. Median absorbs the cold iteration; not advantageous to misra. Scripts/run.py (render_frag_table): Footprint = 0.0 MB rendered as 'n/a' (mimalloc 3.3.0's mi_stats_get is broken; old rendering flattered mimalloc by making it look like it allocated nothing). Source/Misra/Std/Allocator/Slab.c (alloc loop): Explain why the pre-redesign 'bitmap word reads back its bit already set' LOG_FATAL was dropped -- it was a self-consistency probe (defending against a corrupt-with-itself bitmap word, not against caller bugs), and the new contiguous-bitmaps layout makes it a defense against CTZ64 itself being broken. Free's double-free check still defends against the real-world failure mode (releasing a slot twice from user code). Carried forward without change: the alloc-side check is permanently removed -- not re-added under FEATURE_HEAP_VALIDATE_FULL -- because it tested platform code, not caller code. All 104 MisraStdC tests still pass on Linux.
1 parent 85f73f4 commit c855e0e

8 files changed

Lines changed: 75 additions & 51 deletions

File tree

Benchmark/README.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ Single alloc/free pair, 16 B:
1111
| backend | time |
1212
|---|---:|
1313
| tcmalloc | 3.6 ns |
14-
| glibc | 4.6 ns |
15-
| jemalloc | 3.5 ns |
14+
| glibc | 4.9 ns |
15+
| jemalloc | 3.6 ns |
1616
| mimalloc | 7.4 ns |
1717
| misra (Heap only) | 22.7 ns |
18-
| misra-correct (Slab) | 15.3 ns |
18+
| misra-correct (Slab) | 15.0 ns |
1919

2020
## Timing
2121

@@ -25,13 +25,13 @@ One `alloc(size)` immediately followed by `free(ptr)`, repeated. Hot reuse — t
2525

2626
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
2727
|---|---:|---:|---:|---:|---:|---:|---:|---:|
28-
| 16 B | 4.6 | 3.5 | 7.4 | 3.6 | 22.7 | 15.3 | 11.5 | 1610.5 |
29-
| 64 B | 4.6 | 3.7 | 7.5 | 3.6 | 20.8 | 15.0 | 11.5 | 1614.0 |
30-
| 256 B | 5.0 | 4.1 | 9.4 | 3.6 | 20.6 | 15.0 | 11.6 | 1608.0 |
31-
| 1 KiB | 4.9 | 5.8 | 11.0 | 3.6 | 20.0 | 15.0 | 11.8 | 1641.1 |
32-
| 4 KiB | 16.8 | 11.0 | 12.5 | 3.8 | 1636.8 | 15.1 | 11.8 | 1636.1 |
33-
| 16 KiB | 16.1 | 18.9 | 20.9 | 3.8 | 1250.4 | 1608.0 | 11.9 | 1203.0 |
34-
| 64 KiB | 16.1 | 270.8 | 21.0 | 3.9 | 1248.9 | 1621.2 | 11.9 | 1198.1 |
28+
| 16 B | 4.9 | 3.6 | 7.4 | 3.6 | 22.7 | 15.0 | 11.7 | 1620.7 |
29+
| 64 B | 5.1 | 3.7 | 7.5 | 3.6 | 21.3 | 14.9 | 11.6 | 1600.6 |
30+
| 256 B | 5.2 | 4.1 | 9.5 | 3.5 | 21.2 | 15.0 | 11.6 | 1625.0 |
31+
| 1 KiB | 5.1 | 5.8 | 11.0 | 3.5 | 20.7 | 15.0 | 11.6 | 1629.9 |
32+
| 4 KiB | 17.4 | 11.0 | 12.4 | 3.8 | 1641.7 | 15.0 | 11.9 | 1611.2 |
33+
| 16 KiB | 18.0 | 18.6 | 20.8 | 3.8 | 1233.8 | 1635.3 | 11.8 | 1214.2 |
34+
| 64 KiB | 17.5 | 270.5 | 20.5 | 3.8 | 1243.2 | 1659.1 | 11.5 | 1232.6 |
3535

3636
_Values in ns._
3737

@@ -41,9 +41,9 @@ _Values in ns._
4141

4242
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
4343
|---|---:|---:|---:|---:|---:|---:|---:|---:|
44-
| 128 × 64 B | 1.0 | 1.1 | 0.7 | 0.5 | 3.0 | 2.0 | n/a | 153.9 |
45-
| 1024 × 64 B | 8.5 | 11.7 | 6.0 | 4.1 | 32.1 | 20.5 | n/a | 1358.0 |
46-
| 8192 × 64 B | 66.9 | 95.0 | 51.4 | 40.3 | 531.6 | 437.9 | n/a | 17750.9 |
44+
| 128 × 64 B | 1.2 | 1.0 | 0.7 | 0.5 | 3.0 | 2.0 | n/a | 154.2 |
45+
| 1024 × 64 B | 9.0 | 11.6 | 5.5 | 4.0 | 31.9 | 20.5 | n/a | 1351.1 |
46+
| 8192 × 64 B | 81.3 | 94.2 | 51.2 | 39.9 | 537.7 | 437.0 | n/a | 17811.5 |
4747

4848
_Values in us._
4949

@@ -53,9 +53,9 @@ Same shape as the pair test but writes every byte of the allocation before freei
5353

5454
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
5555
|---|---:|---:|---:|---:|---:|---:|---:|---:|
56-
| 64 B | 4.9 | 4.7 | 8.5 | 4.5 | 33.0 | 18.2 | 14.6 | 2026.2 |
57-
| 4 KiB | 30.0 | 25.1 | 27.7 | 20.2 | 2197.8 | 48.5 | 67.8 | 2094.7 |
58-
| 64 KiB | 856.5 | 1060.5 | 866.9 | 841.7 | 13994.6 | 14209.5 | 1136.3 | 13955.8 |
56+
| 64 B | 8.2 | 4.8 | 8.5 | 4.4 | 32.2 | 17.7 | 14.7 | 2006.2 |
57+
| 4 KiB | 30.0 | 25.1 | 27.6 | 20.2 | 2177.9 | 48.3 | 67.7 | 2072.5 |
58+
| 64 KiB | 858.1 | 1067.3 | 862.4 | 841.2 | 13995.5 | 14096.2 | 1148.8 | 13946.5 |
5959

6060
_Values in ns._
6161

@@ -65,7 +65,7 @@ _Values in ns._
6565

6666
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
6767
|---|---:|---:|---:|---:|---:|---:|---:|---:|
68-
| Pareto(1.16, 24) | 28.8 | 13.1 | 14.0 | 12.2 | 45.8 | 44.1 | n/a | 629.8 |
68+
| Pareto(1.16, 24) | 29.0 | 11.9 | 14.0 | 13.0 | 45.2 | 44.8 | n/a | 623.8 |
6969

7070
_Values in us._
7171

@@ -75,7 +75,7 @@ Geometric realloc ladder from 8 B up to 1 MiB. Time per full ladder, lower is be
7575

7676
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
7777
|---|---:|---:|---:|---:|---:|---:|---:|---:|
78-
| 8 B → 1 MiB | 4771.7 | 11580.5 | 16933.1 | 15135.0 | 268603.8 | 258694.1 | 139.9 | 258397.1 |
78+
| 8 B → 1 MiB | 5735.8 | 4313.3 | 17014.8 | 15119.2 | 261663.3 | 260083.2 | 134.8 | 260357.2 |
7979

8080
_Values in ns._
8181

@@ -85,9 +85,9 @@ Allocate N small (32 B) objects, then release them all. Arena does this as one O
8585

8686
| benchmark | glibc | jemalloc | mimalloc | tcmalloc | misra | misra-correct | misra-arena | misra-page |
8787
|---|---:|---:|---:|---:|---:|---:|---:|---:|
88-
| 128 × 32 B | 1.0 | 0.5 | 0.7 | 0.5 | 4.2 | 3.7 | 0.8 | 154.0 |
89-
| 1024 × 32 B | 8.4 | 9.8 | 5.3 | 4.0 | 41.9 | 39.6 | 6.5 | 1222.4 |
90-
| 8192 × 32 B | 68.5 | 86.2 | 47.2 | 31.8 | 574.3 | 562.4 | 52.4 | 15842.2 |
88+
| 128 × 32 B | 1.0 | 0.5 | 0.7 | 0.5 | 4.0 | 3.7 | 0.8 | 152.0 |
89+
| 1024 × 32 B | 8.0 | 9.6 | 5.1 | 3.9 | 40.8 | 39.0 | 6.4 | 1217.0 |
90+
| 8192 × 32 B | 67.0 | 85.4 | 47.7 | 31.8 | 569.5 | 564.7 | 52.6 | 15765.0 |
9191

9292
_Values in us._
9393

@@ -97,11 +97,11 @@ Each allocator's own introspection API reports committed bytes after the workloa
9797

9898
| benchmark | live MB | glibc MB | jemalloc MB | mimalloc MB | tcmalloc MB | misra MB | misra-correct MB | misra-arena MB | misra-page MB |
9999
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
100-
| Checkerboard (4 K small) | 0.6 | 1.3 | 9.1 | 0.0 | 7.0 | 3.1 | 2.5 | n/a | 16.0 |
101-
| Checkerboard (16 K small) | 2.5 | 4.1 | 11.7 | 0.0 | 7.0 | 10.1 | 10.0 | n/a | 64.0 |
102-
| Checkerboard (64 K small) | 10.0 | 15.3 | 21.9 | 0.0 | 16.0 | 40.2 | 40.2 | n/a | 256.0 |
103-
| Lifetime mix | 4.0 | 5.0 | 20.1 | 0.0 | 16.0 | 45.8 | 45.8 | n/a | 64.0 |
104-
| Page overhang | 18.2 | 20.5 | 34.0 | 0.0 | 29.0 | 77.0 | 77.0 | n/a | 256.0 |
100+
| Checkerboard (4 K small) | 0.6 | 2.2 | 9.2 | n/a | 7.0 | 3.1 | 2.5 | n/a | 16.0 |
101+
| Checkerboard (16 K small) | 2.5 | 4.1 | 11.7 | n/a | 7.0 | 10.1 | 10.0 | n/a | 64.0 |
102+
| Checkerboard (64 K small) | 10.0 | 15.3 | 22.0 | n/a | 16.0 | 40.2 | 40.2 | n/a | 256.0 |
103+
| Lifetime mix | 4.0 | 5.0 | 20.4 | n/a | 16.0 | 45.8 | 45.8 | n/a | 64.0 |
104+
| Page overhang | 18.2 | 20.4 | 36.4 | n/a | 29.0 | 77.0 | 77.0 | n/a | 256.0 |
105105

106106
## How to read
107107

@@ -126,8 +126,8 @@ Regenerates this file with measurements from the host.
126126

127127
| | |
128128
|---|---|
129-
| timestamp | 2026-05-20 23:09:43 UTC |
130-
| git rev | 4ed3d620fd4f (perf/slab-bitmap-redesign) |
129+
| timestamp | 2026-05-20 23:26:13 UTC |
130+
| git rev | 85f73f49377d (perf/slab-bitmap-redesign) |
131131
| host CPU | Intel(R) Core(TM) Ultra 7 165U |
132132
| kernel | Linux 6.18.25 |
133133
| compiler | gcc 15.2.0 |

Benchmark/Scripts/run.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,16 @@ def render_frag_table(data: dict) -> str:
217217
cells.append("n/a")
218218
continue
219219
fp = get_counter(j, bench_name, "footprint_MB")
220-
cells.append(f"{fp:.1f}" if fp is not None else "n/a")
220+
# Backends without a working stats API (mimalloc 3.3.0's
221+
# mi_stats_get is broken; see the long comment in
222+
# Allocator_libc.c::bench_footprint_bytes) report 0.0.
223+
# That would flatter them in a fragmentation table -- they
224+
# didn't allocate zero bytes, we just can't read what they
225+
# did. Render those as n/a, not 0.0.
226+
if fp is None or fp == 0.0:
227+
cells.append("n/a")
228+
else:
229+
cells.append(f"{fp:.1f}")
221230
out.append("| " + " | ".join(cells) + " |")
222231
return "\n".join(out)
223232

Benchmark/Source/Allocator.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,17 @@ void bench_teardown(void);
4343
// workload shape.
4444
//
4545
// Outstanding allocations from before the call are NOT preserved; the
46-
// previous allocator's state is destroyed. Call before the first
47-
// bench_alloc of a benchmark; call bench_use_general before any
48-
// benchmark that uses multiple sizes or realloc.
46+
// previous allocator's state is destroyed and a fresh one constructed.
47+
// This is an asymmetry vs the libc-shape backends, which keep
48+
// per-process malloc state across benchmark runs. The asymmetry does
49+
// NOT favour the misra backends: a fresh slab/heap has to fault its
50+
// first page on iteration 1, and Google Benchmark's auto-scaled
51+
// iteration count (typically 10^6+ to fill --benchmark_min_time)
52+
// absorbs that cold-start blip in the median.
53+
//
54+
// Call bench_use_fixed_size before the first bench_alloc of a
55+
// fixed-size benchmark; call bench_use_general before any benchmark
56+
// that uses multiple sizes or realloc.
4957
void bench_use_fixed_size(size_t slot);
5058
void bench_use_general(void);
5159

Benchmark/Source/Allocator_misra.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
#include <Misra/Std/Allocator/Heap.h>
2424
#include <Misra/Std/Allocator/Page.h>
2525

26-
#include <stdlib.h>
27-
2826
static HeapAllocator g_heap;
2927
static Allocator *g_alloc = NULL;
3028
// `g_heap_typed` exists so the bench passes a typed `HeapAllocator *`

Benchmark/Source/Allocator_misra_arena.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#include <Misra/Std/Allocator/Page.h>
2020

2121
static ArenaAllocator g_arena;
22-
static int g_arena_live = 0;
22+
static bool g_arena_live = false;
2323

2424
const char *bench_backend_name(void) {
2525
#ifdef BENCH_MISRA_VARIANT_NAME
@@ -31,13 +31,13 @@ const char *bench_backend_name(void) {
3131

3232
void bench_init(void) {
3333
g_arena = ArenaAllocatorInit();
34-
g_arena_live = 1;
34+
g_arena_live = true;
3535
}
3636

3737
void bench_teardown(void) {
3838
if (g_arena_live) {
3939
ArenaAllocatorDeinit(&g_arena);
40-
g_arena_live = 0;
40+
g_arena_live = false;
4141
}
4242
}
4343

Benchmark/Source/Allocator_misra_correct.c

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
#include <Misra/Std/Allocator/Page.h>
2828
#include <Misra/Std/Allocator/Slab.h>
2929

30-
#include <stdlib.h>
31-
3230
// MODE_NONE = pre-init / post-teardown. MODE_HEAP / MODE_SLAB choose
3331
// which of the two static allocator instances below is currently
3432
// live. The two are unioned by usage, not by struct -- only one is
@@ -38,11 +36,11 @@ typedef enum {
3836
MODE_NONE = 0,
3937
MODE_HEAP = 1,
4038
MODE_SLAB = 2,
41-
} BenchMode;
39+
} bench_mode;
4240

4341
static HeapAllocator g_heap;
4442
static SlabAllocator g_slab;
45-
static BenchMode g_mode = MODE_NONE;
43+
static bench_mode g_mode = MODE_NONE;
4644

4745
const char *bench_backend_name(void) {
4846
#ifdef BENCH_MISRA_VARIANT_NAME
@@ -80,11 +78,11 @@ void bench_teardown(void) {
8078
// Matches MisraStdC's slab max (currently 4096 B, the smallest OS page
8179
// size we target). If the slab grows multi-page support later, raise
8280
// this threshold to match.
83-
#define MISRA_BENCH_SLAB_MAX_SLOT 4096u
81+
#define BENCH_SLAB_MAX_SLOT 4096u
8482

8583
void bench_use_fixed_size(size_t slot) {
8684
tear_current();
87-
if (slot > MISRA_BENCH_SLAB_MAX_SLOT) {
85+
if (slot > BENCH_SLAB_MAX_SLOT) {
8886
// Slab can't hold a slot this big; fall back to Heap and let
8987
// it route through XL (mmap-per-alloc) the way the upstream
9088
// "wrong tool" backend does. The bench's
@@ -124,10 +122,13 @@ void *bench_alloc(size_t n) {
124122

125123
void *bench_realloc(void *p, size_t n) {
126124
// SlabAllocator slot size is fixed at init; realloc is not a
127-
// sensible operation in slab mode and would have to copy through
128-
// a new slab of a different size. The bench's only realloc
125+
// sensible operation in slab mode. The bench's only realloc
129126
// workload (BM_ReallocGrow) runs in MODE_HEAP, so this branch
130-
// should never fire in slab mode. Fail loudly if it does.
127+
// should never fire in slab mode. Return NULL (caller treats as
128+
// alloc failure -- the bench then reads the size-class check
129+
// and moves on); we avoid LOG_FATAL here because aborting the
130+
// whole bench process on a routing mismatch would make the
131+
// failure mode hostile to interactive debugging.
131132
if (g_mode == MODE_SLAB) {
132133
(void)p;
133134
(void)n;

Benchmark/Source/Allocator_misra_page.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#include <Misra/Std/Allocator/Page.h>
1616

1717
static PageAllocator g_page;
18-
static int g_page_live = 0;
18+
static bool g_page_live = false;
1919

2020
const char *bench_backend_name(void) {
2121
#ifdef BENCH_MISRA_VARIANT_NAME
@@ -27,13 +27,13 @@ const char *bench_backend_name(void) {
2727

2828
void bench_init(void) {
2929
g_page = PageAllocatorInit();
30-
g_page_live = 1;
30+
g_page_live = true;
3131
}
3232

3333
void bench_teardown(void) {
3434
if (g_page_live) {
3535
PageAllocatorDeinit(&g_page);
36-
g_page_live = 0;
36+
g_page_live = false;
3737
}
3838
}
3939

Source/Misra/Std/Allocator/Slab.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ void *slab_allocator_allocate(Allocator *self, size bytes, i8 zeroed) {
269269
// free bit. First fit wins. Tail bits in the last word (if any)
270270
// are pre-set to 1 at slab-insert time, so ctz on the inverted
271271
// word can never spuriously hit them.
272+
//
273+
// The pre-redesign code re-read bm[w] here and LOG_FATAL'd if the
274+
// freshly-found bit was already set, defending against an
275+
// inconsistent-with-itself bitmap word. That check was a
276+
// self-consistency probe against rare corruption -- not a useful
277+
// safety net under the contiguous-bitmaps layout, where the only
278+
// way the bit can be set is if `CTZ64(~bm[w])` is broken (which
279+
// is platform code we trust). Free's double-free check (below)
280+
// still defends against the more useful real-world failure mode:
281+
// releasing a slot twice from user code.
272282
for (u32 i = 0; i < slab->slabs_len; i++) {
273283
u64 *bm = &slab->bitmaps[(size)i * (size)bw];
274284
for (u32 w = 0; w < bw; w++) {
@@ -277,8 +287,6 @@ void *slab_allocator_allocate(Allocator *self, size bytes, i8 zeroed) {
277287
continue;
278288
}
279289
u32 bit = CTZ64(inv);
280-
// No tail-bit guard needed: tail bits are pre-set to 1,
281-
// so ctz(~word) never finds them.
282290
bm[w] |= ((u64)1 << bit);
283291
u32 slot_idx = w * 64u + bit;
284292
void *slot = (char *)slab->slabs[i] + ((size)slot_idx << slab->slot_size_shift);

0 commit comments

Comments
 (0)