Skip to content

Commit a99ac76

Browse files
committed
compiler: optimize non-blocking single-case selects
1 parent 3b137c3 commit a99ac76

4 files changed

Lines changed: 273 additions & 17 deletions

File tree

compiler/channel.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,108 @@ func (b *builder) createChanClose(ch llvm.Value) {
104104
b.createRuntimeInvoke("chanClose", []llvm.Value{ch}, "")
105105
}
106106

107+
// createNonBlockingSelect emits IR for a non-blocking select with one channel case.
108+
func (b *builder) createNonBlockingSelect(expr *ssa.Select) llvm.Value {
109+
state := expr.States[0]
110+
ch := b.getValue(state.Chan, state.Pos)
111+
112+
resultType := b.ctx.StructType([]llvm.Type{
113+
b.ctx.Int32Type(),
114+
b.ctx.Int1Type(),
115+
}, false)
116+
117+
var selected llvm.Value
118+
selectOk := llvm.ConstInt(b.ctx.Int1Type(), 1, false)
119+
120+
switch state.Dir {
121+
case types.SendOnly:
122+
sendValue := b.getValue(state.Send, state.Pos)
123+
valueType := b.getLLVMType(state.Send.Type())
124+
isZeroSize := b.targetData.TypeAllocSize(valueType) == 0
125+
126+
valuePtr := llvm.ConstNull(b.dataPtrType)
127+
var valueAlloca, valueAllocaSize llvm.Value
128+
129+
if !isZeroSize {
130+
valueAlloca, valueAllocaSize = b.createTemporaryAlloca(
131+
valueType,
132+
"select.send.value",
133+
)
134+
b.CreateStore(sendValue, valueAlloca)
135+
valuePtr = valueAlloca
136+
}
137+
138+
selected = b.createRuntimeCall(
139+
"chanTrySend",
140+
[]llvm.Value{ch, valuePtr},
141+
"select.sent",
142+
)
143+
144+
if !isZeroSize {
145+
b.emitLifetimeEnd(valueAlloca, valueAllocaSize)
146+
}
147+
148+
case types.RecvOnly:
149+
valueType := b.getLLVMType(
150+
state.Chan.Type().Underlying().(*types.Chan).Elem(),
151+
)
152+
isZeroSize := b.targetData.TypeAllocSize(valueType) == 0
153+
154+
// getChanSelectResult loads the received value later.
155+
recvbuf := llvm.Undef(b.dataPtrType)
156+
runtimeRecvbuf := llvm.ConstNull(b.dataPtrType)
157+
158+
if !isZeroSize {
159+
recvbuf, _ = b.createTemporaryAlloca(
160+
valueType,
161+
"select.recvbuf",
162+
)
163+
runtimeRecvbuf = recvbuf
164+
}
165+
results := b.createRuntimeCall(
166+
"chanTryRecv",
167+
[]llvm.Value{ch, runtimeRecvbuf},
168+
"select.recv",
169+
)
170+
171+
selected = b.CreateExtractValue(results, 0, "select.received")
172+
recvOk := b.CreateExtractValue(results, 1, "select.recv.ok")
173+
selectOk = b.CreateSelect(
174+
selected,
175+
recvOk,
176+
llvm.ConstInt(b.ctx.Int1Type(), 1, false),
177+
"select.ok",
178+
)
179+
180+
if b.selectRecvBuf == nil {
181+
b.selectRecvBuf = make(map[*ssa.Select]llvm.Value)
182+
}
183+
b.selectRecvBuf[expr] = recvbuf
184+
185+
default:
186+
panic("unreachable")
187+
}
188+
189+
selectedIndex := llvm.ConstInt(b.ctx.Int32Type(), 0, false)
190+
defaultIndex := llvm.ConstInt(
191+
b.ctx.Int32Type(),
192+
math.MaxUint32,
193+
false,
194+
)
195+
196+
selectIndex := b.CreateSelect(
197+
selected,
198+
selectedIndex,
199+
defaultIndex,
200+
"select.index",
201+
)
202+
203+
result := llvm.Undef(resultType)
204+
result = b.CreateInsertValue(result, selectIndex, 0, "")
205+
result = b.CreateInsertValue(result, selectOk, 1, "")
206+
return result
207+
}
208+
107209
// createSelect emits all IR necessary for a select statements. That's a
108210
// non-trivial amount of code because select is very complex to implement.
109211
func (b *builder) createSelect(expr *ssa.Select) llvm.Value {
@@ -126,6 +228,10 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value {
126228
}
127229
}
128230

231+
if !expr.Blocking && len(expr.States) == 1 {
232+
return b.createNonBlockingSelect(expr)
233+
}
234+
129235
const maxSelectStates = math.MaxUint32 >> 2
130236
if len(expr.States) > maxSelectStates {
131237
// The runtime code assumes that the number of state must fit in 30 bits

compiler/testdata/channel.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,39 @@ func selectZeroRecv(ch1 chan int, ch2 chan struct{}) {
2323
default:
2424
}
2525
}
26+
27+
func selectNonBlockingSend(ch chan int, value int) bool {
28+
select {
29+
case ch <- value:
30+
return true
31+
default:
32+
return false
33+
}
34+
}
35+
36+
func selectNonBlockingRecv(ch chan int) (int, bool, bool) {
37+
select {
38+
case value, ok := <-ch:
39+
return value, ok, true
40+
default:
41+
return 0, false, false
42+
}
43+
}
44+
45+
func selectNonBlockingZeroSend(ch chan struct{}) bool {
46+
select {
47+
case ch <- struct{}{}:
48+
return true
49+
default:
50+
return false
51+
}
52+
}
53+
54+
func selectNonBlockingZeroRecv(ch chan struct{}) (bool, bool) {
55+
select {
56+
case _, ok := <-ch:
57+
return ok, true
58+
default:
59+
return false, false
60+
}
61+
}

compiler/testdata/channel.ll

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ target triple = "wasm32-unknown-wasi"
66
%runtime.channelOp = type { ptr, ptr, i32, ptr }
77
%runtime.chanSelectState = type { ptr, ptr }
88

9-
declare void @runtime.trackPointer(ptr nocapture readonly, ptr, ptr) #0
9+
declare void @runtime.trackPointer(ptr readonly captures(none), ptr, ptr) #0
1010

1111
; Function Attrs: nounwind
1212
define hidden void @main.init(ptr %context) unnamed_addr #1 {
@@ -19,33 +19,33 @@ define hidden void @main.chanIntSend(ptr dereferenceable_or_null(36) %ch, ptr %c
1919
entry:
2020
%chan.op = alloca %runtime.channelOp, align 8
2121
%chan.value = alloca i32, align 4
22-
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %chan.value)
22+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.value)
2323
store i32 3, ptr %chan.value, align 4
24-
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
24+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.op)
2525
call void @runtime.chanSend(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.op, ptr undef) #3
26-
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
27-
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %chan.value)
26+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.op)
27+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.value)
2828
ret void
2929
}
3030

3131
; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
32-
declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #2
32+
declare void @llvm.lifetime.start.p0(ptr captures(none)) #2
3333

3434
declare void @runtime.chanSend(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #0
3535

3636
; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
37-
declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #2
37+
declare void @llvm.lifetime.end.p0(ptr captures(none)) #2
3838

3939
; Function Attrs: nounwind
4040
define hidden void @main.chanIntRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
4141
entry:
4242
%chan.op = alloca %runtime.channelOp, align 8
4343
%chan.value = alloca i32, align 4
44-
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %chan.value)
45-
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
44+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.value)
45+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.op)
4646
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.value, ptr nonnull %chan.op, ptr undef) #3
47-
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %chan.value)
48-
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
47+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.value)
48+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.op)
4949
ret void
5050
}
5151

@@ -55,19 +55,19 @@ declare i1 @runtime.chanRecv(ptr dereferenceable_or_null(36), ptr, ptr dereferen
5555
define hidden void @main.chanZeroSend(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
5656
entry:
5757
%chan.op = alloca %runtime.channelOp, align 8
58-
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
58+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.op)
5959
call void @runtime.chanSend(ptr %ch, ptr null, ptr nonnull %chan.op, ptr undef) #3
60-
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
60+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.op)
6161
ret void
6262
}
6363

6464
; Function Attrs: nounwind
6565
define hidden void @main.chanZeroRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
6666
entry:
6767
%chan.op = alloca %runtime.channelOp, align 8
68-
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %chan.op)
68+
call void @llvm.lifetime.start.p0(ptr nonnull %chan.op)
6969
%0 = call i1 @runtime.chanRecv(ptr %ch, ptr null, ptr nonnull %chan.op, ptr undef) #3
70-
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %chan.op)
70+
call void @llvm.lifetime.end.p0(ptr nonnull %chan.op)
7171
ret void
7272
}
7373

@@ -77,7 +77,7 @@ entry:
7777
%select.states.alloca = alloca [2 x %runtime.chanSelectState], align 8
7878
%select.send.value = alloca i32, align 4
7979
store i32 1, ptr %select.send.value, align 4
80-
call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %select.states.alloca)
80+
call void @llvm.lifetime.start.p0(ptr nonnull %select.states.alloca)
8181
store ptr %ch1, ptr %select.states.alloca, align 4
8282
%select.states.alloca.repack1 = getelementptr inbounds nuw i8, ptr %select.states.alloca, i32 4
8383
store ptr %select.send.value, ptr %select.states.alloca.repack1, align 4
@@ -86,7 +86,7 @@ entry:
8686
%.repack3 = getelementptr inbounds nuw i8, ptr %select.states.alloca, i32 12
8787
store ptr null, ptr %.repack3, align 4
8888
%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) #3
89-
call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %select.states.alloca)
89+
call void @llvm.lifetime.end.p0(ptr nonnull %select.states.alloca)
9090
%1 = extractvalue { i32, i1 } %select.result, 0
9191
%2 = icmp eq i32 %1, 0
9292
br i1 %2, label %select.done, label %select.next
@@ -104,6 +104,80 @@ select.body: ; preds = %select.next
104104

105105
declare { i32, i1 } @runtime.chanSelect(ptr, ptr, i32, i32, ptr, i32, i32, ptr) #0
106106

107+
; Function Attrs: nounwind
108+
define hidden i1 @main.selectNonBlockingSend(ptr dereferenceable_or_null(36) %ch, i32 %value, ptr %context) unnamed_addr #1 {
109+
entry:
110+
%select.send.value = alloca i32, align 4
111+
call void @llvm.lifetime.start.p0(ptr nonnull %select.send.value)
112+
store i32 %value, ptr %select.send.value, align 4
113+
%select.sent = call i1 @runtime.chanTrySend(ptr %ch, ptr nonnull %select.send.value, ptr undef) #3
114+
call void @llvm.lifetime.end.p0(ptr nonnull %select.send.value)
115+
br i1 %select.sent, label %select.body, label %select.next
116+
117+
select.body: ; preds = %entry
118+
ret i1 true
119+
120+
select.next: ; preds = %entry
121+
ret i1 false
122+
}
123+
124+
declare i1 @runtime.chanTrySend(ptr dereferenceable_or_null(36), ptr, ptr) #0
125+
126+
; Function Attrs: nounwind
127+
define hidden { i32, i1, i1 } @main.selectNonBlockingRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
128+
entry:
129+
%select.recvbuf = alloca i32, align 4
130+
%stackalloc = alloca i8, align 1
131+
call void @llvm.lifetime.start.p0(ptr nonnull %select.recvbuf)
132+
%select.recv = call { i1, i1 } @runtime.chanTryRecv(ptr %ch, ptr nonnull %select.recvbuf, ptr undef) #3
133+
%select.received = extractvalue { i1, i1 } %select.recv, 0
134+
call void @runtime.trackPointer(ptr nonnull %select.recvbuf, ptr nonnull %stackalloc, ptr undef) #3
135+
br i1 %select.received, label %select.body, label %select.next
136+
137+
select.body: ; preds = %entry
138+
%select.recv.ok = extractvalue { i1, i1 } %select.recv, 1
139+
%0 = load i32, ptr %select.recvbuf, align 4
140+
%1 = insertvalue { i32, i1, i1 } zeroinitializer, i32 %0, 0
141+
%2 = insertvalue { i32, i1, i1 } %1, i1 %select.recv.ok, 1
142+
%3 = insertvalue { i32, i1, i1 } %2, i1 true, 2
143+
ret { i32, i1, i1 } %3
144+
145+
select.next: ; preds = %entry
146+
ret { i32, i1, i1 } zeroinitializer
147+
}
148+
149+
declare { i1, i1 } @runtime.chanTryRecv(ptr dereferenceable_or_null(36), ptr, ptr) #0
150+
151+
; Function Attrs: nounwind
152+
define hidden i1 @main.selectNonBlockingZeroSend(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
153+
entry:
154+
%select.sent = call i1 @runtime.chanTrySend(ptr %ch, ptr null, ptr undef) #3
155+
br i1 %select.sent, label %select.body, label %select.next
156+
157+
select.body: ; preds = %entry
158+
ret i1 true
159+
160+
select.next: ; preds = %entry
161+
ret i1 false
162+
}
163+
164+
; Function Attrs: nounwind
165+
define hidden { i1, i1 } @main.selectNonBlockingZeroRecv(ptr dereferenceable_or_null(36) %ch, ptr %context) unnamed_addr #1 {
166+
entry:
167+
%select.recv = call { i1, i1 } @runtime.chanTryRecv(ptr %ch, ptr null, ptr undef) #3
168+
%select.received = extractvalue { i1, i1 } %select.recv, 0
169+
br i1 %select.received, label %select.body, label %select.next
170+
171+
select.body: ; preds = %entry
172+
%select.recv.ok = extractvalue { i1, i1 } %select.recv, 1
173+
%0 = insertvalue { i1, i1 } zeroinitializer, i1 %select.recv.ok, 0
174+
%1 = insertvalue { i1, i1 } %0, i1 true, 1
175+
ret { i1, i1 } %1
176+
177+
select.next: ; preds = %entry
178+
ret { i1, i1 } zeroinitializer
179+
}
180+
107181
attributes #0 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
108182
attributes #1 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
109183
attributes #2 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }

src/runtime/chan.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,46 @@ func chanRecv(ch *channel, value unsafe.Pointer, op *channelOp) bool {
344344
return t.DataUint32() != chanOperationClosed
345345
}
346346

347+
// chanTrySend attempts a non-blocking send.
348+
func chanTrySend(ch *channel, value unsafe.Pointer) bool {
349+
if ch == nil {
350+
return false
351+
}
352+
353+
mask := interrupt.Disable()
354+
ch.lock.Lock()
355+
356+
sent, wake := ch.trySend(value)
357+
358+
ch.lock.Unlock()
359+
if wake != nil {
360+
scheduleTask(wake)
361+
}
362+
interrupt.Restore(mask)
363+
364+
return sent
365+
}
366+
367+
// chanTryRecv attempts a non-blocking receive.
368+
func chanTryRecv(ch *channel, value unsafe.Pointer) (received, ok bool) {
369+
if ch == nil {
370+
return false, true
371+
}
372+
373+
mask := interrupt.Disable()
374+
ch.lock.Lock()
375+
376+
received, ok, wake := ch.tryRecv(value)
377+
378+
ch.lock.Unlock()
379+
if wake != nil {
380+
scheduleTask(wake)
381+
}
382+
interrupt.Restore(mask)
383+
384+
return received, ok
385+
}
386+
347387
// chanClose closes the given channel. If this channel has a receiver or is
348388
// empty, it closes the channel. Else, it panics.
349389
func chanClose(ch *channel) {

0 commit comments

Comments
 (0)