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  }