|
|
|
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()
|
|
|
|
i8ptrType := 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(i8ptrType); 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")
|
|
|
|
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.
|
|
|
|
stackalloc := builder.CreateBitCast(alloca, bitcast.Type(), "stackalloc")
|
|
|
|
bitcast.ReplaceAllUsesWith(stackalloc)
|
|
|
|
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)
|
|
|
|
}
|