github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/transform/allocs.go (about) 1 package transform 2 3 // This file implements an escape analysis pass. It looks for calls to 4 // runtime.alloc and replaces these calls with a stack allocation if the 5 // allocated value does not escape. It uses the LLVM nocapture flag for 6 // interprocedural escape analysis. 7 8 import ( 9 "fmt" 10 "go/token" 11 "regexp" 12 13 "tinygo.org/x/go-llvm" 14 ) 15 16 // OptimizeAllocs tries to replace heap allocations with stack allocations 17 // whenever possible. It relies on the LLVM 'nocapture' flag for interprocedural 18 // escape analysis, and within a function looks whether an allocation can escape 19 // to the heap. 20 // If printAllocs is non-nil, it indicates the regexp of functions for which a 21 // heap allocation explanation should be printed (why the object can't be stack 22 // allocated). 23 func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc uint64, logger func(token.Position, string)) { 24 allocator := mod.NamedFunction("runtime.alloc") 25 if allocator.IsNil() { 26 // nothing to optimize 27 return 28 } 29 30 targetData := llvm.NewTargetData(mod.DataLayout()) 31 defer targetData.Dispose() 32 ptrType := llvm.PointerType(mod.Context().Int8Type(), 0) 33 builder := mod.Context().NewBuilder() 34 defer builder.Dispose() 35 36 for _, heapalloc := range getUses(allocator) { 37 logAllocs := printAllocs != nil && printAllocs.MatchString(heapalloc.InstructionParent().Parent().Name()) 38 if heapalloc.Operand(0).IsAConstantInt().IsNil() { 39 // Do not allocate variable length arrays on the stack. 40 if logAllocs { 41 logAlloc(logger, heapalloc, "size is not constant") 42 } 43 continue 44 } 45 46 size := heapalloc.Operand(0).ZExtValue() 47 if size > maxStackAlloc { 48 // The maximum size for a stack allocation. 49 if logAllocs { 50 logAlloc(logger, heapalloc, fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc)) 51 } 52 continue 53 } 54 55 if size == 0 { 56 // If the size is 0, the pointer is allowed to alias other 57 // zero-sized pointers. Use the pointer to the global that would 58 // also be returned by runtime.alloc. 59 zeroSizedAlloc := mod.NamedGlobal("runtime.zeroSizedAlloc") 60 if !zeroSizedAlloc.IsNil() { 61 heapalloc.ReplaceAllUsesWith(zeroSizedAlloc) 62 heapalloc.EraseFromParentAsInstruction() 63 } 64 continue 65 } 66 67 // In general the pattern is: 68 // %0 = call i8* @runtime.alloc(i32 %size, i8* null) 69 // %1 = bitcast i8* %0 to type* 70 // (use %1 only) 71 // But the bitcast might sometimes be dropped when allocating an *i8. 72 // The 'bitcast' variable below is thus usually a bitcast of the 73 // heapalloc but not always. 74 bitcast := heapalloc // instruction that creates the value 75 if uses := getUses(heapalloc); len(uses) == 1 && !uses[0].IsABitCastInst().IsNil() { 76 // getting only bitcast use 77 bitcast = uses[0] 78 } 79 80 if at := valueEscapesAt(bitcast); !at.IsNil() { 81 if logAllocs { 82 atPos := getPosition(at) 83 msg := "escapes at unknown line" 84 if atPos.Line != 0 { 85 msg = fmt.Sprintf("escapes at line %d", atPos.Line) 86 } 87 logAlloc(logger, heapalloc, msg) 88 } 89 continue 90 } 91 // The pointer value does not escape. 92 93 // Determine the appropriate alignment of the alloca. The size of the 94 // allocation gives us a hint what the alignment should be. 95 var alignment int 96 if size%2 != 0 { 97 alignment = 1 98 } else if size%4 != 0 { 99 alignment = 2 100 } else if size%8 != 0 { 101 alignment = 4 102 } else { 103 alignment = 8 104 } 105 if pointerAlignment := targetData.ABITypeAlignment(ptrType); pointerAlignment < alignment { 106 // Use min(alignment, alignof(void*)) as the alignment. 107 alignment = pointerAlignment 108 } 109 110 // Insert alloca in the entry block. Do it here so that mem2reg can 111 // promote it to a SSA value. 112 fn := bitcast.InstructionParent().Parent() 113 builder.SetInsertPointBefore(fn.EntryBasicBlock().FirstInstruction()) 114 allocaType := llvm.ArrayType(mod.Context().Int8Type(), int(size)) 115 alloca := builder.CreateAlloca(allocaType, "stackalloc") 116 alloca.SetAlignment(alignment) 117 118 // Zero the allocation inside the block where the value was originally allocated. 119 zero := llvm.ConstNull(alloca.AllocatedType()) 120 builder.SetInsertPointBefore(bitcast) 121 store := builder.CreateStore(zero, alloca) 122 store.SetAlignment(alignment) 123 124 // Replace heap alloc bitcast with stack alloc bitcast. 125 bitcast.ReplaceAllUsesWith(alloca) 126 if heapalloc != bitcast { 127 bitcast.EraseFromParentAsInstruction() 128 } 129 heapalloc.EraseFromParentAsInstruction() 130 } 131 } 132 133 // valueEscapesAt returns the instruction where the given value may escape and a 134 // nil llvm.Value if it definitely doesn't. The value must be an instruction. 135 func valueEscapesAt(value llvm.Value) llvm.Value { 136 uses := getUses(value) 137 for _, use := range uses { 138 if use.IsAInstruction().IsNil() { 139 panic("expected instruction use") 140 } 141 switch use.InstructionOpcode() { 142 case llvm.GetElementPtr: 143 if at := valueEscapesAt(use); !at.IsNil() { 144 return at 145 } 146 case llvm.BitCast: 147 // A bitcast escapes if the casted-to value escapes. 148 if at := valueEscapesAt(use); !at.IsNil() { 149 return at 150 } 151 case llvm.Load: 152 // Load does not escape. 153 case llvm.Store: 154 // Store only escapes when the value is stored to, not when the 155 // value is stored into another value. 156 if use.Operand(0) == value { 157 return use 158 } 159 case llvm.Call: 160 if !hasFlag(use, value, "nocapture") { 161 return use 162 } 163 case llvm.ICmp: 164 // Comparing pointers don't let the pointer escape. 165 // This is often a compiler-inserted nil check. 166 default: 167 // Unknown instruction, might escape. 168 return use 169 } 170 } 171 172 // Checked all uses, and none let the pointer value escape. 173 return llvm.Value{} 174 } 175 176 // logAlloc prints a message to stderr explaining why the given object had to be 177 // allocated on the heap. 178 func logAlloc(logger func(token.Position, string), allocCall llvm.Value, reason string) { 179 logger(getPosition(allocCall), "object allocated on the heap: "+reason) 180 }