Skip to content

Commit 8ae64a0

Browse files
committed
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.
1 parent d8a21ed commit 8ae64a0

17 files changed

Lines changed: 62 additions & 11 deletions

compiler/gc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ func (b *builder) createAlloc(sizeValue, layoutValue llvm.Value, align int, comm
2020
allocFunc = "alloc_noheap"
2121
}
2222

23+
// Allocs that don't allocate anything can return an architecture-specific
24+
// sentinel value.
25+
if !sizeValue.IsAConstantInt().IsNil() && sizeValue.ZExtValue() == 0 {
26+
allocFunc = "alloc_zero"
27+
}
28+
29+
// Make the runtime call.
2330
call := b.createRuntimeCall(allocFunc, []llvm.Value{sizeValue, layoutValue}, comment)
2431
if align != 0 {
2532
// TODO: make sure all callsites set the correct alignment.

compiler/symbol.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value)
162162
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0))
163163
case "machine.keepAliveNoEscape", "machine.unsafeNoEscape":
164164
llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0))
165-
case "runtime.alloc", "runtime.alloc_noheap":
165+
case "runtime.alloc", "runtime.alloc_noheap", "runtime.alloc_zero":
166166
// Tell the optimizer that runtime.alloc is an allocator, meaning that it
167167
// returns values that are never null and never alias to an existing value.
168168
for _, attrName := range []string{"noalias", "nonnull"} {

compiler/testdata/gc.ll

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ entry:
7575
define hidden void @main.newStruct(ptr %context) unnamed_addr #1 {
7676
entry:
7777
%stackalloc = alloca i8, align 1
78-
%new = call align 1 ptr @runtime.alloc(i32 0, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3
78+
%new = call align 1 ptr @runtime.alloc_zero(i32 0, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3
7979
call void @runtime.trackPointer(ptr nonnull %new, ptr nonnull %stackalloc, ptr undef) #3
8080
store ptr %new, ptr @main.struct1, align 4
8181
%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:
9393
ret void
9494
}
9595

96+
; Function Attrs: allockind("alloc,zeroed") allocsize(0)
97+
declare noalias nonnull ptr @runtime.alloc_zero(i32, ptr, ptr) #2
98+
9699
; Function Attrs: nounwind
97100
define hidden ptr @main.newFuncValue(ptr %context) unnamed_addr #1 {
98101
entry:

interp/interpreter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
299299
// means that monotonic time in the time package is counted from
300300
// time.Time{}.Sub(1), which should be fine.
301301
locals[inst.localIndex] = literalValue{uint64(0)}
302-
case callFn.name == "runtime.alloc" || callFn.name == "runtime.alloc_noheap":
302+
case callFn.name == "runtime.alloc" || callFn.name == "runtime.alloc_noheap" || callFn.name == "runtime.alloc_zero":
303303
// Allocate heap memory. At compile time, this is instead done
304304
// by creating a global variable.
305305

src/runtime/arch_avr.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const GOARCH = "arm" // avr pretends to be arm
99
// The bitness of the CPU (e.g. 8, 32, 64).
1010
const TargetBits = 8
1111

12+
const zeroSizeAllocPtr uintptr = 16 // part of the first protected page
13+
1214
const deferExtraRegs = 1 // the frame pointer (Y register) also needs to be stored
1315

1416
const callInstSize = 2 // "call" is 4 bytes, "rcall" is 2 bytes

src/runtime/arch_cortexm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const GOARCH = "arm"
1111
// The bitness of the CPU (e.g. 8, 32, 64).
1212
const TargetBits = 32
1313

14+
const zeroSizeAllocPtr uintptr = 16 // part of the interrupt vector
15+
1416
const deferExtraRegs = 0
1517

1618
const callInstSize = 4 // "bl someFunction" is 4 bytes

src/runtime/arch_tinygoriscv.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package runtime
44

55
import "device/riscv"
66

7+
const zeroSizeAllocPtr uintptr = 0xffff_fff0 // should be unused on most RISC-V chips
8+
79
const deferExtraRegs = 0
810

911
const callInstSize = 4 // 8 without relaxation, maybe 4 with relaxation

src/runtime/arch_tinygowasm.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const GOARCH = "wasm"
1111
// The bitness of the CPU (e.g. 8, 32, 64).
1212
const TargetBits = 32
1313

14+
// zeroSizedAlloc a sentinel that gets returned when allocating 0 bytes.
15+
// Using this instead of a constant value since I can't easily find a memory
16+
// location that is definitely not going to end up as a valid pointer.
17+
var zeroSizedAlloc uint8
18+
19+
var zeroSizeAllocPtr = &zeroSizedAlloc
20+
1421
const deferExtraRegs = 0
1522

1623
const callInstSize = 1 // unknown and irrelevant (llvm.returnaddress doesn't work), so make something up

src/runtime/arch_xtensa.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import "device"
66

77
const GOARCH = "arm" // xtensa pretends to be arm
88

9+
const zeroSizeAllocPtr uintptr = 16 // part of early flash: partition table, etc
10+
911
// The bitness of the CPU (e.g. 8, 32, 64).
1012
const TargetBits = 32
1113

src/runtime/gc.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,22 @@ import "unsafe"
88
// It is used instead of normal alloc in //go:noheap functions, and must either
99
// be optimized away or throw a linker error.
1010
func alloc_noheap(size uintptr, layout unsafe.Pointer) unsafe.Pointer
11+
12+
// Special alloc function that returns a sentinel value that can never be on the
13+
// heap or match any other valid pointer. An alloc(0, xxx) call can be safely
14+
// converted to an alloc_zero(0, xxx) call as an optimization.
15+
//
16+
// It is always a good idea to inline this function, since the result is a
17+
// constant. Marking it as go:inline to be sure even though the compiler should
18+
// already be doing this.
19+
//
20+
//go:inline
21+
func alloc_zero(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
22+
// Returning a constant here is safe, since the Go spec does not require
23+
// multiple zero-sized allocations to be unequal when compared for equality:
24+
//
25+
// > Pointers to distinct zero-size variables may or may not be equal.
26+
//
27+
// Source: https://go.dev/ref/spec#Comparison_operators
28+
return unsafe.Pointer(zeroSizeAllocPtr)
29+
}

0 commit comments

Comments
 (0)