Skip to content

Commit

Permalink
runtime: rewrite channel implementation
Browse files Browse the repository at this point in the history
This rewrite simplifies the channel implementation considerably, with
34% less LOC. Perhaps the most important change is the removal of the
channel state, which made sense when we had only send and receive
operations but only makes things more compliated when multiple select
operations can be pending on a single channel.

I did this rewrite originally to make it possible to make channels
parallelism-safe. The current implementation is not parallelism-safe,
but it will be easy to make it so (the main additions will be a channel
lock, a global select lock, and an atomic compare-and-swap in
chanQueue.pop).
  • Loading branch information
aykevl committed Nov 20, 2024
1 parent d1fe02d commit 60741a3
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 562 deletions.
37 changes: 20 additions & 17 deletions compiler/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ func (b *builder) createChanSend(instr *ssa.Send) {
b.CreateStore(chanValue, valueAlloca)
}

// Allocate blockedlist buffer.
channelBlockedList := b.getLLVMRuntimeType("channelBlockedList")
channelBlockedListAlloca, channelBlockedListAllocaSize := b.createTemporaryAlloca(channelBlockedList, "chan.blockedList")
// Allocate buffer for the channel operation.
channelOp := b.getLLVMRuntimeType("channelOp")
channelOpAlloca, channelOpAllocaSize := b.createTemporaryAlloca(channelOp, "chan.op")

// Do the send.
b.createRuntimeCall("chanSend", []llvm.Value{ch, valueAlloca, channelBlockedListAlloca}, "")
b.createRuntimeCall("chanSend", []llvm.Value{ch, valueAlloca, channelOpAlloca}, "")

// End the lifetime of the allocas.
// This also works around a bug in CoroSplit, at least in LLVM 8:
// https://bugs.llvm.org/show_bug.cgi?id=41742
b.emitLifetimeEnd(channelBlockedListAlloca, channelBlockedListAllocaSize)
b.emitLifetimeEnd(channelOpAlloca, channelOpAllocaSize)
if !isZeroSize {
b.emitLifetimeEnd(valueAlloca, valueAllocaSize)
}
Expand All @@ -72,20 +72,20 @@ func (b *builder) createChanRecv(unop *ssa.UnOp) llvm.Value {
valueAlloca, valueAllocaSize = b.createTemporaryAlloca(valueType, "chan.value")
}

// Allocate blockedlist buffer.
channelBlockedList := b.getLLVMRuntimeType("channelBlockedList")
channelBlockedListAlloca, channelBlockedListAllocaSize := b.createTemporaryAlloca(channelBlockedList, "chan.blockedList")
// Allocate buffer for the channel operation.
channelOp := b.getLLVMRuntimeType("channelOp")
channelOpAlloca, channelOpAllocaSize := b.createTemporaryAlloca(channelOp, "chan.op")

// Do the receive.
commaOk := b.createRuntimeCall("chanRecv", []llvm.Value{ch, valueAlloca, channelBlockedListAlloca}, "")
commaOk := b.createRuntimeCall("chanRecv", []llvm.Value{ch, valueAlloca, channelOpAlloca}, "")
var received llvm.Value
if isZeroSize {
received = llvm.ConstNull(valueType)
} else {
received = b.CreateLoad(valueType, valueAlloca, "chan.received")
b.emitLifetimeEnd(valueAlloca, valueAllocaSize)
}
b.emitLifetimeEnd(channelBlockedListAlloca, channelBlockedListAllocaSize)
b.emitLifetimeEnd(channelOpAlloca, channelOpAllocaSize)

if unop.CommaOk {
tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{valueType, b.ctx.Int1Type()}, false))
Expand Down Expand Up @@ -198,26 +198,29 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value {
if expr.Blocking {
// Stack-allocate operation structures.
// If these were simply created as a slice, they would heap-allocate.
chBlockAllocaType := llvm.ArrayType(b.getLLVMRuntimeType("channelBlockedList"), len(selectStates))
chBlockAlloca, chBlockSize := b.createTemporaryAlloca(chBlockAllocaType, "select.block.alloca")
chBlockLen := llvm.ConstInt(b.uintptrType, uint64(len(selectStates)), false)
chBlockPtr := b.CreateGEP(chBlockAllocaType, chBlockAlloca, []llvm.Value{
opsAllocaType := llvm.ArrayType(b.getLLVMRuntimeType("channelOp"), len(selectStates))
opsAlloca, opsSize := b.createTemporaryAlloca(opsAllocaType, "select.block.alloca")
opsLen := llvm.ConstInt(b.uintptrType, uint64(len(selectStates)), false)
opsPtr := b.CreateGEP(opsAllocaType, opsAlloca, []llvm.Value{
llvm.ConstInt(b.ctx.Int32Type(), 0, false),
llvm.ConstInt(b.ctx.Int32Type(), 0, false),
}, "select.block")

results = b.createRuntimeCall("chanSelect", []llvm.Value{
recvbuf,
statesPtr, statesLen, statesLen, // []chanSelectState
chBlockPtr, chBlockLen, chBlockLen, // []channelBlockList
opsPtr, opsLen, opsLen, // []channelOp
}, "select.result")

// Terminate the lifetime of the operation structures.
b.emitLifetimeEnd(chBlockAlloca, chBlockSize)
b.emitLifetimeEnd(opsAlloca, opsSize)
} else {
results = b.createRuntimeCall("tryChanSelect", []llvm.Value{
opsPtr := llvm.ConstNull(b.dataPtrType)
opsLen := llvm.ConstInt(b.uintptrType, 0, false)
results = b.createRuntimeCall("chanSelect", []llvm.Value{
recvbuf,
statesPtr, statesLen, statesLen, // []chanSelectState
opsPtr, opsLen, opsLen, // []channelOp (nil slice)
}, "select.result")
}

Expand Down
52 changes: 26 additions & 26 deletions compiler/testdata/channel.ll
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source_filename = "channel.go"
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-wasi"

%runtime.channelBlockedList = type { ptr, ptr, ptr, { ptr, i32, i32 } }
%runtime.channelOp = type { ptr, ptr, i32, ptr }
%runtime.chanSelectState = type { ptr, ptr }

; Function Attrs: allockind("alloc,zeroed") allocsize(0)
Expand All @@ -18,64 +18,64 @@ entry:
}

; Function Attrs: nounwind
define hidden void @main.chanIntSend(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #2 {
define hidden void @main.chanIntSend(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #2 {
entry:
%chan.blockedList = alloca %runtime.channelBlockedList, align 8
%chan.op = alloca %runtime.channelOp, align 8
%chan.value = alloca i32, align 4
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %chan.value)
store i32 3, ptr %chan.value, align 4
call void @llvm.lifetime.start.p0(i64 24, ptr nonnull %chan.blockedList)
call void @runtime.chanSend(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.blockedList, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %chan.blockedList)
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
call void @runtime.chanSend(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.op, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %chan.value)
ret void
}

; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #3

declare void @runtime.chanSend(ptr dereferenceable_or_null(32), ptr, ptr dereferenceable_or_null(24), ptr) #1
declare void @runtime.chanSend(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #1

; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #3

; Function Attrs: nounwind
define hidden void @main.chanIntRecv(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #2 {
define hidden void @main.chanIntRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #2 {
entry:
%chan.blockedList = alloca %runtime.channelBlockedList, align 8
%chan.op = alloca %runtime.channelOp, align 8
%chan.value = alloca i32, align 4
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %chan.value)
call void @llvm.lifetime.start.p0(i64 24, ptr nonnull %chan.blockedList)
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.blockedList, ptr undef) #4
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.op, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %chan.value)
call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %chan.blockedList)
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
ret void
}

declare i1 @runtime.chanRecv(ptr dereferenceable_or_null(32), ptr, ptr dereferenceable_or_null(24), ptr) #1
declare i1 @runtime.chanRecv(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #1

; Function Attrs: nounwind
define hidden void @main.chanZeroSend(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #2 {
define hidden void @main.chanZeroSend(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #2 {
entry:
%chan.blockedList = alloca %runtime.channelBlockedList, align 8
call void @llvm.lifetime.start.p0(i64 24, ptr nonnull %chan.blockedList)
call void @runtime.chanSend(ptr %ch, ptr null, ptr nonnull %chan.blockedList, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %chan.blockedList)
%chan.op = alloca %runtime.channelOp, align 8
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
call void @runtime.chanSend(ptr %ch, ptr null, ptr nonnull %chan.op, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
ret void
}

; Function Attrs: nounwind
define hidden void @main.chanZeroRecv(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #2 {
define hidden void @main.chanZeroRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #2 {
entry:
%chan.blockedList = alloca %runtime.channelBlockedList, align 8
call void @llvm.lifetime.start.p0(i64 24, ptr nonnull %chan.blockedList)
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr null, ptr nonnull %chan.blockedList, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %chan.blockedList)
%chan.op = alloca %runtime.channelOp, align 8
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr null, ptr nonnull %chan.op, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
ret void
}

; Function Attrs: nounwind
define hidden void @main.selectZeroRecv(ptr dereferenceable_or_null(32) %ch1, ptr dereferenceable_or_null(32) %ch2, ptr %context) unnamed_addr #2 {
define hidden void @main.selectZeroRecv(ptr dereferenceable_or_null(36) %ch1, ptr dereferenceable_or_null(36) %ch2, ptr %context) unnamed_addr #2 {
entry:
%select.states.alloca = alloca [2 x %runtime.chanSelectState], align 8
%select.send.value = alloca i32, align 4
Expand All @@ -88,7 +88,7 @@ entry:
store ptr %ch2, ptr %0, align 4
%.repack3 = getelementptr inbounds [2 x %runtime.chanSelectState], ptr %select.states.alloca, i32 0, i32 1, i32 1
store ptr null, ptr %.repack3, align 4
%select.result = call { i32, i1 } @runtime.tryChanSelect(ptr undef, ptr nonnull %select.states.alloca, i32 2, i32 2, ptr undef) #4
%select.result = call { i32, i1 } @runtime.chanSelect(ptr undef, ptr nonnull %select.states.alloca, i32 2, i32 2, ptr null, i32 0, i32 0, ptr undef) #4
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %select.states.alloca)
%1 = extractvalue { i32, i1 } %select.result, 0
%2 = icmp eq i32 %1, 0
Expand All @@ -105,7 +105,7 @@ select.body: ; preds = %select.next
br label %select.done
}

declare { i32, i1 } @runtime.tryChanSelect(ptr, ptr, i32, i32, ptr) #1
declare { i32, i1 } @runtime.chanSelect(ptr, ptr, i32, i32, ptr, i32, i32, ptr) #1

attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" }
attributes #1 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" }
Expand Down
4 changes: 2 additions & 2 deletions compiler/testdata/goroutine-cortex-m-qemu-tasks.ll
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ entry:
declare i32 @runtime.sliceCopy(ptr nocapture writeonly, ptr nocapture readonly, i32, i32, i32, ptr) #2

; Function Attrs: nounwind
define hidden void @main.closeBuiltinGoroutine(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #1 {
define hidden void @main.closeBuiltinGoroutine(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
entry:
call void @runtime.chanClose(ptr %ch, ptr undef) #9
ret void
}

declare void @runtime.chanClose(ptr dereferenceable_or_null(32), ptr) #2
declare void @runtime.chanClose(ptr dereferenceable_or_null(36), ptr) #2

; Function Attrs: nounwind
define hidden void @main.startInterfaceMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #1 {
Expand Down
4 changes: 2 additions & 2 deletions compiler/testdata/goroutine-wasm-asyncify.ll
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ entry:
declare i32 @runtime.sliceCopy(ptr nocapture writeonly, ptr nocapture readonly, i32, i32, i32, ptr) #1

; Function Attrs: nounwind
define hidden void @main.closeBuiltinGoroutine(ptr dereferenceable_or_null(32) %ch, ptr %context) unnamed_addr #2 {
define hidden void @main.closeBuiltinGoroutine(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #2 {
entry:
call void @runtime.chanClose(ptr %ch, ptr undef) #9
ret void
}

declare void @runtime.chanClose(ptr dereferenceable_or_null(32), ptr) #1
declare void @runtime.chanClose(ptr dereferenceable_or_null(36), ptr) #1

; Function Attrs: nounwind
define hidden void @main.startInterfaceMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
Expand Down
Loading

0 comments on commit 60741a3

Please sign in to comment.