From 7b6ef65fe7b927594e4f62cb7a7334137f4c2d9e Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 19 May 2019 18:36:27 +0200 Subject: [PATCH] compiler: create temporary allocas with appropriate lifetimes Make sure all allocas are created in the entry block and are given the right lifetimes. This is good for code quality: * Moving allocas to the entry block makes sure they are always allocated statically (avoiding the need for a frame pointer) and do not grow the stack on each new alloca instruction. This is especially useful in loops where it could otherwise lead to a stack overflow even though there is no recursion. * Adding lifetime markers allows LLVM to reuse stack areas for different allocas as long as their lifetimes do not overlap. All in all, this reduces code size in all tested cases for the BBC micro:bit, and reduces code size for most cases for WebAssembly. --- compiler/channel.go | 32 +++++++++++++------------------- compiler/compiler.go | 32 ++++++++++++++++++-------------- compiler/llvm.go | 36 +++++++++++++++++++++++++++--------- compiler/map.go | 20 ++++++++++---------- compiler/wordpack.go | 17 +++++++++++++---- 5 files changed, 81 insertions(+), 56 deletions(-) diff --git a/compiler/channel.go b/compiler/channel.go index 8f1a86036..b5bcd95b9 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -23,47 +23,41 @@ func (c *Compiler) emitMakeChan(expr *ssa.MakeChan) (llvm.Value, error) { // emitChanSend emits a pseudo chan send operation. It is lowered to the actual // channel send operation during goroutine lowering. func (c *Compiler) emitChanSend(frame *Frame, instr *ssa.Send) { - valueType := c.getLLVMType(instr.X.Type()) ch := c.getValue(frame, instr.Chan) chanValue := c.getValue(frame, instr.X) - valueSize := llvm.ConstInt(c.uintptrType, c.targetData.TypeAllocSize(chanValue.Type()), false) - coroutine := c.createRuntimeCall("getCoroutine", nil, "") // store value-to-send - c.builder.SetInsertPointBefore(coroutine.InstructionParent().Parent().EntryBasicBlock().FirstInstruction()) - valueAlloca := c.builder.CreateAlloca(valueType, "chan.value") - c.builder.SetInsertPointBefore(coroutine) - c.builder.SetInsertPointAtEnd(coroutine.InstructionParent()) + valueType := c.getLLVMType(instr.X.Type()) + valueAlloca, valueAllocaCast, valueAllocaSize := c.createTemporaryAlloca(valueType, "chan.value") c.builder.CreateStore(chanValue, valueAlloca) - valueAllocaCast := c.builder.CreateBitCast(valueAlloca, c.i8ptrType, "chan.value.i8ptr") // Do the send. + coroutine := c.createRuntimeCall("getCoroutine", nil, "") + valueSize := llvm.ConstInt(c.uintptrType, c.targetData.TypeAllocSize(chanValue.Type()), false) c.createRuntimeCall("chanSend", []llvm.Value{coroutine, ch, valueAllocaCast, valueSize}, "") - // Make sure CoroSplit includes the alloca in the coroutine frame. - // This is a bit dirty, but it works (at least in LLVM 8). - valueSizeI64 := llvm.ConstInt(c.ctx.Int64Type(), c.targetData.TypeAllocSize(chanValue.Type()), false) - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{valueSizeI64, valueAllocaCast}, "") + // End the lifetime of the alloca. + // This also works around a bug in CoroSplit, at least in LLVM 8: + // https://bugs.llvm.org/show_bug.cgi?id=41742 + c.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) } // emitChanRecv emits a pseudo chan receive operation. It is lowered to the // actual channel receive operation during goroutine lowering. func (c *Compiler) emitChanRecv(frame *Frame, unop *ssa.UnOp) llvm.Value { valueType := c.getLLVMType(unop.X.Type().(*types.Chan).Elem()) - valueSize := llvm.ConstInt(c.uintptrType, c.targetData.TypeAllocSize(valueType), false) ch := c.getValue(frame, unop.X) - coroutine := c.createRuntimeCall("getCoroutine", nil, "") // Allocate memory to receive into. - c.builder.SetInsertPointBefore(coroutine.InstructionParent().Parent().EntryBasicBlock().FirstInstruction()) - valueAlloca := c.builder.CreateAlloca(valueType, "chan.value") - c.builder.SetInsertPointBefore(coroutine) - c.builder.SetInsertPointAtEnd(coroutine.InstructionParent()) - valueAllocaCast := c.builder.CreateBitCast(valueAlloca, c.i8ptrType, "chan.value.i8ptr") + valueAlloca, valueAllocaCast, valueAllocaSize := c.createTemporaryAlloca(valueType, "chan.value") // Do the receive. + coroutine := c.createRuntimeCall("getCoroutine", nil, "") + valueSize := llvm.ConstInt(c.uintptrType, c.targetData.TypeAllocSize(valueType), false) c.createRuntimeCall("chanRecv", []llvm.Value{coroutine, ch, valueAllocaCast, valueSize}, "") received := c.builder.CreateLoad(valueAlloca, "chan.received") + c.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) + if unop.CommaOk { commaOk := c.createRuntimeCall("getTaskPromiseData", []llvm.Value{coroutine}, "chan.commaOk.wide") commaOk = c.builder.CreateTrunc(commaOk, c.ctx.Int1Type(), "chan.commaOk") diff --git a/compiler/compiler.go b/compiler/compiler.go index 5f09e54a4..aebea4c6c 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1369,7 +1369,6 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { switch expr := expr.(type) { case *ssa.Alloc: typ := c.getLLVMType(expr.Type().Underlying().(*types.Pointer).Elem()) - var buf llvm.Value if expr.Heap { size := c.targetData.TypeAllocSize(typ) // Calculate ^uintptr(0) @@ -1380,15 +1379,16 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { } // TODO: escape analysis sizeValue := llvm.ConstInt(c.uintptrType, size, false) - buf = c.createRuntimeCall("alloc", []llvm.Value{sizeValue}, expr.Comment) + buf := c.createRuntimeCall("alloc", []llvm.Value{sizeValue}, expr.Comment) buf = c.builder.CreateBitCast(buf, llvm.PointerType(typ, 0), "") + return buf, nil } else { - buf = c.builder.CreateAlloca(typ, expr.Comment) + buf := c.createEntryBlockAlloca(typ, expr.Comment) if c.targetData.TypeAllocSize(typ) != 0 { c.builder.CreateStore(c.getZeroValue(typ), buf) // zero-initialize var } + return buf, nil } - return buf, nil case *ssa.BinOp: x := c.getValue(frame, expr.X) y := c.getValue(frame, expr.Y) @@ -1451,10 +1451,12 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { // This could be done directly, but as this is a very infrequent // operation it's much easier to bitcast it through an alloca. resultType := c.getLLVMType(expr.Type()) - alloca := c.builder.CreateAlloca(value.Type(), "") + alloca, allocaPtr, allocaSize := c.createTemporaryAlloca(value.Type(), "union.alloca") c.builder.CreateStore(value, alloca) - bitcast := c.builder.CreateBitCast(alloca, llvm.PointerType(resultType, 0), "") - return c.builder.CreateLoad(bitcast, ""), nil + bitcast := c.builder.CreateBitCast(alloca, llvm.PointerType(resultType, 0), "union.bitcast") + result := c.builder.CreateLoad(bitcast, "union.result") + c.emitLifetimeEnd(allocaPtr, allocaSize) + return result, nil } result := c.builder.CreateExtractValue(value, expr.Field, "") return result, nil @@ -1494,11 +1496,13 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { // Can't load directly from array (as index is non-constant), so have to // do it using an alloca+gep+load. - alloca := c.builder.CreateAlloca(array.Type(), "index.alloca") + alloca, allocaPtr, allocaSize := c.createTemporaryAlloca(array.Type(), "index.alloca") c.builder.CreateStore(array, alloca) zero := llvm.ConstInt(c.ctx.Int32Type(), 0, false) ptr := c.builder.CreateInBoundsGEP(alloca, []llvm.Value{zero, index}, "index.gep") - return c.builder.CreateLoad(ptr, "index.load"), nil + result := c.builder.CreateLoad(ptr, "index.load") + c.emitLifetimeEnd(allocaPtr, allocaSize) + return result, nil case *ssa.IndexAddr: val := c.getValue(frame, expr.X) index := c.getValue(frame, expr.Index) @@ -1658,16 +1662,16 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { llvmKeyType := c.getLLVMType(rangeVal.Type().Underlying().(*types.Map).Key()) llvmValueType := c.getLLVMType(rangeVal.Type().Underlying().(*types.Map).Elem()) - mapKeyAlloca := c.builder.CreateAlloca(llvmKeyType, "range.key") - mapKeyPtr := c.builder.CreateBitCast(mapKeyAlloca, c.i8ptrType, "range.keyptr") - mapValueAlloca := c.builder.CreateAlloca(llvmValueType, "range.value") - mapValuePtr := c.builder.CreateBitCast(mapValueAlloca, c.i8ptrType, "range.valueptr") + mapKeyAlloca, mapKeyPtr, mapKeySize := c.createTemporaryAlloca(llvmKeyType, "range.key") + mapValueAlloca, mapValuePtr, mapValueSize := c.createTemporaryAlloca(llvmValueType, "range.value") ok := c.createRuntimeCall("hashmapNext", []llvm.Value{llvmRangeVal, it, mapKeyPtr, mapValuePtr}, "range.next") tuple := llvm.Undef(c.ctx.StructType([]llvm.Type{c.ctx.Int1Type(), llvmKeyType, llvmValueType}, false)) tuple = c.builder.CreateInsertValue(tuple, ok, 0, "") tuple = c.builder.CreateInsertValue(tuple, c.builder.CreateLoad(mapKeyAlloca, ""), 1, "") tuple = c.builder.CreateInsertValue(tuple, c.builder.CreateLoad(mapValueAlloca, ""), 2, "") + c.emitLifetimeEnd(mapKeyPtr, mapKeySize) + c.emitLifetimeEnd(mapValuePtr, mapValueSize) return tuple, nil } case *ssa.Phi: @@ -1684,7 +1688,7 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { default: panic("unknown type in range: " + typ.String()) } - it := c.builder.CreateAlloca(iteratorType, "range.it") + it, _, _ := c.createTemporaryAlloca(iteratorType, "range.it") c.builder.CreateStore(c.getZeroValue(iteratorType), it) return it, nil case *ssa.Select: diff --git a/compiler/llvm.go b/compiler/llvm.go index a49573456..fb52b0069 100644 --- a/compiler/llvm.go +++ b/compiler/llvm.go @@ -24,23 +24,41 @@ func getUses(value llvm.Value) []llvm.Value { // createEntryBlockAlloca creates a new alloca in the entry block, even though // the IR builder is located elsewhere. It assumes that the insert point is -// after the last instruction in the current block. Also, it adds lifetime -// information to the IR signalling that the alloca won't be used before this -// point. +// at the end of the current block. +func (c *Compiler) createEntryBlockAlloca(t llvm.Type, name string) llvm.Value { + currentBlock := c.builder.GetInsertBlock() + entryBlock := currentBlock.Parent().EntryBasicBlock() + if entryBlock.FirstInstruction().IsNil() { + c.builder.SetInsertPointAtEnd(entryBlock) + } else { + c.builder.SetInsertPointBefore(entryBlock.FirstInstruction()) + } + alloca := c.builder.CreateAlloca(t, name) + c.builder.SetInsertPointAtEnd(currentBlock) + return alloca +} + +// createTemporaryAlloca creates a new alloca in the entry block and adds +// lifetime start infromation in the IR signalling that the alloca won't be used +// before this point. // // This is useful for creating temporary allocas for intrinsics. Don't forget to -// end the lifetime after you're done with it. -func (c *Compiler) createEntryBlockAlloca(t llvm.Type, name string) (alloca, bitcast, size llvm.Value) { - currentBlock := c.builder.GetInsertBlock() - c.builder.SetInsertPointBefore(currentBlock.Parent().EntryBasicBlock().FirstInstruction()) - alloca = c.builder.CreateAlloca(t, name) - c.builder.SetInsertPointAtEnd(currentBlock) +// end the lifetime using emitLifetimeEnd after you're done with it. +func (c *Compiler) createTemporaryAlloca(t llvm.Type, name string) (alloca, bitcast, size llvm.Value) { + alloca = c.createEntryBlockAlloca(t, name) bitcast = c.builder.CreateBitCast(alloca, c.i8ptrType, name+".bitcast") size = llvm.ConstInt(c.ctx.Int64Type(), c.targetData.TypeAllocSize(t), false) c.builder.CreateCall(c.getLifetimeStartFunc(), []llvm.Value{size, bitcast}, "") return } +// emitLifetimeEnd signals the end of an (alloca) lifetime by calling the +// llvm.lifetime.end intrinsic. It is commonly used together with +// createTemporaryAlloca. +func (c *Compiler) emitLifetimeEnd(ptr, size llvm.Value) { + c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{size, ptr}, "") +} + // getLifetimeStartFunc returns the llvm.lifetime.start intrinsic and creates it // first if it doesn't exist yet. func (c *Compiler) getLifetimeStartFunc() llvm.Value { diff --git a/compiler/map.go b/compiler/map.go index da8e68743..3f0d95237 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -15,7 +15,7 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu // Allocate the memory for the resulting type. Do not zero this memory: it // will be zeroed by the hashmap get implementation if the key is not // present in the map. - mapValueAlloca, mapValuePtr, mapValueSize := c.createEntryBlockAlloca(llvmValueType, "hashmap.value") + mapValueAlloca, mapValuePtr, mapValueSize := c.createTemporaryAlloca(llvmValueType, "hashmap.value") // Do the lookup. How it is done depends on the key type. var commaOkValue llvm.Value @@ -27,12 +27,12 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu // key can be compared with runtime.memequal // Store the key in an alloca, in the entry block to avoid dynamic stack // growth. - mapKeyAlloca, mapKeyPtr, mapKeySize := c.createEntryBlockAlloca(key.Type(), "hashmap.key") + mapKeyAlloca, mapKeyPtr, mapKeySize := c.createTemporaryAlloca(key.Type(), "hashmap.key") c.builder.CreateStore(key, mapKeyAlloca) // Fetch the value from the hashmap. params := []llvm.Value{m, mapKeyPtr, mapValuePtr} commaOkValue = c.createRuntimeCall("hashmapBinaryGet", params, "") - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{mapKeySize, mapKeyPtr}, "") + c.emitLifetimeEnd(mapKeyPtr, mapKeySize) } else { // Not trivially comparable using memcmp. return llvm.Value{}, c.makeError(pos, "only strings, bools, ints or structs of bools/ints are supported as map keys, but got: "+keyType.String()) @@ -41,7 +41,7 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu // Load the resulting value from the hashmap. The value is set to the zero // value if the key doesn't exist in the hashmap. mapValue := c.builder.CreateLoad(mapValueAlloca, "") - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{mapValueSize, mapValuePtr}, "") + c.emitLifetimeEnd(mapValuePtr, mapValueSize) if commaOk { tuple := llvm.Undef(c.ctx.StructType([]llvm.Type{llvmValueType, c.ctx.Int1Type()}, false)) @@ -54,7 +54,7 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu } func (c *Compiler) emitMapUpdate(keyType types.Type, m, key, value llvm.Value, pos token.Pos) { - valueAlloca, valuePtr, valueSize := c.createEntryBlockAlloca(value.Type(), "hashmap.value") + valueAlloca, valuePtr, valueSize := c.createTemporaryAlloca(value.Type(), "hashmap.value") c.builder.CreateStore(value, valueAlloca) keyType = keyType.Underlying() if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { @@ -63,15 +63,15 @@ func (c *Compiler) emitMapUpdate(keyType types.Type, m, key, value llvm.Value, p c.createRuntimeCall("hashmapStringSet", params, "") } else if hashmapIsBinaryKey(keyType) { // key can be compared with runtime.memequal - keyAlloca, keyPtr, keySize := c.createEntryBlockAlloca(key.Type(), "hashmap.key") + keyAlloca, keyPtr, keySize := c.createTemporaryAlloca(key.Type(), "hashmap.key") c.builder.CreateStore(key, keyAlloca) params := []llvm.Value{m, keyPtr, valuePtr} c.createRuntimeCall("hashmapBinarySet", params, "") - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{keySize, keyPtr}, "") + c.emitLifetimeEnd(keyPtr, keySize) } else { c.addError(pos, "only strings, bools, ints or structs of bools/ints are supported as map keys, but got: "+keyType.String()) } - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{valueSize, valuePtr}, "") + c.emitLifetimeEnd(valuePtr, valueSize) } func (c *Compiler) emitMapDelete(keyType types.Type, m, key llvm.Value, pos token.Pos) error { @@ -82,11 +82,11 @@ func (c *Compiler) emitMapDelete(keyType types.Type, m, key llvm.Value, pos toke c.createRuntimeCall("hashmapStringDelete", params, "") return nil } else if hashmapIsBinaryKey(keyType) { - keyAlloca, keyPtr, keySize := c.createEntryBlockAlloca(key.Type(), "hashmap.key") + keyAlloca, keyPtr, keySize := c.createTemporaryAlloca(key.Type(), "hashmap.key") c.builder.CreateStore(key, keyAlloca) params := []llvm.Value{m, keyPtr} c.createRuntimeCall("hashmapBinaryDelete", params, "") - c.builder.CreateCall(c.getLifetimeEndFunc(), []llvm.Value{keySize, keyPtr}, "") + c.emitLifetimeEnd(keyPtr, keySize) return nil } else { return c.makeError(pos, "only strings, bools, ints or structs of bools/ints are supported as map keys, but got: "+keyType.String()) diff --git a/compiler/wordpack.go b/compiler/wordpack.go index caeb7956c..b76bf6a35 100644 --- a/compiler/wordpack.go +++ b/compiler/wordpack.go @@ -34,7 +34,7 @@ func (c *Compiler) emitPointerPack(values []llvm.Value) llvm.Value { } // Because packedType is a struct and we have to cast it to a *i8, store // it in an alloca first for bitcasting (store+bitcast+load). - packedAlloc = c.builder.CreateAlloca(packedType, "") + packedAlloc, _, _ = c.createTemporaryAlloca(packedType, "") } else { // Packed data is bigger than a pointer, so allocate it on the heap. sizeValue := llvm.ConstInt(c.uintptrType, size, false) @@ -54,7 +54,11 @@ func (c *Compiler) emitPointerPack(values []llvm.Value) llvm.Value { if packedHeapAlloc.IsNil() { // Load value (as *i8) from the alloca. packedAlloc = c.builder.CreateBitCast(packedAlloc, llvm.PointerType(c.i8ptrType, 0), "") - return c.builder.CreateLoad(packedAlloc, "") + result := c.builder.CreateLoad(packedAlloc, "") + packedPtr := c.builder.CreateBitCast(packedAlloc, c.i8ptrType, "") + packedSize := llvm.ConstInt(c.ctx.Int64Type(), c.targetData.TypeAllocSize(packedAlloc.Type()), false) + c.emitLifetimeEnd(packedPtr, packedSize) + return result } else { // Get the original heap allocation pointer, which already is an *i8. return packedHeapAlloc @@ -66,7 +70,7 @@ func (c *Compiler) emitPointerUnpack(ptr llvm.Value, valueTypes []llvm.Type) []l packedType := c.ctx.StructType(valueTypes, false) // Get a correctly-typed pointer to the packed data. - var packedAlloc llvm.Value + var packedAlloc, packedRawAlloc llvm.Value size := c.targetData.TypeAllocSize(packedType) if size == 0 { // No data to unpack. @@ -80,7 +84,7 @@ func (c *Compiler) emitPointerUnpack(ptr llvm.Value, valueTypes []llvm.Type) []l return []llvm.Value{c.builder.CreatePtrToInt(ptr, valueTypes[0], "unpack.int")} } // Fallback: load it using an alloca. - packedRawAlloc := c.builder.CreateAlloca(llvm.PointerType(c.i8ptrType, 0), "unpack.raw.alloc") + packedRawAlloc, _, _ = c.createTemporaryAlloca(llvm.PointerType(c.i8ptrType, 0), "unpack.raw.alloc") packedRawValue := c.builder.CreateBitCast(ptr, llvm.PointerType(c.i8ptrType, 0), "unpack.raw.value") c.builder.CreateStore(packedRawValue, packedRawAlloc) packedAlloc = c.builder.CreateBitCast(packedRawAlloc, llvm.PointerType(packedType, 0), "unpack.alloc") @@ -104,5 +108,10 @@ func (c *Compiler) emitPointerUnpack(ptr llvm.Value, valueTypes []llvm.Type) []l gep := c.builder.CreateInBoundsGEP(packedAlloc, indices, "") values[i] = c.builder.CreateLoad(gep, "") } + if !packedRawAlloc.IsNil() { + allocPtr := c.builder.CreateBitCast(packedRawAlloc, c.i8ptrType, "") + allocSize := llvm.ConstInt(c.ctx.Int64Type(), c.targetData.TypeAllocSize(c.uintptrType), false) + c.emitLifetimeEnd(allocPtr, allocSize) + } return values }