Os.heap_relief(): the allocator's freed-but-cached memory handed back, once after a load

Beside Os.heap_bytes: malloc_zone_pressure_relief(NULL, 0) on macOS (weak, so a libc without it reads
0) and HeapCompact(GetProcessHeap(), 0) on Windows - kernel32 only, so the Windows build imports
nothing new; the bytes it says it released. Once after a load, never per frame.

Measured, for the record: on macOS it does NOT reach the large-block cache. A C program that frees six
15 MB blocks still holds 90 MB of MALLOC_LARGE (empty) after relief on every zone (it returns 0); only
MallocLargeCache=0 in the environment AT PROCESS START turns the cache off (read at malloc's init -
set later, it does nothing). Maroon Lake's watcher sets it for the processes it spawns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 14:30:20 +03:00
parent 5297996640
commit e958d00373
5 changed files with 34759 additions and 34607 deletions

View file

@ -24,6 +24,9 @@
# Os.temp_dir() -> string the system temporary directory
# Os.heap_bytes() -> long malloc's live bytes (macOS; 0 elsewhere): a test that a
# path allocates nothing, with no renderer linked in
# Os.heap_relief() -> long give the heap's freed-but-cached memory back to the system
# (macOS malloc_zone_pressure_relief, Windows HeapCompact;
# 0 elsewhere): once after a load, never per frame
#
# Determinism: args/env/platform are non-deterministic host input — read them at
# startup to configure the game, but keep them out of the replayable simulation.
@ -41,7 +44,7 @@ function is_os_ns(meth: pointer) -> bool {
if (meth == "exit") or (meth == "pid") or (meth == "platform") or (meth == "arch") { return true }
if (meth == "stdout_write") or (meth == "stderr_write") { return true }
if (meth == "save_dir") or (meth == "config_dir") or (meth == "cache_dir") or (meth == "temp_dir") { return true }
if (meth == "heap_bytes") { return true }
if (meth == "heap_bytes") or (meth == "heap_relief") { return true }
return false
}
@ -61,6 +64,10 @@ function emit_os_ns(meth: pointer, e: Node) -> Val {
g_uses_heap = true
return val(emit_bind("call i64 @lp_os_heap_bytes()"), "long")
}
if (meth == "heap_relief") {
g_uses_relief = true
return val(emit_bind("call i64 @lp_os_heap_relief()"), "long")
}
if (meth == "exit") {
let n = emit_expr(e.kids[0])
emit(` call void @exit(i32 {n.code})\n`)
@ -261,6 +268,21 @@ function emit_os_prelude() -> void {
emith(" %m = getelementptr i8, ptr %buf, i64 1024\n ret ptr %m\n}\n")
}
# Os.heap_relief: what the allocator keeps cached after a free (a large block libmalloc holds EMPTY)
# handed back, the bytes it says it released. Weak outside Windows, so a libc without it reads 0
function emit_os_relief() -> void {
emith("define i64 @lp_os_heap_relief() {\nentry:\n")
if g_target_win {
emith(" %h = call ptr @GetProcessHeap()\n %n = call i64 @HeapCompact(ptr %h, i32 0)\n ret i64 %n\n}\n")
emith("declare ptr @GetProcessHeap()\ndeclare i64 @HeapCompact(ptr, i32)\n")
return
}
emith(" %mz = icmp ne ptr @malloc_zone_pressure_relief, null\n br i1 %mz, label %mac, label %none\n")
emith("mac:\n %n = call i64 @malloc_zone_pressure_relief(ptr null, i64 0)\n ret i64 %n\n")
emith("none:\n ret i64 0\n}\n")
emith("declare extern_weak i64 @malloc_zone_pressure_relief(ptr, i64)\n")
}
# Os.heap_bytes: malloc_zone_statistics' in-use bytes, all zones. The symbol is weak, so a libc
# without it links and reads 0.
function emit_os_heap() -> void {