cmd/compile: support optimizing switch statements with fallthroughs to lookup tables

Switch cases that end in a fallthrough, and the case that follows it,
can't be optimized to a lookup table. Others should still be eligible
for optimization.

Change-Id: Iebdde2ab590f2be89ba08a2dc3326553c5a4083c
Reviewed-on: https://go-review.googlesource.com/c/go/+/764440
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
qmuntal 2026-04-09 09:45:38 +02:00 committed by Quim Muntal
parent d79a0079f5
commit 1f5c165a81
2 changed files with 41 additions and 15 deletions

View file

@ -388,15 +388,6 @@ func tryLookupTable(sw *ir.SwitchStmt, cond ir.Node) {
return // 64-bit switches on 32-bit archs
}
// Bail out if any case uses fallthrough. Removing cases from the switch
// would break fallthrough chains between adjacent cases.
// TODO: we could still optimize cases that don't fall through, even if some cases do.
for _, ncase := range sw.Cases {
if fall, _ := endsInFallthrough(ncase.Body); fall {
return
}
}
fn := ir.CurFunc
if fn == nil || fn.Type().NumResults() != 1 {
return // only handle single return value
@ -427,11 +418,17 @@ func tryLookupTable(sw *ir.SwitchStmt, cond ir.Node) {
excludeSet := make(map[int64]bool) // case values with non-const bodies
var defaultVal ir.Node // non-nil if default returns a constant
minVal, maxVal := int64(math.MaxInt64), int64(math.MinInt64)
var excludeNextCase bool // true if the previous case ends in fallthrough
for i, ncase := range sw.Cases {
// A case that is the target of a fallthrough must be excluded,
// since removing it would break the fallthrough chain.
isFallthroughTarget := excludeNextCase
excludeNextCase, _ = endsInFallthrough(ncase.Body)
if len(ncase.List) == 0 {
// Default case: check if it returns a constant (for gap filling).
if isConstReturn(ncase) {
if isConstReturn(ncase) && !isFallthroughTarget {
defaultVal = ncase.Body[0].(*ir.ReturnStmt).Results[0]
}
continue
@ -451,11 +448,11 @@ func tryLookupTable(sw *ir.SwitchStmt, cond ir.Node) {
return
}
if !isConstReturn(ncase) {
// Non-const case body: exclude these values from the table
// so the mask redirects them to the normal switch, preserving
// Go's top-to-bottom case evaluation order. For example:
// case 3: sideEffect(); return 30 → exclude slot 3
if !isConstReturn(ncase) || isFallthroughTarget || excludeNextCase {
// Non-const body, fallthrough source, or fallthrough target:
// exclude these values from the table so the mask redirects
// them to the normal switch, preserving Go's top-to-bottom
// case evaluation order.
for _, v := range vals {
excludeSet[v] = true
}

View file

@ -76,6 +76,35 @@ func squareLookup(x int) int {
}
}
// lookup tables work even when some cases use fallthrough,
// as long as enough non-fallthrough cases return constants.
func fallthroughLookup(x int) int {
// amd64:`LEAQ .*\(SB\)` `MOVQ .*\(.*\)\(.*\*8\)` -`JMP \(.*\)\(.*\)$`
// arm64:`MOVD \(R.*\)\(R.*<<3\)` -`JMP \(R.*\)$`
switch x {
case 1:
return 1
case 2:
return 2
case 3:
fallthrough
case 4:
return 40
case 5:
return 5
case 6:
return 6
case 7:
return 7
case 8:
return 8
case 9:
fallthrough
default:
return x * x
}
}
// use lookup tables for 8+ bool-returning cases
func boolLookup(x int) bool {
// amd64:`LEAQ .*\(SB\)` `MOVBLZX .*\(.*\)` -`JMP \(.*\)\(.*\)$`