blob: 16d94614d815c575435a220ee1a4e385d61e5dbc [file] [log] [blame]
package ssa
import (
"cmd/compile/internal/types"
"strconv"
"testing"
)
func BenchmarkNilCheckDeep1(b *testing.B) { benchmarkNilCheckDeep(b, 1) }
func BenchmarkNilCheckDeep10(b *testing.B) { benchmarkNilCheckDeep(b, 10) }
func BenchmarkNilCheckDeep100(b *testing.B) { benchmarkNilCheckDeep(b, 100) }
func BenchmarkNilCheckDeep1000(b *testing.B) { benchmarkNilCheckDeep(b, 1000) }
func BenchmarkNilCheckDeep10000(b *testing.B) { benchmarkNilCheckDeep(b, 10000) }
// benchmarkNilCheckDeep is a stress test of nilcheckelim.
// It uses the worst possible input: A linear string of
// nil checks, none of which can be eliminated.
// Run with multiple depths to observe big-O behavior.
func benchmarkNilCheckDeep(b *testing.B, depth int) {
c := testConfig(b)
ptrType := c.config.Types.BytePtr
var blocs []bloc
blocs = append(blocs,
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto(blockn(0)),
),
)
for i := 0; i < depth; i++ {
blocs = append(blocs,
Bloc(blockn(i),
Valu(ptrn(i), OpAddr, ptrType, 0, nil, "sb"),
Valu(booln(i), OpIsNonNil, c.config.Types.Bool, 0, nil, ptrn(i)),
If(booln(i), blockn(i+1), "exit"),
),
)
}
blocs = append(blocs,
Bloc(blockn(depth), Goto("exit")),
Bloc("exit", Exit("mem")),
)
fun := c.Fun("entry", blocs...)
CheckFunc(fun.f)
b.SetBytes(int64(depth)) // helps for eyeballing linearity
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
nilcheckelim(fun.f)
}
}
func blockn(n int) string { return "b" + strconv.Itoa(n) }
func ptrn(n int) string { return "p" + strconv.Itoa(n) }
func booln(n int) string { return "c" + strconv.Itoa(n) }
func isNilCheck(b *Block) bool {
return b.Kind == BlockIf && b.Controls[0].Op == OpIsNonNil
}
// TestNilcheckSimple verifies that a second repeated nilcheck is removed.
func TestNilcheckSimple(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "secondCheck", "exit")),
Bloc("secondCheck",
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool2", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
t.Errorf("secondCheck was not eliminated")
}
}
}
// TestNilcheckDomOrder ensures that the nil check elimination isn't dependent
// on the order of the dominees.
func TestNilcheckDomOrder(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "secondCheck", "exit")),
Bloc("exit",
Exit("mem")),
Bloc("secondCheck",
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool2", "extra", "exit")),
Bloc("extra",
Goto("exit")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
t.Errorf("secondCheck was not eliminated")
}
}
}
// TestNilcheckAddr verifies that nilchecks of OpAddr constructed values are removed.
func TestNilcheckAddr(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpAddr, ptrType, 0, nil, "sb"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["checkPtr"] && isNilCheck(b) {
t.Errorf("checkPtr was not eliminated")
}
}
}
// TestNilcheckAddPtr verifies that nilchecks of OpAddPtr constructed values are removed.
func TestNilcheckAddPtr(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("off", OpConst64, c.config.Types.Int64, 20, nil),
Valu("ptr1", OpAddPtr, ptrType, 0, nil, "sb", "off"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["checkPtr"] && isNilCheck(b) {
t.Errorf("checkPtr was not eliminated")
}
}
}
// TestNilcheckPhi tests that nil checks of phis, for which all values are known to be
// non-nil are removed.
func TestNilcheckPhi(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Valu("sp", OpSP, c.config.Types.Uintptr, 0, nil),
Valu("baddr", OpLocalAddr, c.config.Types.Bool, 0, "b", "sp", "mem"),
Valu("bool1", OpLoad, c.config.Types.Bool, 0, nil, "baddr", "mem"),
If("bool1", "b1", "b2")),
Bloc("b1",
Valu("ptr1", OpAddr, ptrType, 0, nil, "sb"),
Goto("checkPtr")),
Bloc("b2",
Valu("ptr2", OpAddr, ptrType, 0, nil, "sb"),
Goto("checkPtr")),
// both ptr1 and ptr2 are guaranteed non-nil here
Bloc("checkPtr",
Valu("phi", OpPhi, ptrType, 0, nil, "ptr1", "ptr2"),
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "phi"),
If("bool2", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["checkPtr"] && isNilCheck(b) {
t.Errorf("checkPtr was not eliminated")
}
}
}
// TestNilcheckKeepRemove verifies that duplicate checks of the same pointer
// are removed, but checks of different pointers are not.
func TestNilcheckKeepRemove(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "differentCheck", "exit")),
Bloc("differentCheck",
Valu("ptr2", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr2"),
If("bool2", "secondCheck", "exit")),
Bloc("secondCheck",
Valu("bool3", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool3", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
foundDifferentCheck := false
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
t.Errorf("secondCheck was not eliminated")
}
if b == fun.blocks["differentCheck"] && isNilCheck(b) {
foundDifferentCheck = true
}
}
if !foundDifferentCheck {
t.Errorf("removed differentCheck, but shouldn't have")
}
}
// TestNilcheckInFalseBranch tests that nil checks in the false branch of a nilcheck
// block are *not* removed.
func TestNilcheckInFalseBranch(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("bool1", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool1", "extra", "secondCheck")),
Bloc("secondCheck",
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool2", "extra", "thirdCheck")),
Bloc("thirdCheck",
Valu("bool3", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool3", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
foundSecondCheck := false
foundThirdCheck := false
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
foundSecondCheck = true
}
if b == fun.blocks["thirdCheck"] && isNilCheck(b) {
foundThirdCheck = true
}
}
if !foundSecondCheck {
t.Errorf("removed secondCheck, but shouldn't have [false branch]")
}
if !foundThirdCheck {
t.Errorf("removed thirdCheck, but shouldn't have [false branch]")
}
}
// TestNilcheckUser verifies that a user nil check that dominates a generated nil check
// wil remove the generated nil check.
func TestNilcheckUser(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("nilptr", OpConstNil, ptrType, 0, nil),
Valu("bool1", OpNeqPtr, c.config.Types.Bool, 0, nil, "ptr1", "nilptr"),
If("bool1", "secondCheck", "exit")),
Bloc("secondCheck",
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool2", "extra", "exit")),
Bloc("extra",
Goto("exit")),
Bloc("exit",
Exit("mem")))
CheckFunc(fun.f)
// we need the opt here to rewrite the user nilcheck
opt(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
t.Errorf("secondCheck was not eliminated")
}
}
}
// TestNilcheckBug reproduces a bug in nilcheckelim found by compiling math/big
func TestNilcheckBug(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("mem", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Goto("checkPtr")),
Bloc("checkPtr",
Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
Valu("nilptr", OpConstNil, ptrType, 0, nil),
Valu("bool1", OpNeqPtr, c.config.Types.Bool, 0, nil, "ptr1", "nilptr"),
If("bool1", "secondCheck", "couldBeNil")),
Bloc("couldBeNil",
Goto("secondCheck")),
Bloc("secondCheck",
Valu("bool2", OpIsNonNil, c.config.Types.Bool, 0, nil, "ptr1"),
If("bool2", "extra", "exit")),
Bloc("extra",
// prevent fuse from eliminating this block
Valu("store", OpStore, types.TypeMem, 0, ptrType, "ptr1", "nilptr", "mem"),
Goto("exit")),
Bloc("exit",
Valu("phi", OpPhi, types.TypeMem, 0, nil, "mem", "store"),
Exit("phi")))
CheckFunc(fun.f)
// we need the opt here to rewrite the user nilcheck
opt(fun.f)
nilcheckelim(fun.f)
// clean up the removed nil check
fuse(fun.f, fuseTypePlain)
deadcode(fun.f)
CheckFunc(fun.f)
foundSecondCheck := false
for _, b := range fun.f.Blocks {
if b == fun.blocks["secondCheck"] && isNilCheck(b) {
foundSecondCheck = true
}
}
if !foundSecondCheck {
t.Errorf("secondCheck was eliminated, but shouldn't have")
}
}