diff options
Diffstat (limited to 'src/cmd/compile/internal/ssa/nilcheck_test.go')
-rw-r--r-- | src/cmd/compile/internal/ssa/nilcheck_test.go | 434 |
1 files changed, 434 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/ssa/nilcheck_test.go b/src/cmd/compile/internal/ssa/nilcheck_test.go new file mode 100644 index 0000000..16d9461 --- /dev/null +++ b/src/cmd/compile/internal/ssa/nilcheck_test.go @@ -0,0 +1,434 @@ +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") + } +} |