Skip to content

Commit aca2fd6

Browse files
hash-page-table: allocator restructure + 15-round convention sweep
Allocator restructure - Each allocator owns its own OS pages; PageAllocator dependency dropped from Heap / Arena / Slab. - Heap: hash-indexed pages[] + per-class warm list; xl-page descriptor table on its own mmap, page-rounded for grow/unmap symmetry. - Typed-self everywhere: dispatch enters typed, vtable cast happens once at *Init compound literal, no `_dyn` thunks below the boundary. - Extracted `_Os.{h,c}` shim (os_page_map / unmap / round_up) shared by every backend. - Dirty-bit deep-validate gating on Page + Heap (LSB of `__magic`). - New public accessors: HeapAllocator{PageCount,XlCount}, PageAllocator{FootprintBytes,EntryCount}, BudgetAllocatorSlotCount. - Arena.c: `zeroed` flag now honoured on in-chunk reuse (was returning stale tenant bytes after `ArenaAllocatorReset`). - AllocatorStats accounting documented as typed-direct-skip (only the `Allocator *` dyn arm updates counters). Bench / perf infrastructure - Per-op + perf-record harnesses for HeapAllocator vs libc (Bench-Perf/). - Cross-allocator comparison driver (Benchmark/) with libc / misra / misra-arena / misra-page split; Page footprint sampling via the new accessor. Sys / parsers - Sys/*: direct syscalls (Linux x86_64/aarch64 + Darwin BSD class) via Source/Misra/_Syscall.h; FEATURE_DIRECT_SYSCALL gates the wrapper path. ErrnoOf(ret) unifies `<0`-means-`-errno` (direct) and `-1`+errno (libc fallback). - Proc.c: child exit path on execve failure (was returning into parent code as a second process); wait4 sentinel switched to `< 0`; GetCurrentExecutablePath readlink sentinel switched to `< 0`; stdin/stdout/stderr pipe initialisers fixed (was leaving fd 0). - Socket.c / Dns.c / Dir.c / Proc.c: alias wrappers purged; canonical `LOG_SYS_ERROR(ErrnoOf(ret), ...)` everywhere. - Mutex.h+c: gating unified on FEATURE_DIRECT_SYSCALL; dead pthread fallback removed; NULL guards added to Lock/Unlock. - Backtrace / SymbolResolver / PdbCache / MachoCache stay outside the Sys umbrella (parser-name collision risk); umbrella comment lists the full carve-out. - Parsers: ElfFindSection / PeFindSection / MachoFindSection now take Str / Zstr / char* via _Generic; dns_resolve_4_one same. - Elf.c: `class` field renamed to `elf_class` (C++ keyword). - Dns.c: VecPushBackR failure path no longer leaks the Str name. Containers - Validator-before-field-access in Map.c / List.c / Graph.c validate_*; closes NULL-deref on a magic-OK handle with NULL allocator pointer. - Float / Int observers const-cascaded across Memory / Convert / Compare / Math / Private headers (and their callers). - Alias wrappers removed: MapTryGetPtr, MapGetOrInsertPtr, BitVecInitWithCapacityMacro. Callers migrated. - Iter / StrIter: direction-aware peek (PeekNext / PeekPrev) wired through the `dir` field; macro args parenthesised. Std / freestanding - Io.c read-text-int family (_read_u8..u64 / i8..i64) consolidated via `_MAKE_READ_TXT_INT` stamping macro (net -460 LOC). - _Freestanding.c: `_setjmp` deduplicated as `__attribute__((alias( "setjmp")))`; jmp_buf size docs corrected (x86_64 64 B, aarch64 104 B). - Zstr.c parseInt: removed `s[-1]` UB when no digits consumed. - Types.h: NULL is `((void *)0)`; FORMAT_STRING uses `__attribute__` (was `__attribute`); INVERT_ENDIANNESS{4,8} body and arg paren-fixed (was silently corrupting the high half for any direct caller). Tests - TestRunner: `expect_failure=false` now installs OnAbort and routes through one setjmp before the test body (previously skipped the setjmp install AND the OnAbort reset). - Tests/Std: `&alloc.base` replaced with `ALLOCATOR_OF(&alloc)`; white-box private-field reads/writes carry `// intentional bypass:` comments per the new CONVENTIONS phrase. - Tests/meson.build: PdbCache + MachoCache gated on `opt_file and opt_sys_dir` so minimal builds link. - Fuzz/Harness/Str.{c,h}: deleted (stub: referenced non-existent APIs). Docs / build - CODING-CONVENTIONS.md: new sections for `intentional bypass:` canonical phrase, AllocatorStats typed-direct-skip, .c-local macro-stamping pattern as approved alternative to alias wrappers; rule reaffirmed: NO MISRA_ prefix on public macros (only include guards + MISRA_SYS_* constants); NO alias wrappers (neither `#define foo bar` nor `static inline T foo(...){return bar(...);}`); _Generic inlined at each call site, Zstr arms paired with char*. - README.md: minimal-build example with full disable-flag list and cascade explanation (alloc_debug auto-enables map + sys_backtrace + parser_elf + parser_dwarf); file / iter / alloc_arena flagged as not-currently-disable-able with rationale. - Misra.h / Std.h / Sys.h umbrellas: FEATURE_* gates and carve-outs brought in line with the actual feature matrix; Container.h umbrella now exposes Buf.h. - meson.build: dead libm dep removed. - File-path header comments normalised to lowercase `/// file: relative/path.ext` across every header and source. Build / test status: Linux 104/104 + Mac 99/99 green at this tip.
1 parent 979b34c commit aca2fd6

230 files changed

Lines changed: 5066 additions & 4736 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Bench-Perf/libc_bench.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Mirror of misra_bench.c for libc malloc/free. Same N, SZ, REPS so
2+
// perf cycle counts are directly comparable.
3+
4+
#include <stdio.h>
5+
#include <stdlib.h>
6+
#include <string.h>
7+
8+
#define N 16384
9+
#define SZ 64
10+
#define REPS 200
11+
12+
static void *ptrs[N];
13+
14+
__attribute__((noinline))
15+
static void libc_alloc_phase(void) {
16+
for (size_t i = 0; i < N; i++) {
17+
ptrs[i] = malloc(SZ);
18+
}
19+
}
20+
21+
__attribute__((noinline))
22+
static void libc_free_phase(void) {
23+
for (size_t i = N; i-- > 0;) {
24+
free(ptrs[i]);
25+
}
26+
}
27+
28+
int main(int argc, char **argv) {
29+
// Warm up
30+
libc_alloc_phase();
31+
libc_free_phase();
32+
33+
for (int r = 0; r < REPS; r++) {
34+
libc_alloc_phase();
35+
libc_free_phase();
36+
}
37+
38+
(void)argc;
39+
(void)argv;
40+
return 0;
41+
}

Bench-Perf/misra_bench.c

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Standalone misra HeapAllocator bench for `perf record`.
2+
//
3+
// Runs many iterations of "alloc N items, free N items" so perf can
4+
// attribute cycles to specific functions. No Google Benchmark, no C++,
5+
// no libstdc++ noise.
6+
//
7+
// Compile:
8+
// nix-shell -p clang --command 'clang -O3 -g -march=x86-64-v3 \
9+
// -IInclude -Ibuild-bench/Include \
10+
// Bench-Perf/misra_bench.c build-bench/libmisra_std.a -lm -o /tmp/misra_bench'
11+
//
12+
// Run under perf:
13+
// perf record -F 9999 --call-graph=lbr -o /tmp/misra.data -- /tmp/misra_bench alloc
14+
// perf report -i /tmp/misra.data
15+
16+
#include <Misra/Std/Allocator.h>
17+
#include <Misra/Std/Allocator/Heap.h>
18+
19+
#include <stdio.h>
20+
#include <stdlib.h>
21+
#include <string.h>
22+
23+
#define N 16384
24+
#define SZ 64
25+
#define REPS 200
26+
27+
static void *ptrs[N];
28+
29+
// Two top-level entry points so `perf report` cleanly separates them.
30+
// `__attribute__((noinline))` keeps perf from folding them into main.
31+
32+
// Pass the TYPED HeapAllocator* so _Generic picks the
33+
// heap_allocator_allocate/heap_allocator_deallocate symbols
34+
// directly. Passing as Allocator* would route through
35+
// AllocatorAlloc_dyn + ValidateAllocator (the runtime-vtable path)
36+
// which is only needed when the concrete type isn't known at the
37+
// call site. The official bench (Allocator_misra.c) does the same.
38+
__attribute__((noinline))
39+
static void misra_alloc_phase(HeapAllocator *h) {
40+
for (size_t i = 0; i < N; i++) {
41+
ptrs[i] = AllocatorAlloc(h, SZ, 0);
42+
}
43+
}
44+
45+
__attribute__((noinline))
46+
static void misra_free_phase(HeapAllocator *h) {
47+
for (size_t i = N; i-- > 0;) {
48+
AllocatorFree(h, ptrs[i]);
49+
}
50+
}
51+
52+
int main(int argc, char **argv) {
53+
int run_alloc = 1, run_free = 1;
54+
if (argc >= 2) {
55+
if (strcmp(argv[1], "alloc") == 0) { run_free = 0; }
56+
else if (strcmp(argv[1], "free") == 0) { run_alloc = 0; }
57+
else if (strcmp(argv[1], "both") == 0) {}
58+
else { fprintf(stderr, "usage: %s [alloc|free|both]\n", argv[0]); return 1; }
59+
}
60+
61+
HeapAllocator h = HeapAllocatorInit();
62+
63+
// Warm up: one full cycle so descriptor array reaches steady-state
64+
// size. This isolates measurement from one-time growth costs.
65+
misra_alloc_phase(&h);
66+
misra_free_phase(&h);
67+
68+
if (run_alloc && run_free) {
69+
// both: full alloc-then-free cycle per rep.
70+
for (int r = 0; r < REPS; r++) {
71+
misra_alloc_phase(&h);
72+
misra_free_phase(&h);
73+
}
74+
} else if (run_alloc) {
75+
// alloc-only: allocate once per rep, free at the END after all
76+
// reps so perf samples land entirely in the alloc path. Costs
77+
// REPS*N allocations of working memory; HeapAllocator handles
78+
// this comfortably for N=16384.
79+
for (int r = 0; r < REPS; r++) {
80+
misra_alloc_phase(&h);
81+
}
82+
misra_free_phase(&h);
83+
} else if (run_free) {
84+
// free-only: pre-allocate once outside the timed loop, then
85+
// free + re-alloc per rep. The re-alloc is a known confound
86+
// (it shows up in perf samples too); the alternative would be
87+
// to alloc REPS*N up front and free them in chunks, which
88+
// changes per-iteration cache behaviour. Document and leave
89+
// the trade-off for the operator to interpret.
90+
misra_alloc_phase(&h);
91+
for (int r = 0; r < REPS; r++) {
92+
misra_free_phase(&h);
93+
misra_alloc_phase(&h);
94+
}
95+
misra_free_phase(&h);
96+
}
97+
98+
HeapAllocatorDeinit(&h);
99+
return 0;
100+
}

Bench-Perf/perop_bench.c

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Per-operation latency measurement using clock_gettime(CLOCK_MONOTONIC).
2+
//
3+
// Records nanoseconds for EVERY individual alloc and EVERY individual
4+
// free. Dumps medians + percentiles so you can see the distribution,
5+
// not just averages.
6+
//
7+
// Build:
8+
// clang -O3 -g -march=x86-64-v3 -IInclude -Ibuild-perf/Include \
9+
// Bench-Perf/perop_bench.c build-perf/libmisra_std.a -lm -o /tmp/perop_misra
10+
// clang -O3 -g -march=x86-64-v3 -DUSE_LIBC \
11+
// Bench-Perf/perop_bench.c -lm -o /tmp/perop_libc
12+
13+
#include <stdio.h>
14+
#include <stdint.h>
15+
#include <stdlib.h>
16+
#include <string.h>
17+
#include <time.h>
18+
19+
#ifndef USE_LIBC
20+
# include <Misra/Std/Allocator.h>
21+
# include <Misra/Std/Allocator/Heap.h>
22+
#endif
23+
24+
#define N 16384 // ops per phase
25+
#define SZ 64
26+
#define REPS 50 // outer reps for warmup + noise reduction
27+
28+
static void *ptrs[N];
29+
30+
// Pull the timer into one place so the inlining cost is identical
31+
// across libc/misra paths. Marked `inline` because the per-op
32+
// bookkeeping calls it twice per iteration and we want the
33+
// clock_gettime call sequence visible at each measurement site.
34+
static inline uint64_t now_ns(void) {
35+
struct timespec ts;
36+
clock_gettime(CLOCK_MONOTONIC, &ts);
37+
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
38+
}
39+
40+
// Two-buffer storage for per-op timings, lazily allocated big enough
41+
// for the whole run.
42+
#define TOTAL_OPS (N * REPS)
43+
static uint64_t alloc_ns[TOTAL_OPS];
44+
static uint64_t free_ns[TOTAL_OPS];
45+
46+
static int cmp_u64(const void *a, const void *b) {
47+
uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b;
48+
return (x > y) - (x < y);
49+
}
50+
51+
static void report(const char *label, uint64_t *arr, size_t n) {
52+
qsort(arr, n, sizeof(uint64_t), cmp_u64);
53+
uint64_t sum = 0;
54+
for (size_t i = 0; i < n; ++i) sum += arr[i];
55+
double mean = (double)sum / (double)n;
56+
printf("%-15s n=%zu min=%lu p50=%lu p90=%lu p99=%lu p999=%lu max=%lu mean=%.2f\n",
57+
label, n,
58+
arr[0],
59+
arr[n/2],
60+
arr[(size_t)(n*0.90)],
61+
arr[(size_t)(n*0.99)],
62+
arr[(size_t)(n*0.999)],
63+
arr[n-1],
64+
mean);
65+
}
66+
67+
int main(void) {
68+
#ifndef USE_LIBC
69+
HeapAllocator h = HeapAllocatorInit();
70+
// Typed dispatch: the _Generic in AllocatorAlloc picks
71+
// heap_allocator_allocate directly. Passing `Allocator *` would
72+
// route through AllocatorAlloc_dyn (ValidateAllocator + vtable
73+
// indirect) and measure a different path than misra_bench does.
74+
#define DO_ALLOC() AllocatorAlloc(&h, SZ, 0)
75+
#define DO_FREE(p) AllocatorFree(&h, p)
76+
#else
77+
#define DO_ALLOC() malloc(SZ)
78+
#define DO_FREE(p) free(p)
79+
#endif
80+
81+
// Warmup: fill & drain once so descriptor arrays are sized.
82+
for (size_t i = 0; i < N; i++) ptrs[i] = DO_ALLOC();
83+
for (size_t i = N; i-- > 0;) DO_FREE(ptrs[i]);
84+
85+
size_t a_idx = 0, f_idx = 0;
86+
87+
for (int r = 0; r < REPS; r++) {
88+
// Alloc phase, per-op timing.
89+
for (size_t i = 0; i < N; i++) {
90+
uint64_t t0 = now_ns();
91+
ptrs[i] = DO_ALLOC();
92+
uint64_t t1 = now_ns();
93+
alloc_ns[a_idx++] = t1 - t0;
94+
}
95+
// Free phase, per-op timing.
96+
for (size_t i = N; i-- > 0;) {
97+
uint64_t t0 = now_ns();
98+
DO_FREE(ptrs[i]);
99+
uint64_t t1 = now_ns();
100+
free_ns[f_idx++] = t1 - t0;
101+
}
102+
}
103+
104+
report("alloc (ns)", alloc_ns, a_idx);
105+
report("free (ns)", free_ns, f_idx);
106+
107+
#ifndef USE_LIBC
108+
HeapAllocatorDeinit(&h);
109+
#endif
110+
return 0;
111+
}

Benchmark/Source/Allocator_misra.c

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ void *bench_realloc(void *p, size_t n) {
8484
}
8585

8686
void bench_free(void *p) {
87-
// typed-direct AllocatorFree resolves to heap_allocator_free
88-
// static-inline trampoline (one tail call into heap_allocator_deallocate).
87+
// typed-direct AllocatorFree resolves to heap_allocator_deallocate
88+
// directly (the _Generic arm names the deallocate symbol, no
89+
// trampoline in between).
8990
AllocatorFree(g_heap_typed, p);
9091
}
9192

@@ -99,23 +100,14 @@ uint64_t bench_live_bytes(void) {
99100
}
100101

101102
uint64_t bench_footprint_bytes(void) {
102-
// The HeapAllocator's embedded PageAllocator tracks every live
103-
// mmap region in `entries[0..len-1]`. Each entry carries the
104-
// rounded mmap length (PageEntry.bytes -- see Page.h:32-40).
105-
// Summing gives the exact kernel-committed footprint of misra's
106-
// heap, with zero contamination from the bench process's other
107-
// memory (gbench, std::vectors, libc).
108-
//
109-
// entries[] holds: every user page that the heap's S/M/L bins
110-
// are carving slots out of; every XL passthrough region (one
111-
// mmap per >2048 B allocation); and the heap's own
112-
// descriptor-array storage. So it's the complete "what did
113-
// misra ask the kernel for" number.
114-
if (!g_alloc) return 0;
115-
const PageAllocator *p = &g_heap.page;
116-
uint64_t total = 0;
117-
for (u32 i = 0; i < p->len; i++) {
118-
total += (uint64_t)p->entries[i].bytes;
119-
}
120-
return total;
103+
// The HeapAllocator no longer embeds a PageAllocator -- it goes
104+
// straight to the kernel via the internal `_Os.h` shim and tracks
105+
// its OS-page descriptors in its own `pages[]` hash table plus
106+
// `xl[]` passthrough array. Those descriptors are not part of the
107+
// public surface, so there's no allocator-introspective accessor
108+
// we can call here without reaching into private fields. Fall back
109+
// to the stats-tracked live-bytes number, which underestimates
110+
// kernel footprint (no per-page overhead) but is the cleanest
111+
// public-API readout available.
112+
return bench_live_bytes();
121113
}

Benchmark/Source/Allocator_misra_arena.c

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,9 @@ uint64_t bench_live_bytes(void) {
9797
}
9898

9999
uint64_t bench_footprint_bytes(void) {
100-
if (!g_arena_live) return 0;
101-
const PageAllocator *p = &g_arena.page;
102-
uint64_t total = 0;
103-
for (u32 i = 0; i < p->len; i++) {
104-
total += (uint64_t)p->entries[i].bytes;
105-
}
106-
return total;
100+
// ArenaAllocator no longer embeds a PageAllocator; it talks directly
101+
// to the kernel via the internal `_Os.h` shim and tracks chunks
102+
// privately. Fall back to live-bytes from stats (undercounts the
103+
// per-chunk header overhead, but the only public-API readout).
104+
return bench_live_bytes();
107105
}

Benchmark/Source/Allocator_misra_correct.c

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
#include <Misra/Std/Allocator.h>
2626
#include <Misra/Std/Allocator/Heap.h>
27-
#include <Misra/Std/Allocator/Page.h>
2827
#include <Misra/Std/Allocator/Slab.h>
2928

3029
// MODE_NONE = pre-init / post-teardown. MODE_HEAP / MODE_SLAB choose
@@ -165,21 +164,9 @@ uint64_t bench_live_bytes(void) {
165164
}
166165

167166
uint64_t bench_footprint_bytes(void) {
168-
// Both HeapAllocator and SlabAllocator embed a PageAllocator at
169-
// the same field name. The exact mmap footprint is the sum of
170-
// PageEntry.bytes across its entries[] vector. Zero process
171-
// noise from gbench, std::vectors, libstdc++, etc.
172-
const PageAllocator *p;
173-
if (g_mode == MODE_SLAB) {
174-
p = &g_slab.page;
175-
} else if (g_mode == MODE_HEAP) {
176-
p = &g_heap.page;
177-
} else {
178-
return 0;
179-
}
180-
uint64_t total = 0;
181-
for (u32 i = 0; i < p->len; i++) {
182-
total += (uint64_t)p->entries[i].bytes;
183-
}
184-
return total;
167+
// HeapAllocator and SlabAllocator no longer embed a PageAllocator;
168+
// each talks to the kernel directly. Footprint measurement via the
169+
// PageAllocator entries[] vector is no longer available here.
170+
(void)g_mode;
171+
return 0;
185172
}

Benchmark/Source/Allocator_misra_page.c

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,5 @@ uint64_t bench_live_bytes(void) {
8181

8282
uint64_t bench_footprint_bytes(void) {
8383
if (!g_page_live) return 0;
84-
uint64_t total = 0;
85-
for (u32 i = 0; i < g_page.len; i++) {
86-
total += (uint64_t)g_page.entries[i].bytes;
87-
}
88-
return total;
84+
return (uint64_t)PageAllocatorFootprintBytes(&g_page);
8985
}

Benchmark/meson.build

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Allocator benchmark. Off by default; enable with -Dbenchmark=true.
22
#
3-
# Builds six binaries that share the same Google Benchmark suite and
3+
# Builds eight binaries that share the same Google Benchmark suite and
44
# differ only in which allocator they exercise:
55
#
66
# bench-glibc -- default malloc resolution, no extra link
@@ -15,10 +15,15 @@
1515
# HeapAllocator for mixed-size benches.
1616
# Honours the bench_use_fixed_size hint
1717
# that the bench harness emits.
18+
# bench-misra-arena -- ArenaAllocator: bump-pointer with bulk
19+
# O(1) reset. Targets BM_ArenaBumpReset.
20+
# bench-misra-page -- PageAllocator: one mmap per alloc, page-
21+
# rounded. Targets page-aligned sizes.
1822
#
1923
# Each backend is one C/C++ source file plus the shared bench harness.
20-
# The libc-shape backends share Allocator_libc.c; the two misra backends
21-
# each have their own (Allocator_misra.c / Allocator_misra_correct.c).
24+
# The libc-shape backends share Allocator_libc.c; the four misra backends
25+
# each have their own (Allocator_misra.c, Allocator_misra_correct.c,
26+
# Allocator_misra_arena.c, Allocator_misra_page.c).
2227
#
2328
# All binaries link the same misra_std the parent project just built --
2429
# no separate library build, no flake, no nix dependency. Run them via

Bin/Resolve.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// file : bin/resolve.c
1+
/// file : Bin/Resolve.c
22
/// author : Siddharth Mishra (admin@brightprogrammer.in)
33
/// This is free and unencumbered software released into the public domain.
44
///

0 commit comments

Comments
 (0)