Description
Currently, code like:
select {
case c1 <- v1: f1()
case v2 = <-c2: f2()
case v3, ok = <-c3: f3()
default: f4()
}
gets rewritten by cmd/compile into:
var sel struct {
tcase, ncase uint16
pollorder, lockorder *uint8
scase [4]runtime.scase
lockorderarr, pollorderarr [4]uint8
}
runtime.newselect(&sel, unsafe.Sizeof(sel), 4)
if runtime.selectsend(&sel, c1, &v1) { f1(); goto after }
if runtime.selectrecv(&sel, c2, &v2) { f2(); goto after }
if runtime.selectrecv2(&sel, c3, &v3, &ok) { f3(); goto after }
if runtime.selectdefault(&sel) { f4(); goto after }
runtime.selectgo(&sel)
after:
The select{send,recv,recv2,default} functions always return false the first time they're called, but internally they save the caller's PC into &sel. runtime.selectgo never returns; instead it waits for a channel operation that can succeed, and then returns to the appropriate PC, behaving as though the function call returned twice.
(To make a C analogy, select{send,recv,recv2,default} are like setjmp, and selectgo is like longjmp.)
This proposal is to instead compile it as (something like):
var sel = struct{...}{tcase: 4, scase: [4]runtime.scase{
{elem: &v1, chan: c1, kind: runtime.caseSend},
{elem: &v2, chan: c2, kind: runtime.caseRecv},
{elem: &v3, chan: c3, kind: runtime.caseRecv, receivedp: &ok},
{kind: runtime.caseDefault},
}}
switch runtime.select(&sel) {
case 0: f1()
case 1: f2()
case 2: f3()
case 3: f4()
default: undef
}
Pros:
-
The returns-twice and returns-never logic complicates the CFG. For example, the liveness analysis pass needs to traverse these implicit edges by recognizing runtime.selectfoo function calls. It has also caused bugs in SSA optimizations (for example https://go-review.googlesource.com/#/c/37376/).
-
gccgo already does this according to @ianlancetaylor
-
I wouldn't be surprised if the compiler is able to more efficiently initialize the select data structure as a straight forward composite literal, than as a bunch of runtime calls.
-
We can eliminate a few fields. For example, hselect.ncase and scase.{pc,so}. Potentially more simplifications.
Cons:
- Currently switch statements are compiled into binary searches, whereas the current select implementation is able to directly jump to the appropriate destination PC. We could optimize switch statements to use jump tables though, at least for the special case of lowered select statements.
Activity
cespare commentedon Feb 28, 2017
(Related switch-optimization issues are #5496 and #15780.)
randall77 commentedon Feb 28, 2017
A similar return-twice pattern happens for defer. If possible we should do a similar thing for defers.
mdempsky commentedon Feb 28, 2017
@randall77 I think cleaning up deferproc is likely worth doing for similar reasons, but I think the details are distinct enough to track in a separate proposal.
josharian commentedon Mar 1, 2017
I like it. Most select cases are small (https://github.com/josharian/countselectcases/blob/master/README.md has some old data), so it'll be a linear not binary search, but I'd wager it'll be imperceptible, particularly since we're eliminating a linear number of function calls.
aclements commentedon Mar 1, 2017
I admit that I don't fully understand the reasons why it's done the way it is today, but I support this change, too. In addition to dramatically simplifying some very strange semantics in the runtime, this would eliminate the only use of setcallerpc in the runtime, which has been a thorn in my side before.
ianlancetaylor commentedon Mar 1, 2017
I believe it would eliminate the only use of
setcallerpc
anywhere.griesemer commentedon Mar 1, 2017
I'm all for this. The suggestion approach seems much cleaner.
mdempsky commentedon Mar 1, 2017
One minor complication I discovered while prototyping this: currently we perform race instrumentation within selectgo using the caller PCs recorded by select{send,recv,default}. This shows up when
case ch <- v:
races withclose(ch)
orcase v = <-ch:
races with another load/store ofv
.We could continue instrumenting within selectgo, but it would mean any races would point to the entire select statement, rather than the individual case.
Alternatively, we can have the compiler insert instrumentation around the selectgo call, so they can have appropriate line numbers and precise race reports. This would amount to a raceread call for each send case, and just rewriting
case v = <-ch:
tocase vtmp := <-ch: v = vtmp
(which we already do at least in some cases) so the compiler's usual instrumentation pass can handle it.gopherbot commentedon Mar 2, 2017
CL https://golang.org/cl/37661 mentions this issue.
mdempsky commentedon Mar 2, 2017
CL 37661 gets rid of the setjmp/longjmp control flow, but keeps the selectfoo calls in place for the purpose of race instrumentation. Post-walk, the AST now looks like:
Further simplifications are still possible.
[-]proposal: cmd/compile,runtime: compile multiway select statements as switch statements[/-][+]cmd/compile,runtime: compile multiway select statements as switch statements[/+]10 remaining items