From 6315db21f7d85f4766589d5b4c8ca6c54aa8e7a4 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 8 Sep 2021 17:07:10 +0200 Subject: [PATCH] compiler: avoid zero-sized alloca in channel operations This works around a bug in LLVM (https://bugs.llvm.org/show_bug.cgi?id=49916) but seems like a good change in general. --- compiler/channel.go | 37 +++++++++--- compiler/compiler.go | 2 +- compiler/compiler_test.go | 1 + compiler/testdata/channel.go | 25 ++++++++ compiler/testdata/channel.ll | 114 +++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 compiler/testdata/channel.go create mode 100644 compiler/testdata/channel.ll diff --git a/compiler/channel.go b/compiler/channel.go index a036fbf4..a9886e10 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -32,8 +32,15 @@ func (b *builder) createChanSend(instr *ssa.Send) { // store value-to-send valueType := b.getLLVMType(instr.X.Type()) - valueAlloca, valueAllocaCast, valueAllocaSize := b.createTemporaryAlloca(valueType, "chan.value") - b.CreateStore(chanValue, valueAlloca) + isZeroSize := b.targetData.TypeAllocSize(valueType) == 0 + var valueAlloca, valueAllocaCast, valueAllocaSize llvm.Value + if isZeroSize { + valueAlloca = llvm.ConstNull(llvm.PointerType(valueType, 0)) + valueAllocaCast = llvm.ConstNull(b.i8ptrType) + } else { + valueAlloca, valueAllocaCast, valueAllocaSize = b.createTemporaryAlloca(valueType, "chan.value") + b.CreateStore(chanValue, valueAlloca) + } // Allocate blockedlist buffer. channelBlockedList := b.mod.GetTypeByName("runtime.channelBlockedList") @@ -46,7 +53,9 @@ func (b *builder) createChanSend(instr *ssa.Send) { // This also works around a bug in CoroSplit, at least in LLVM 8: // https://bugs.llvm.org/show_bug.cgi?id=41742 b.emitLifetimeEnd(channelBlockedListAllocaCast, channelBlockedListAllocaSize) - b.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) + if !isZeroSize { + b.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) + } } // createChanRecv emits a pseudo chan receive operation. It is lowered to the @@ -56,7 +65,14 @@ func (b *builder) createChanRecv(unop *ssa.UnOp) llvm.Value { ch := b.getValue(unop.X) // Allocate memory to receive into. - valueAlloca, valueAllocaCast, valueAllocaSize := b.createTemporaryAlloca(valueType, "chan.value") + isZeroSize := b.targetData.TypeAllocSize(valueType) == 0 + var valueAlloca, valueAllocaCast, valueAllocaSize llvm.Value + if isZeroSize { + valueAlloca = llvm.ConstNull(llvm.PointerType(valueType, 0)) + valueAllocaCast = llvm.ConstNull(b.i8ptrType) + } else { + valueAlloca, valueAllocaCast, valueAllocaSize = b.createTemporaryAlloca(valueType, "chan.value") + } // Allocate blockedlist buffer. channelBlockedList := b.mod.GetTypeByName("runtime.channelBlockedList") @@ -64,9 +80,14 @@ func (b *builder) createChanRecv(unop *ssa.UnOp) llvm.Value { // Do the receive. commaOk := b.createRuntimeCall("chanRecv", []llvm.Value{ch, valueAllocaCast, channelBlockedListAlloca}, "") - received := b.CreateLoad(valueAlloca, "chan.received") + var received llvm.Value + if isZeroSize { + received = llvm.ConstNull(valueType) + } else { + received = b.CreateLoad(valueAlloca, "chan.received") + b.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) + } b.emitLifetimeEnd(channelBlockedListAllocaCast, channelBlockedListAllocaSize) - b.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) if unop.CommaOk { tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{valueType, b.ctx.Int1Type()}, false)) @@ -116,7 +137,6 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value { // determine the receive buffer size and alignment. recvbufSize := uint64(0) recvbufAlign := 0 - hasReceives := false var selectStates []llvm.Value chanSelectStateType := b.getLLVMRuntimeType("chanSelectState") for _, state := range expr.States { @@ -133,7 +153,6 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value { if align := b.targetData.ABITypeAlignment(llvmType); align > recvbufAlign { recvbufAlign = align } - hasReceives = true case types.SendOnly: // Store this value in an alloca and put a pointer to this alloca // in the send state. @@ -150,7 +169,7 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value { // Create a receive buffer, where the received value will be stored. recvbuf := llvm.Undef(b.i8ptrType) - if hasReceives { + if recvbufSize != 0 { allocaType := llvm.ArrayType(b.ctx.Int8Type(), int(recvbufSize)) recvbufAlloca, _, _ := b.createTemporaryAlloca(allocaType, "select.recvbuf.alloca") recvbufAlloca.SetAlignment(recvbufAlign) diff --git a/compiler/compiler.go b/compiler/compiler.go index c0ad39d4..1ca798d1 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -23,7 +23,7 @@ import ( // Version of the compiler pacakge. Must be incremented each time the compiler // package changes in a way that affects the generated LLVM module. // This version is independent of the TinyGo version number. -const Version = 18 // last change: fix duplicated named structs +const Version = 19 // last change: fix channel ops with zero values func init() { llvm.InitializeAllTargets() diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 0fb9fa91..a4ac0f7b 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -51,6 +51,7 @@ func TestCompiler(t *testing.T) { {"pragma.go", ""}, {"goroutine.go", "wasm"}, {"goroutine.go", "cortex-m-qemu"}, + {"channel.go", ""}, {"intrinsics.go", "cortex-m-qemu"}, {"intrinsics.go", "wasm"}, } diff --git a/compiler/testdata/channel.go b/compiler/testdata/channel.go new file mode 100644 index 00000000..ecc837a7 --- /dev/null +++ b/compiler/testdata/channel.go @@ -0,0 +1,25 @@ +package main + +func chanIntSend(ch chan int) { + ch <- 3 +} + +func chanIntRecv(ch chan int) { + <-ch +} + +func chanZeroSend(ch chan struct{}) { + ch <- struct{}{} +} + +func chanZeroRecv(ch chan struct{}) { + <-ch +} + +func selectZeroRecv(ch1 chan int, ch2 chan struct{}) { + select { + case ch1 <- 1: + case <-ch2: + default: + } +} diff --git a/compiler/testdata/channel.ll b/compiler/testdata/channel.ll new file mode 100644 index 00000000..0d33d330 --- /dev/null +++ b/compiler/testdata/channel.ll @@ -0,0 +1,114 @@ +; ModuleID = 'channel.go' +source_filename = "channel.go" +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32--wasi" + +%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } +%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } +%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" } +%"internal/task.state" = type { i8* } +%runtime.chanSelectState = type { %runtime.channel*, i8* } + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) + +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.chanIntSend(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %chan.blockedList = alloca %runtime.channelBlockedList, align 8 + %chan.value = alloca i32, align 4 + %chan.value.bitcast = bitcast i32* %chan.value to i8* + call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %chan.value.bitcast) + store i32 3, i32* %chan.value, align 4 + %chan.blockedList.bitcast = bitcast %runtime.channelBlockedList* %chan.blockedList to i8* + call void @llvm.lifetime.start.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + call void @runtime.chanSend(%runtime.channel* %ch, i8* nonnull %chan.value.bitcast, %runtime.channelBlockedList* nonnull %chan.blockedList, i8* undef, i8* null) + call void @llvm.lifetime.end.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %chan.value.bitcast) + ret void +} + +; Function Attrs: argmemonly nounwind willreturn +declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) #0 + +declare void @runtime.chanSend(%runtime.channel* dereferenceable_or_null(32), i8*, %runtime.channelBlockedList* dereferenceable_or_null(24), i8*, i8*) + +; Function Attrs: argmemonly nounwind willreturn +declare void @llvm.lifetime.end.p0i8(i64 immarg, i8* nocapture) #0 + +define hidden void @main.chanIntRecv(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %chan.blockedList = alloca %runtime.channelBlockedList, align 8 + %chan.value = alloca i32, align 4 + %chan.value.bitcast = bitcast i32* %chan.value to i8* + call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %chan.value.bitcast) + %chan.blockedList.bitcast = bitcast %runtime.channelBlockedList* %chan.blockedList to i8* + call void @llvm.lifetime.start.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + %0 = call i1 @runtime.chanRecv(%runtime.channel* %ch, i8* nonnull %chan.value.bitcast, %runtime.channelBlockedList* nonnull %chan.blockedList, i8* undef, i8* null) + call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %chan.value.bitcast) + call void @llvm.lifetime.end.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + ret void +} + +declare i1 @runtime.chanRecv(%runtime.channel* dereferenceable_or_null(32), i8*, %runtime.channelBlockedList* dereferenceable_or_null(24), i8*, i8*) + +define hidden void @main.chanZeroSend(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %chan.blockedList = alloca %runtime.channelBlockedList, align 8 + %chan.blockedList.bitcast = bitcast %runtime.channelBlockedList* %chan.blockedList to i8* + call void @llvm.lifetime.start.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + call void @runtime.chanSend(%runtime.channel* %ch, i8* null, %runtime.channelBlockedList* nonnull %chan.blockedList, i8* undef, i8* null) + call void @llvm.lifetime.end.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + ret void +} + +define hidden void @main.chanZeroRecv(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %chan.blockedList = alloca %runtime.channelBlockedList, align 8 + %chan.blockedList.bitcast = bitcast %runtime.channelBlockedList* %chan.blockedList to i8* + call void @llvm.lifetime.start.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + %0 = call i1 @runtime.chanRecv(%runtime.channel* %ch, i8* null, %runtime.channelBlockedList* nonnull %chan.blockedList, i8* undef, i8* null) + call void @llvm.lifetime.end.p0i8(i64 24, i8* nonnull %chan.blockedList.bitcast) + ret void +} + +define hidden void @main.selectZeroRecv(%runtime.channel* dereferenceable_or_null(32) %ch1, %runtime.channel* dereferenceable_or_null(32) %ch2, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %select.states.alloca = alloca [2 x %runtime.chanSelectState], align 8 + %select.send.value = alloca i32, align 4 + store i32 1, i32* %select.send.value, align 4 + %select.states.alloca.bitcast = bitcast [2 x %runtime.chanSelectState]* %select.states.alloca to i8* + call void @llvm.lifetime.start.p0i8(i64 16, i8* nonnull %select.states.alloca.bitcast) + %.repack = getelementptr inbounds [2 x %runtime.chanSelectState], [2 x %runtime.chanSelectState]* %select.states.alloca, i32 0, i32 0, i32 0 + store %runtime.channel* %ch1, %runtime.channel** %.repack, align 8 + %.repack1 = getelementptr inbounds [2 x %runtime.chanSelectState], [2 x %runtime.chanSelectState]* %select.states.alloca, i32 0, i32 0, i32 1 + %0 = bitcast i8** %.repack1 to i32** + store i32* %select.send.value, i32** %0, align 4 + %.repack3 = getelementptr inbounds [2 x %runtime.chanSelectState], [2 x %runtime.chanSelectState]* %select.states.alloca, i32 0, i32 1, i32 0 + store %runtime.channel* %ch2, %runtime.channel** %.repack3, align 8 + %.repack4 = getelementptr inbounds [2 x %runtime.chanSelectState], [2 x %runtime.chanSelectState]* %select.states.alloca, i32 0, i32 1, i32 1 + store i8* null, i8** %.repack4, align 4 + %select.states = getelementptr inbounds [2 x %runtime.chanSelectState], [2 x %runtime.chanSelectState]* %select.states.alloca, i32 0, i32 0 + %select.result = call { i32, i1 } @runtime.tryChanSelect(i8* undef, %runtime.chanSelectState* nonnull %select.states, i32 2, i32 2, i8* undef, i8* null) + call void @llvm.lifetime.end.p0i8(i64 16, i8* nonnull %select.states.alloca.bitcast) + %1 = extractvalue { i32, i1 } %select.result, 0 + %2 = icmp eq i32 %1, 0 + br i1 %2, label %select.done, label %select.next + +select.done: ; preds = %select.body, %select.next, %entry + ret void + +select.next: ; preds = %entry + %3 = icmp eq i32 %1, 1 + br i1 %3, label %select.body, label %select.done + +select.body: ; preds = %select.next + br label %select.done +} + +declare { i32, i1 } @runtime.tryChanSelect(i8*, %runtime.chanSelectState*, i32, i32, i8*, i8*) + +attributes #0 = { argmemonly nounwind willreturn }