mirror of https://github.com/golang/go.git
435 lines
12 KiB
Go
435 lines
12 KiB
Go
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")
|
|
}
|
|
}
|