
This is a big change: apart from removing LLVM 14 it also removes typed pointer support (which was only fully supported in LLVM up to version 14). This removes about 200 lines of code, but more importantly removes a ton of special cases for LLVM 14.
188 строки
6,1 КиБ
Go
188 строки
6,1 КиБ
Go
package transform
|
|
|
|
// This file implements an escape analysis pass. It looks for calls to
|
|
// runtime.alloc and replaces these calls with a stack allocation if the
|
|
// allocated value does not escape. It uses the LLVM nocapture flag for
|
|
// interprocedural escape analysis.
|
|
|
|
import (
|
|
"fmt"
|
|
"go/token"
|
|
"regexp"
|
|
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// maxStackAlloc is the maximum size of an object that will be allocated on the
|
|
// stack. Bigger objects have increased risk of stack overflows and thus will
|
|
// always be heap allocated.
|
|
//
|
|
// TODO: tune this, this is just a random value.
|
|
// This value is also used in the compiler when translating ssa.Alloc nodes.
|
|
const maxStackAlloc = 256
|
|
|
|
// OptimizeAllocs tries to replace heap allocations with stack allocations
|
|
// whenever possible. It relies on the LLVM 'nocapture' flag for interprocedural
|
|
// escape analysis, and within a function looks whether an allocation can escape
|
|
// to the heap.
|
|
// If printAllocs is non-nil, it indicates the regexp of functions for which a
|
|
// heap allocation explanation should be printed (why the object can't be stack
|
|
// allocated).
|
|
func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, logger func(token.Position, string)) {
|
|
allocator := mod.NamedFunction("runtime.alloc")
|
|
if allocator.IsNil() {
|
|
// nothing to optimize
|
|
return
|
|
}
|
|
|
|
targetData := llvm.NewTargetData(mod.DataLayout())
|
|
defer targetData.Dispose()
|
|
ptrType := llvm.PointerType(mod.Context().Int8Type(), 0)
|
|
builder := mod.Context().NewBuilder()
|
|
defer builder.Dispose()
|
|
|
|
for _, heapalloc := range getUses(allocator) {
|
|
logAllocs := printAllocs != nil && printAllocs.MatchString(heapalloc.InstructionParent().Parent().Name())
|
|
if heapalloc.Operand(0).IsAConstantInt().IsNil() {
|
|
// Do not allocate variable length arrays on the stack.
|
|
if logAllocs {
|
|
logAlloc(logger, heapalloc, "size is not constant")
|
|
}
|
|
continue
|
|
}
|
|
|
|
size := heapalloc.Operand(0).ZExtValue()
|
|
if size > maxStackAlloc {
|
|
// The maximum size for a stack allocation.
|
|
if logAllocs {
|
|
logAlloc(logger, heapalloc, fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc))
|
|
}
|
|
continue
|
|
}
|
|
|
|
if size == 0 {
|
|
// If the size is 0, the pointer is allowed to alias other
|
|
// zero-sized pointers. Use the pointer to the global that would
|
|
// also be returned by runtime.alloc.
|
|
zeroSizedAlloc := mod.NamedGlobal("runtime.zeroSizedAlloc")
|
|
if !zeroSizedAlloc.IsNil() {
|
|
heapalloc.ReplaceAllUsesWith(zeroSizedAlloc)
|
|
heapalloc.EraseFromParentAsInstruction()
|
|
}
|
|
continue
|
|
}
|
|
|
|
// In general the pattern is:
|
|
// %0 = call i8* @runtime.alloc(i32 %size, i8* null)
|
|
// %1 = bitcast i8* %0 to type*
|
|
// (use %1 only)
|
|
// But the bitcast might sometimes be dropped when allocating an *i8.
|
|
// The 'bitcast' variable below is thus usually a bitcast of the
|
|
// heapalloc but not always.
|
|
bitcast := heapalloc // instruction that creates the value
|
|
if uses := getUses(heapalloc); len(uses) == 1 && !uses[0].IsABitCastInst().IsNil() {
|
|
// getting only bitcast use
|
|
bitcast = uses[0]
|
|
}
|
|
|
|
if at := valueEscapesAt(bitcast); !at.IsNil() {
|
|
if logAllocs {
|
|
atPos := getPosition(at)
|
|
msg := "escapes at unknown line"
|
|
if atPos.Line != 0 {
|
|
msg = fmt.Sprintf("escapes at line %d", atPos.Line)
|
|
}
|
|
logAlloc(logger, heapalloc, msg)
|
|
}
|
|
continue
|
|
}
|
|
// The pointer value does not escape.
|
|
|
|
// Determine the appropriate alignment of the alloca. The size of the
|
|
// allocation gives us a hint what the alignment should be.
|
|
var alignment int
|
|
if size%2 != 0 {
|
|
alignment = 1
|
|
} else if size%4 != 0 {
|
|
alignment = 2
|
|
} else if size%8 != 0 {
|
|
alignment = 4
|
|
} else {
|
|
alignment = 8
|
|
}
|
|
if pointerAlignment := targetData.ABITypeAlignment(ptrType); pointerAlignment < alignment {
|
|
// Use min(alignment, alignof(void*)) as the alignment.
|
|
alignment = pointerAlignment
|
|
}
|
|
|
|
// Insert alloca in the entry block. Do it here so that mem2reg can
|
|
// promote it to a SSA value.
|
|
fn := bitcast.InstructionParent().Parent()
|
|
builder.SetInsertPointBefore(fn.EntryBasicBlock().FirstInstruction())
|
|
allocaType := llvm.ArrayType(mod.Context().Int8Type(), int(size))
|
|
alloca := builder.CreateAlloca(allocaType, "stackalloc")
|
|
alloca.SetAlignment(alignment)
|
|
|
|
// Zero the allocation inside the block where the value was originally allocated.
|
|
zero := llvm.ConstNull(alloca.AllocatedType())
|
|
builder.SetInsertPointBefore(bitcast)
|
|
store := builder.CreateStore(zero, alloca)
|
|
store.SetAlignment(alignment)
|
|
|
|
// Replace heap alloc bitcast with stack alloc bitcast.
|
|
bitcast.ReplaceAllUsesWith(alloca)
|
|
if heapalloc != bitcast {
|
|
bitcast.EraseFromParentAsInstruction()
|
|
}
|
|
heapalloc.EraseFromParentAsInstruction()
|
|
}
|
|
}
|
|
|
|
// valueEscapesAt returns the instruction where the given value may escape and a
|
|
// nil llvm.Value if it definitely doesn't. The value must be an instruction.
|
|
func valueEscapesAt(value llvm.Value) llvm.Value {
|
|
uses := getUses(value)
|
|
for _, use := range uses {
|
|
if use.IsAInstruction().IsNil() {
|
|
panic("expected instruction use")
|
|
}
|
|
switch use.InstructionOpcode() {
|
|
case llvm.GetElementPtr:
|
|
if at := valueEscapesAt(use); !at.IsNil() {
|
|
return at
|
|
}
|
|
case llvm.BitCast:
|
|
// A bitcast escapes if the casted-to value escapes.
|
|
if at := valueEscapesAt(use); !at.IsNil() {
|
|
return at
|
|
}
|
|
case llvm.Load:
|
|
// Load does not escape.
|
|
case llvm.Store:
|
|
// Store only escapes when the value is stored to, not when the
|
|
// value is stored into another value.
|
|
if use.Operand(0) == value {
|
|
return use
|
|
}
|
|
case llvm.Call:
|
|
if !hasFlag(use, value, "nocapture") {
|
|
return use
|
|
}
|
|
case llvm.ICmp:
|
|
// Comparing pointers don't let the pointer escape.
|
|
// This is often a compiler-inserted nil check.
|
|
default:
|
|
// Unknown instruction, might escape.
|
|
return use
|
|
}
|
|
}
|
|
|
|
// Checked all uses, and none let the pointer value escape.
|
|
return llvm.Value{}
|
|
}
|
|
|
|
// logAlloc prints a message to stderr explaining why the given object had to be
|
|
// allocated on the heap.
|
|
func logAlloc(logger func(token.Position, string), allocCall llvm.Value, reason string) {
|
|
logger(getPosition(allocCall), "object allocated on the heap: "+reason)
|
|
}
|