From 5572fc41cc45a8f36b6a3cb0d19021fa2c936053 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:25:50 -0500 Subject: [PATCH] runtime: reserve 16GB of heap address space on 64-bit unix allocateHeap capped the heap at 1GB for all targets, so with -gc=conservative or -gc=precise any single allocation approaching 1GB (for example a scrypt key-derivation buffer with N=1<<20, r=8) failed with out of memory regardless of available system RAM. Reserve 16GB of virtual address space on 64-bit targets instead. The mmap is a reservation, not a commitment: pages cost physical memory only when first touched, and the existing halve-on-failure loop still adapts when the map is refused. 32-bit targets keep the 1GB cap; the size is derived from TargetBits as a shifted constant, since a plain 16GB literal does not compile on 32-bit targets even in a dead branch. This is the direction the growHeap comment already points at: "If we run out of memory, we should consider increasing heapMaxSize on 64-bit systems." With this, the practical limit under the blocks GC on 64-bit hosts becomes actual system memory, matching the boehm default and big Go; true exhaustion surfaces as the OS's memory pressure handling rather than a fatal error at an arbitrary 1GB. --- src/runtime/runtime_unix.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 24b208c387..487e2d1ca8 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -318,13 +318,24 @@ func procUnpin() { var heapSize uintptr = 128 * 1024 // small amount to start var heapMaxSize uintptr +// heapMaxReserve is how much virtual address space to reserve for the heap: +// 16GB (1<<34) on 64-bit targets and 1GB (1<<30) on 32-bit ones. The +// reservation is not a commitment: pages cost physical RAM only when first +// touched, and allocateHeap's halving loop still adapts if mmap refuses. +// A flat 1GB cap on 64-bit made any single allocation approaching 1GB +// (e.g. a scrypt key-derivation buffer with N=1<<20, r=8) fail regardless +// of available system memory. Deriving the shift from TargetBits keeps the +// constant within uintptr range on 32-bit targets, where a plain 16GB +// literal would not compile even in a dead branch. +const heapMaxReserve = 1 << (30 + 4*(TargetBits/64)) + var heapStart, heapEnd uintptr func allocateHeap() { // Allocate a large chunk of virtual memory. Because it is virtual, it won't // really be allocated in RAM. Memory will only be allocated when it is // first touched. - heapMaxSize = 1 * 1024 * 1024 * 1024 // 1GB for the entire heap + heapMaxSize = heapMaxReserve for { addr := mmap(nil, heapMaxSize, flag_PROT_READ|flag_PROT_WRITE, flag_MAP_PRIVATE|flag_MAP_ANONYMOUS, -1, 0) if addr == unsafe.Pointer(^uintptr(0)) {