| 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") | |
| } | |
| } |