Skip to content

Commit 3d8cb26

Browse files
committed
cmd/compile: modify switches of strings to use jump table for lengths
Reorganize the way we rewrite expression switches on strings, so that jump tables are naturally used for the outer switch on the string length. The changes to the prove pass in this CL are required so as to not repeat the test for string length in each case. name old time/op new time/op delta SwitchStringPredictable 2.28ns ± 9% 2.08ns ± 5% -9.04% (p=0.000 n=10+10) SwitchStringUnpredictable 10.5ns ± 1% 9.5ns ± 1% -9.08% (p=0.000 n=9+10) Update #5496 Update #34381 Change-Id: Ie6846b1dd27f3e472f7c30dfcc598c68d440b997 Reviewed-on: https://go-review.googlesource.com/c/go/+/395714 Run-TryBot: Keith Randall <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 01b9ae2 commit 3d8cb26

File tree

3 files changed

+162
-31
lines changed

3 files changed

+162
-31
lines changed

src/cmd/compile/internal/ssa/prove.go

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const (
1616
unknown branch = iota
1717
positive
1818
negative
19+
// The outedges from a jump table are jumpTable0,
20+
// jumpTable0+1, jumpTable0+2, etc. There could be an
21+
// arbitrary number so we can't list them all here.
22+
jumpTable0
1923
)
2024

2125
// relation represents the set of possible relations between
@@ -940,20 +944,31 @@ func prove(f *Func) {
940944
// getBranch returns the range restrictions added by p
941945
// when reaching b. p is the immediate dominator of b.
942946
func getBranch(sdom SparseTree, p *Block, b *Block) branch {
943-
if p == nil || p.Kind != BlockIf {
947+
if p == nil {
944948
return unknown
945949
}
946-
// If p and p.Succs[0] are dominators it means that every path
947-
// from entry to b passes through p and p.Succs[0]. We care that
948-
// no path from entry to b passes through p.Succs[1]. If p.Succs[0]
949-
// has one predecessor then (apart from the degenerate case),
950-
// there is no path from entry that can reach b through p.Succs[1].
951-
// TODO: how about p->yes->b->yes, i.e. a loop in yes.
952-
if sdom.IsAncestorEq(p.Succs[0].b, b) && len(p.Succs[0].b.Preds) == 1 {
953-
return positive
954-
}
955-
if sdom.IsAncestorEq(p.Succs[1].b, b) && len(p.Succs[1].b.Preds) == 1 {
956-
return negative
950+
switch p.Kind {
951+
case BlockIf:
952+
// If p and p.Succs[0] are dominators it means that every path
953+
// from entry to b passes through p and p.Succs[0]. We care that
954+
// no path from entry to b passes through p.Succs[1]. If p.Succs[0]
955+
// has one predecessor then (apart from the degenerate case),
956+
// there is no path from entry that can reach b through p.Succs[1].
957+
// TODO: how about p->yes->b->yes, i.e. a loop in yes.
958+
if sdom.IsAncestorEq(p.Succs[0].b, b) && len(p.Succs[0].b.Preds) == 1 {
959+
return positive
960+
}
961+
if sdom.IsAncestorEq(p.Succs[1].b, b) && len(p.Succs[1].b.Preds) == 1 {
962+
return negative
963+
}
964+
case BlockJumpTable:
965+
// TODO: this loop can lead to quadratic behavior, as
966+
// getBranch can be called len(p.Succs) times.
967+
for i, e := range p.Succs {
968+
if sdom.IsAncestorEq(e.b, b) && len(e.b.Preds) == 1 {
969+
return jumpTable0 + branch(i)
970+
}
971+
}
957972
}
958973
return unknown
959974
}
@@ -984,11 +999,36 @@ func addIndVarRestrictions(ft *factsTable, b *Block, iv indVar) {
984999
// branching from Block b in direction br.
9851000
func addBranchRestrictions(ft *factsTable, b *Block, br branch) {
9861001
c := b.Controls[0]
987-
switch br {
988-
case negative:
1002+
switch {
1003+
case br == negative:
9891004
addRestrictions(b, ft, boolean, nil, c, eq)
990-
case positive:
1005+
case br == positive:
9911006
addRestrictions(b, ft, boolean, nil, c, lt|gt)
1007+
case br >= jumpTable0:
1008+
idx := br - jumpTable0
1009+
val := int64(idx)
1010+
if v, off := isConstDelta(c); v != nil {
1011+
// Establish the bound on the underlying value we're switching on,
1012+
// not on the offset-ed value used as the jump table index.
1013+
c = v
1014+
val -= off
1015+
}
1016+
old, ok := ft.limits[c.ID]
1017+
if !ok {
1018+
old = noLimit
1019+
}
1020+
ft.limitStack = append(ft.limitStack, limitFact{c.ID, old})
1021+
if val < old.min || val > old.max || uint64(val) < old.umin || uint64(val) > old.umax {
1022+
ft.unsat = true
1023+
if b.Func.pass.debug > 2 {
1024+
b.Func.Warnl(b.Pos, "block=%s outedge=%d %s=%d unsat", b, idx, c, val)
1025+
}
1026+
} else {
1027+
ft.limits[c.ID] = limit{val, val, uint64(val), uint64(val)}
1028+
if b.Func.pass.debug > 2 {
1029+
b.Func.Warnl(b.Pos, "block=%s outedge=%d %s=%d", b, idx, c, val)
1030+
}
1031+
}
9921032
default:
9931033
panic("unknown branch")
9941034
}
@@ -1343,10 +1383,14 @@ func removeBranch(b *Block, branch branch) {
13431383
// attempt to preserve statement marker.
13441384
b.Pos = b.Pos.WithIsStmt()
13451385
}
1346-
b.Kind = BlockFirst
1347-
b.ResetControls()
1348-
if branch == positive {
1349-
b.swapSuccessors()
1386+
if branch == positive || branch == negative {
1387+
b.Kind = BlockFirst
1388+
b.ResetControls()
1389+
if branch == positive {
1390+
b.swapSuccessors()
1391+
}
1392+
} else {
1393+
// TODO: figure out how to remove an entry from a jump table
13501394
}
13511395
}
13521396

src/cmd/compile/internal/test/switch_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,49 @@ func benchmarkSwitch32(b *testing.B, predictable bool) {
7777
sink = n
7878
}
7979

80+
func BenchmarkSwitchStringPredictable(b *testing.B) {
81+
benchmarkSwitchString(b, true)
82+
}
83+
func BenchmarkSwitchStringUnpredictable(b *testing.B) {
84+
benchmarkSwitchString(b, false)
85+
}
86+
func benchmarkSwitchString(b *testing.B, predictable bool) {
87+
a := []string{
88+
"foo",
89+
"foo1",
90+
"foo22",
91+
"foo333",
92+
"foo4444",
93+
"foo55555",
94+
"foo666666",
95+
"foo7777777",
96+
}
97+
n := 0
98+
rng := newRNG()
99+
for i := 0; i < b.N; i++ {
100+
rng = rng.next(predictable)
101+
switch a[rng.value()&7] {
102+
case "foo":
103+
n += 1
104+
case "foo1":
105+
n += 2
106+
case "foo22":
107+
n += 3
108+
case "foo333":
109+
n += 4
110+
case "foo4444":
111+
n += 5
112+
case "foo55555":
113+
n += 6
114+
case "foo666666":
115+
n += 7
116+
case "foo7777777":
117+
n += 8
118+
}
119+
}
120+
sink = n
121+
}
122+
80123
// A simple random number generator used to make switches conditionally predictable.
81124
type rng uint64
82125

src/cmd/compile/internal/walk/switch.go

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func walkSwitchExpr(sw *ir.SwitchStmt) {
6767
base.Pos = lno
6868

6969
s := exprSwitch{
70+
pos: lno,
7071
exprname: cond,
7172
}
7273

@@ -113,6 +114,7 @@ func walkSwitchExpr(sw *ir.SwitchStmt) {
113114

114115
// An exprSwitch walks an expression switch.
115116
type exprSwitch struct {
117+
pos src.XPos
116118
exprname ir.Node // value being switched on
117119

118120
done ir.Nodes
@@ -183,17 +185,59 @@ func (s *exprSwitch) flush() {
183185
}
184186
runs = append(runs, cc[start:])
185187

186-
// Perform two-level binary search.
187-
binarySearch(len(runs), &s.done,
188-
func(i int) ir.Node {
189-
return ir.NewBinaryExpr(base.Pos, ir.OLE, ir.NewUnaryExpr(base.Pos, ir.OLEN, s.exprname), ir.NewInt(runLen(runs[i-1])))
190-
},
191-
func(i int, nif *ir.IfStmt) {
192-
run := runs[i]
193-
nif.Cond = ir.NewBinaryExpr(base.Pos, ir.OEQ, ir.NewUnaryExpr(base.Pos, ir.OLEN, s.exprname), ir.NewInt(runLen(run)))
194-
s.search(run, &nif.Body)
195-
},
196-
)
188+
if len(runs) == 1 {
189+
s.search(runs[0], &s.done)
190+
return
191+
}
192+
// We have strings of more than one length. Generate an
193+
// outer switch which switches on the length of the string
194+
// and an inner switch in each case which resolves all the
195+
// strings of the same length. The code looks something like this:
196+
197+
// goto outerLabel
198+
// len5:
199+
// ... search among length 5 strings ...
200+
// goto endLabel
201+
// len8:
202+
// ... search among length 8 strings ...
203+
// goto endLabel
204+
// ... other lengths ...
205+
// outerLabel:
206+
// switch len(s) {
207+
// case 5: goto len5
208+
// case 8: goto len8
209+
// ... other lengths ...
210+
// }
211+
// endLabel:
212+
213+
outerLabel := typecheck.AutoLabel(".s")
214+
endLabel := typecheck.AutoLabel(".s")
215+
216+
// Jump around all the individual switches for each length.
217+
s.done.Append(ir.NewBranchStmt(s.pos, ir.OGOTO, outerLabel))
218+
219+
var outer exprSwitch
220+
outer.exprname = ir.NewUnaryExpr(s.pos, ir.OLEN, s.exprname)
221+
outer.exprname.SetType(types.Types[types.TINT])
222+
223+
for _, run := range runs {
224+
// Target label to jump to when we match this length.
225+
label := typecheck.AutoLabel(".s")
226+
227+
// Search within this run of same-length strings.
228+
pos := run[0].pos
229+
s.done.Append(ir.NewLabelStmt(pos, label))
230+
s.search(run, &s.done)
231+
s.done.Append(ir.NewBranchStmt(pos, ir.OGOTO, endLabel))
232+
233+
// Add length case to outer switch.
234+
cas := ir.NewBasicLit(pos, constant.MakeInt64(runLen(run)))
235+
jmp := ir.NewBranchStmt(pos, ir.OGOTO, label)
236+
outer.Add(pos, cas, jmp)
237+
}
238+
s.done.Append(ir.NewLabelStmt(s.pos, outerLabel))
239+
outer.Emit(&s.done)
240+
s.done.Append(ir.NewLabelStmt(s.pos, endLabel))
197241
return
198242
}
199243

@@ -278,7 +322,6 @@ func (s *exprSwitch) tryJumpTable(cc []exprClause, out *ir.Nodes) bool {
278322
}
279323
}
280324
out.Append(jt)
281-
// TODO: handle the size portion of string switches using a jump table.
282325
return true
283326
}
284327

@@ -587,6 +630,7 @@ func (s *typeSwitch) flush() {
587630
}
588631
cc = merged
589632

633+
// TODO: figure out if we could use a jump table using some low bits of the type hashes.
590634
binarySearch(len(cc), &s.done,
591635
func(i int) ir.Node {
592636
return ir.NewBinaryExpr(base.Pos, ir.OLE, s.hashname, ir.NewInt(int64(cc[i-1].hash)))

0 commit comments

Comments
 (0)