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.
This commit is contained in:
Ayke van Laethem
2026-05-25 18:55:36 +02:00
committed by Ron Evans
parent 221ea6a54c
commit 134de98ba5
18 changed files with 63 additions and 12 deletions
+2
View File
@@ -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 // points into the register file or I/O sapce (which is memory mapped but shouldn't be accessed directly)
const deferExtraRegs = 1 // the frame pointer (Y register) also needs to be stored
const callInstSize = 2 // "call" is 4 bytes, "rcall" is 2 bytes
+2
View File
@@ -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
+2
View File
@@ -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
+7
View File
@@ -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
+2
View File
@@ -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
+19
View File
@@ -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)
}
+1 -4
View File
@@ -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.
@@ -405,7 +402,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() {
+1 -4
View File
@@ -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()
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+5
View File
@@ -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.