From 6b9ab7bd6a26035b0d79af06db5eb1ceda1fff97 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 May 2026 18:09:01 +0200 Subject: [PATCH 1/3] compiler: refactor runtime.alloc call This refactor makes no other changes. --- compiler/compiler.go | 15 ++------------- compiler/defer.go | 2 +- compiler/gc.go | 19 +++++++++++++++++++ compiler/llvm.go | 6 +----- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index eaf7b58b1c..24b90f20b5 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -192,16 +192,6 @@ func newBuilder(c *compilerContext, irbuilder llvm.Builder, f *ssa.Function) *bu } } -// Return the runtime.alloc function variant. -// This is normally just "alloc", but is "alloc_noheap" if the //go:noheap -// pragma is used. -func (b *builder) allocFunc() string { - if b.info.noheap { - return "alloc_noheap" - } - return "alloc" -} - type blockInfo struct { // entry is the LLVM basic block corresponding to the start of this *ssa.Block. entry llvm.BasicBlock @@ -2183,9 +2173,8 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { } sizeValue := llvm.ConstInt(b.uintptrType, size, false) layoutValue := b.createObjectLayout(typ, expr.Pos()) - buf := b.createRuntimeCall(b.allocFunc(), []llvm.Value{sizeValue, layoutValue}, expr.Comment) align := b.targetData.ABITypeAlignment(typ) - buf.AddCallSiteAttribute(0, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(align))) + buf := b.createAlloc(sizeValue, layoutValue, align, expr.Comment) return buf, nil } else { buf := llvmutil.CreateEntryBlockAlloca(b.Builder, typ, expr.Comment) @@ -2415,7 +2404,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { } sliceSize := b.CreateBinOp(llvm.Mul, elemSizeValue, sliceCapCast, "makeslice.cap") layoutValue := b.createObjectLayout(llvmElemType, expr.Pos()) - slicePtr := b.createRuntimeCall(b.allocFunc(), []llvm.Value{sliceSize, layoutValue}, "makeslice.buf") + slicePtr := b.createAlloc(sliceSize, layoutValue, 0, "makeslice.buf") slicePtr.AddCallSiteAttribute(0, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(elemAlign))) // Extend or truncate if necessary. This is safe as we've already done diff --git a/compiler/defer.go b/compiler/defer.go index ec2bbe00e1..adc5558892 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -488,7 +488,7 @@ func (b *builder) createDefer(instr *ssa.Defer) { size := b.targetData.TypeAllocSize(deferredCallType) sizeValue := llvm.ConstInt(b.uintptrType, size, false) nilPtr := llvm.ConstNull(b.dataPtrType) - alloca = b.createRuntimeCall(b.allocFunc(), []llvm.Value{sizeValue, nilPtr}, "defer.alloc.call") + alloca = b.createAlloc(sizeValue, nilPtr, 0, "defer.alloc.call") } if b.NeedsStackObjects { b.trackPointer(alloca) diff --git a/compiler/gc.go b/compiler/gc.go index 5ca79b91ba..5c59373a99 100644 --- a/compiler/gc.go +++ b/compiler/gc.go @@ -10,6 +10,25 @@ import ( "tinygo.org/x/go-llvm" ) +// Heap-allocate a buffer of the given size. This will typically call +// runtime.alloc. +func (b *builder) createAlloc(sizeValue, layoutValue llvm.Value, align int, comment string) llvm.Value { + // Normally allocate using "runtime.alloc", but use "runtime.alloc_noheap" + // if the //go:noheap pragma is used. + allocFunc := "alloc" + if b.info.noheap { + allocFunc = "alloc_noheap" + } + + call := b.createRuntimeCall(allocFunc, []llvm.Value{sizeValue, layoutValue}, comment) + if align != 0 { + // TODO: make sure all callsites set the correct alignment. + call.AddCallSiteAttribute(0, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(align))) + } + + return call +} + // trackExpr inserts pointer tracking intrinsics for the GC if the expression is // one of the expressions that need this. func (b *builder) trackExpr(expr ssa.Value, value llvm.Value) { diff --git a/compiler/llvm.go b/compiler/llvm.go index c25a8d4a35..a7fe725bba 100644 --- a/compiler/llvm.go +++ b/compiler/llvm.go @@ -129,11 +129,7 @@ func (b *builder) emitPointerPack(values []llvm.Value) llvm.Value { // Packed data is bigger than a pointer, so allocate it on the heap. sizeValue := llvm.ConstInt(b.uintptrType, size, false) align := b.targetData.ABITypeAlignment(packedType) - packedAlloc := b.createRuntimeCall(b.allocFunc(), []llvm.Value{ - sizeValue, - llvm.ConstNull(b.dataPtrType), - }, "") - packedAlloc.AddCallSiteAttribute(0, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(align))) + packedAlloc := b.createAlloc(sizeValue, llvm.ConstNull(b.dataPtrType), align, "") if b.NeedsStackObjects { b.trackPointer(packedAlloc) } From d8a21ed3419e8b3f0e9bdf9cd17d7ed8878c9c5d Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 May 2026 18:28:05 +0200 Subject: [PATCH 2/3] runtime: move alloc_noheap call (pure refactor) See the next commit, where this makes more sense. --- src/runtime/gc.go | 10 ++++++++++ src/runtime/runtime.go | 5 ----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 src/runtime/gc.go diff --git a/src/runtime/gc.go b/src/runtime/gc.go new file mode 100644 index 0000000000..c8c1706e96 --- /dev/null +++ b/src/runtime/gc.go @@ -0,0 +1,10 @@ +package runtime + +// Shared code for the various garbage collectors. + +import "unsafe" + +// Special alloc function that should never actually be called. +// It is used instead of normal alloc in //go:noheap functions, and must either +// be optimized away or throw a linker error. +func alloc_noheap(size uintptr, layout unsafe.Pointer) unsafe.Pointer diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 872b225b9d..c9b0959384 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -66,11 +66,6 @@ func llvm_sponentry() unsafe.Pointer //export strlen func strlen(ptr unsafe.Pointer) uintptr -// Special alloc function that should never actually be called. -// It is used instead of normal alloc in //go:noheap functions, and must either -// be optimized away or throw a linker error. -func alloc_noheap(size uintptr, layout unsafe.Pointer) unsafe.Pointer - //export malloc func malloc(size uintptr) unsafe.Pointer From 8ae64a0c64712d7345c7040dfcca76f9b749e26b Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 May 2026 18:55:36 +0200 Subject: [PATCH 3/3] compiler, runtime: optimize zero-sized allocations Instead of referring to an unused global, use a constant value. This is safe even when using `-gc=none` (since no actual memory gets allocated) which wasn't the case before. It should also reduce binary size by a few bytes for most programs. --- compiler/gc.go | 7 +++++++ compiler/symbol.go | 2 +- compiler/testdata/gc.ll | 5 ++++- interp/interpreter.go | 2 +- src/runtime/arch_avr.go | 2 ++ src/runtime/arch_cortexm.go | 2 ++ src/runtime/arch_tinygoriscv.go | 2 ++ src/runtime/arch_tinygowasm.go | 7 +++++++ src/runtime/arch_xtensa.go | 2 ++ src/runtime/gc.go | 19 +++++++++++++++++++ src/runtime/gc_blocks.go | 5 +---- src/runtime/gc_boehm.go | 5 +---- src/runtime/os_darwin.go | 2 ++ src/runtime/os_linux.go | 2 ++ src/runtime/os_windows.go | 2 ++ src/runtime/runtime_arm7tdmi.go | 2 ++ src/runtime/runtime_nintendoswitch.go | 5 +++++ 17 files changed, 62 insertions(+), 11 deletions(-) diff --git a/compiler/gc.go b/compiler/gc.go index 5c59373a99..b5246e2187 100644 --- a/compiler/gc.go +++ b/compiler/gc.go @@ -20,6 +20,13 @@ func (b *builder) createAlloc(sizeValue, layoutValue llvm.Value, align int, comm allocFunc = "alloc_noheap" } + // Allocs that don't allocate anything can return an architecture-specific + // sentinel value. + if !sizeValue.IsAConstantInt().IsNil() && sizeValue.ZExtValue() == 0 { + allocFunc = "alloc_zero" + } + + // Make the runtime call. call := b.createRuntimeCall(allocFunc, []llvm.Value{sizeValue, layoutValue}, comment) if align != 0 { // TODO: make sure all callsites set the correct alignment. diff --git a/compiler/symbol.go b/compiler/symbol.go index 0e78e2ce30..fec98ce3b4 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -162,7 +162,7 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) case "machine.keepAliveNoEscape", "machine.unsafeNoEscape": llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) - case "runtime.alloc", "runtime.alloc_noheap": + case "runtime.alloc", "runtime.alloc_noheap", "runtime.alloc_zero": // Tell the optimizer that runtime.alloc is an allocator, meaning that it // returns values that are never null and never alias to an existing value. for _, attrName := range []string{"noalias", "nonnull"} { diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index 14f9aa8b25..e2b0090698 100644 --- a/compiler/testdata/gc.ll +++ b/compiler/testdata/gc.ll @@ -75,7 +75,7 @@ entry: define hidden void @main.newStruct(ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %new = call align 1 ptr @runtime.alloc(i32 0, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 + %new = call align 1 ptr @runtime.alloc_zero(i32 0, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new, ptr @main.struct1, align 4 %new1 = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 @@ -93,6 +93,9 @@ entry: ret void } +; Function Attrs: allockind("alloc,zeroed") allocsize(0) +declare noalias nonnull ptr @runtime.alloc_zero(i32, ptr, ptr) #2 + ; Function Attrs: nounwind define hidden ptr @main.newFuncValue(ptr %context) unnamed_addr #1 { entry: diff --git a/interp/interpreter.go b/interp/interpreter.go index 710f3e8133..ea6f678c26 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -299,7 +299,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // means that monotonic time in the time package is counted from // time.Time{}.Sub(1), which should be fine. locals[inst.localIndex] = literalValue{uint64(0)} - case callFn.name == "runtime.alloc" || callFn.name == "runtime.alloc_noheap": + case callFn.name == "runtime.alloc" || callFn.name == "runtime.alloc_noheap" || callFn.name == "runtime.alloc_zero": // Allocate heap memory. At compile time, this is instead done // by creating a global variable. diff --git a/src/runtime/arch_avr.go b/src/runtime/arch_avr.go index 251d154354..f52a5a56a0 100644 --- a/src/runtime/arch_avr.go +++ b/src/runtime/arch_avr.go @@ -9,6 +9,8 @@ const GOARCH = "arm" // avr pretends to be arm // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 8 +const zeroSizeAllocPtr uintptr = 16 // part of the first protected page + const deferExtraRegs = 1 // the frame pointer (Y register) also needs to be stored const callInstSize = 2 // "call" is 4 bytes, "rcall" is 2 bytes diff --git a/src/runtime/arch_cortexm.go b/src/runtime/arch_cortexm.go index 6ea4a4838e..96339ec595 100644 --- a/src/runtime/arch_cortexm.go +++ b/src/runtime/arch_cortexm.go @@ -11,6 +11,8 @@ const GOARCH = "arm" // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 32 +const zeroSizeAllocPtr uintptr = 16 // part of the interrupt vector + const deferExtraRegs = 0 const callInstSize = 4 // "bl someFunction" is 4 bytes diff --git a/src/runtime/arch_tinygoriscv.go b/src/runtime/arch_tinygoriscv.go index 921c775a5e..d0f6d2cc04 100644 --- a/src/runtime/arch_tinygoriscv.go +++ b/src/runtime/arch_tinygoriscv.go @@ -4,6 +4,8 @@ package runtime import "device/riscv" +const zeroSizeAllocPtr uintptr = 0xffff_fff0 // should be unused on most RISC-V chips + const deferExtraRegs = 0 const callInstSize = 4 // 8 without relaxation, maybe 4 with relaxation diff --git a/src/runtime/arch_tinygowasm.go b/src/runtime/arch_tinygowasm.go index 0f61fe0082..cfb8dfef64 100644 --- a/src/runtime/arch_tinygowasm.go +++ b/src/runtime/arch_tinygowasm.go @@ -11,6 +11,13 @@ const GOARCH = "wasm" // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 32 +// zeroSizedAlloc a sentinel that gets returned when allocating 0 bytes. +// Using this instead of a constant value since I can't easily find a memory +// location that is definitely not going to end up as a valid pointer. +var zeroSizedAlloc uint8 + +var zeroSizeAllocPtr = &zeroSizedAlloc + const deferExtraRegs = 0 const callInstSize = 1 // unknown and irrelevant (llvm.returnaddress doesn't work), so make something up diff --git a/src/runtime/arch_xtensa.go b/src/runtime/arch_xtensa.go index 557bee9a79..f3192e0000 100644 --- a/src/runtime/arch_xtensa.go +++ b/src/runtime/arch_xtensa.go @@ -6,6 +6,8 @@ import "device" const GOARCH = "arm" // xtensa pretends to be arm +const zeroSizeAllocPtr uintptr = 16 // part of early flash: partition table, etc + // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 32 diff --git a/src/runtime/gc.go b/src/runtime/gc.go index c8c1706e96..6367d34eff 100644 --- a/src/runtime/gc.go +++ b/src/runtime/gc.go @@ -8,3 +8,22 @@ import "unsafe" // It is used instead of normal alloc in //go:noheap functions, and must either // be optimized away or throw a linker error. func alloc_noheap(size uintptr, layout unsafe.Pointer) unsafe.Pointer + +// Special alloc function that returns a sentinel value that can never be on the +// heap or match any other valid pointer. An alloc(0, xxx) call can be safely +// converted to an alloc_zero(0, xxx) call as an optimization. +// +// It is always a good idea to inline this function, since the result is a +// constant. Marking it as go:inline to be sure even though the compiler should +// already be doing this. +// +//go:inline +func alloc_zero(size uintptr, layout unsafe.Pointer) unsafe.Pointer { + // Returning a constant here is safe, since the Go spec does not require + // multiple zero-sized allocations to be unequal when compared for equality: + // + // > Pointers to distinct zero-size variables may or may not be equal. + // + // Source: https://go.dev/ref/spec#Comparison_operators + return unsafe.Pointer(zeroSizeAllocPtr) +} diff --git a/src/runtime/gc_blocks.go b/src/runtime/gc_blocks.go index a10b594375..b87789a812 100644 --- a/src/runtime/gc_blocks.go +++ b/src/runtime/gc_blocks.go @@ -58,9 +58,6 @@ var ( gcLock task.PMutex // lock to avoid race conditions on multicore systems ) -// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. -var zeroSizedAlloc uint8 - // Provide some abstraction over heap blocks. // blockState stores the four states in which a block can be. @@ -391,7 +388,7 @@ func calculateHeapAddresses() { //go:noinline func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { if size == 0 { - return unsafe.Pointer(&zeroSizedAlloc) + return alloc_zero(size, layout) } if interrupt.In() { diff --git a/src/runtime/gc_boehm.go b/src/runtime/gc_boehm.go index 3b5d2ac067..2b2159f5d1 100644 --- a/src/runtime/gc_boehm.go +++ b/src/runtime/gc_boehm.go @@ -26,9 +26,6 @@ import ( const needsStaticHeap = false -// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. -var zeroSizedAlloc uint8 - var gcLock task.PMutex func initHeap() { @@ -67,7 +64,7 @@ func markCurrentGoroutineStack(sp uintptr) { //go:noinline func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { if size == 0 { - return unsafe.Pointer(&zeroSizedAlloc) + return alloc_zero(size, layout) } gcLock.Lock() diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index 7197c43973..c5d73b104c 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -6,6 +6,8 @@ import "unsafe" const GOOS = "darwin" +const zeroSizeAllocPtr uintptr = 16 // part of the first protected page + const ( // See https://github.com/golang/go/blob/master/src/syscall/zerrors_darwin_amd64.go flag_PROT_READ = 0x1 diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 0ae105c5fc..97dd41e032 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -11,6 +11,8 @@ import ( const GOOS = "linux" +const zeroSizeAllocPtr uintptr = 16 // part of the first protected page + const ( // See https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/mman-common.h flag_PROT_READ = 0x1 diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go index a124e7ab14..ee8c0435a1 100644 --- a/src/runtime/os_windows.go +++ b/src/runtime/os_windows.go @@ -4,6 +4,8 @@ import "unsafe" const GOOS = "windows" +const zeroSizeAllocPtr uintptr = 16 // part of the first protected page + //export GetModuleHandleExA func _GetModuleHandleExA(dwFlags uint32, lpModuleName unsafe.Pointer, phModule **exeHeader) bool diff --git a/src/runtime/runtime_arm7tdmi.go b/src/runtime/runtime_arm7tdmi.go index fe0b648b5f..2dea3b27de 100644 --- a/src/runtime/runtime_arm7tdmi.go +++ b/src/runtime/runtime_arm7tdmi.go @@ -36,6 +36,8 @@ var _sidata [0]byte //go:extern _edata var _edata [0]byte +const zeroSizeAllocPtr uintptr = 16 // points somewhere in the BIOS which is not readable + // Entry point for Go. Initialize all packages and call main.main(). // //export main diff --git a/src/runtime/runtime_nintendoswitch.go b/src/runtime/runtime_nintendoswitch.go index 074e18287c..b2d13f86bb 100644 --- a/src/runtime/runtime_nintendoswitch.go +++ b/src/runtime/runtime_nintendoswitch.go @@ -4,6 +4,11 @@ package runtime import "unsafe" +// Not sure whether there is anything on this location, but it doesn't look like +// it according to the memory map: +// https://switchbrew.org/wiki/Memory_layout +const zeroSizeAllocPtr uintptr = 16 + const ( // Handles infoTypeTotalMemorySize = 6 // Total amount of memory available for process.