blob: 7c7a4dacf01c568f27dd72a8cb85a1d29a64ab77 [file] [log] [blame] [edit]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssa
import (
"cmd/compile/internal/types"
"cmd/internal/src"
"fmt"
"sort"
"testing"
)
func TestDeadStore(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
t.Logf("PTRTYPE %v", ptrType)
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, c.config.Types.Bool, 1, nil),
Valu("addr1", OpAddr, ptrType, 0, nil, "sb"),
Valu("addr2", OpAddr, ptrType, 0, nil, "sb"),
Valu("addr3", OpAddr, ptrType, 0, nil, "sb"),
Valu("zero1", OpZero, types.TypeMem, 1, c.config.Types.Bool, "addr3", "start"),
Valu("store1", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr1", "v", "zero1"),
Valu("store2", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr2", "v", "store1"),
Valu("store3", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr1", "v", "store2"),
Valu("store4", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr3", "v", "store3"),
Goto("exit")),
Bloc("exit",
Exit("store3")))
CheckFunc(fun.f)
dse(fun.f)
CheckFunc(fun.f)
v1 := fun.values["store1"]
if v1.Op != OpCopy {
t.Errorf("dead store not removed")
}
v2 := fun.values["zero1"]
if v2.Op != OpCopy {
t.Errorf("dead store (zero) not removed")
}
}
func TestDeadStorePhi(t *testing.T) {
// make sure we don't get into an infinite loop with phi values.
c := testConfig(t)
ptrType := c.config.Types.BytePtr
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, c.config.Types.Bool, 1, nil),
Valu("addr", OpAddr, ptrType, 0, nil, "sb"),
Goto("loop")),
Bloc("loop",
Valu("phi", OpPhi, types.TypeMem, 0, nil, "start", "store"),
Valu("store", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr", "v", "phi"),
If("v", "loop", "exit")),
Bloc("exit",
Exit("store")))
CheckFunc(fun.f)
dse(fun.f)
CheckFunc(fun.f)
}
func TestDeadStoreTypes(t *testing.T) {
// Make sure a narrow store can't shadow a wider one. We test an even
// stronger restriction, that one store can't shadow another unless the
// types of the address fields are identical (where identicalness is
// decided by the CSE pass).
c := testConfig(t)
t1 := c.config.Types.UInt64.PtrTo()
t2 := c.config.Types.UInt32.PtrTo()
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, c.config.Types.Bool, 1, nil),
Valu("addr1", OpAddr, t1, 0, nil, "sb"),
Valu("addr2", OpAddr, t2, 0, nil, "sb"),
Valu("store1", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr1", "v", "start"),
Valu("store2", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr2", "v", "store1"),
Goto("exit")),
Bloc("exit",
Exit("store2")))
CheckFunc(fun.f)
cse(fun.f)
dse(fun.f)
CheckFunc(fun.f)
v := fun.values["store1"]
if v.Op == OpCopy {
t.Errorf("store %s incorrectly removed", v)
}
}
func TestDeadStoreUnsafe(t *testing.T) {
// Make sure a narrow store can't shadow a wider one. The test above
// covers the case of two different types, but unsafe pointer casting
// can get to a point where the size is changed but type unchanged.
c := testConfig(t)
ptrType := c.config.Types.UInt64.PtrTo()
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, c.config.Types.Bool, 1, nil),
Valu("addr1", OpAddr, ptrType, 0, nil, "sb"),
Valu("store1", OpStore, types.TypeMem, 0, c.config.Types.Int64, "addr1", "v", "start"), // store 8 bytes
Valu("store2", OpStore, types.TypeMem, 0, c.config.Types.Bool, "addr1", "v", "store1"), // store 1 byte
Goto("exit")),
Bloc("exit",
Exit("store2")))
CheckFunc(fun.f)
cse(fun.f)
dse(fun.f)
CheckFunc(fun.f)
v := fun.values["store1"]
if v.Op == OpCopy {
t.Errorf("store %s incorrectly removed", v)
}
}
func TestDeadStoreSmallStructInit(t *testing.T) {
c := testConfig(t)
ptrType := c.config.Types.BytePtr
typ := types.NewStruct([]*types.Field{
types.NewField(src.NoXPos, &types.Sym{Name: "A"}, c.config.Types.Int),
types.NewField(src.NoXPos, &types.Sym{Name: "B"}, c.config.Types.Int),
})
name := c.Temp(typ)
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sp", OpSP, c.config.Types.Uintptr, 0, nil),
Valu("zero", OpConst64, c.config.Types.Int, 0, nil),
Valu("v6", OpLocalAddr, ptrType, 0, name, "sp", "start"),
Valu("v3", OpOffPtr, ptrType, 8, nil, "v6"),
Valu("v22", OpOffPtr, ptrType, 0, nil, "v6"),
Valu("zerostore1", OpStore, types.TypeMem, 0, c.config.Types.Int, "v22", "zero", "start"),
Valu("zerostore2", OpStore, types.TypeMem, 0, c.config.Types.Int, "v3", "zero", "zerostore1"),
Valu("v8", OpLocalAddr, ptrType, 0, name, "sp", "zerostore2"),
Valu("v23", OpOffPtr, ptrType, 8, nil, "v8"),
Valu("v25", OpOffPtr, ptrType, 0, nil, "v8"),
Valu("zerostore3", OpStore, types.TypeMem, 0, c.config.Types.Int, "v25", "zero", "zerostore2"),
Valu("zerostore4", OpStore, types.TypeMem, 0, c.config.Types.Int, "v23", "zero", "zerostore3"),
Goto("exit")),
Bloc("exit",
Exit("zerostore4")))
fun.f.Name = "smallstructinit"
CheckFunc(fun.f)
cse(fun.f)
dse(fun.f)
CheckFunc(fun.f)
v1 := fun.values["zerostore1"]
if v1.Op != OpCopy {
t.Errorf("dead store not removed")
}
v2 := fun.values["zerostore2"]
if v2.Op != OpCopy {
t.Errorf("dead store not removed")
}
}
func TestDeadStoreArrayGap(t *testing.T) {
c := testConfig(t)
ptr := c.config.Types.BytePtr
i64 := c.config.Types.Int64
typ := types.NewArray(i64, 5)
tmp := c.Temp(typ)
fun := c.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sp", OpSP, c.config.Types.Uintptr, 0, nil),
Valu("base", OpLocalAddr, ptr, 0, tmp, "sp", "start"),
Valu("p0", OpOffPtr, ptr, 0, nil, "base"),
Valu("p1", OpOffPtr, ptr, 8, nil, "base"),
Valu("p2", OpOffPtr, ptr, 16, nil, "base"),
Valu("p3", OpOffPtr, ptr, 24, nil, "base"),
Valu("p4", OpOffPtr, ptr, 32, nil, "base"),
Valu("one", OpConst64, i64, 1, nil),
Valu("seven", OpConst64, i64, 7, nil),
Valu("zero", OpConst64, i64, 0, nil),
Valu("mem0", OpZero, types.TypeMem, 40, typ, "base", "start"),
Valu("s0", OpStore, types.TypeMem, 0, i64, "p0", "one", "mem0"),
Valu("s1", OpStore, types.TypeMem, 0, i64, "p1", "seven", "s0"),
Valu("s2", OpStore, types.TypeMem, 0, i64, "p3", "one", "s1"),
Valu("s3", OpStore, types.TypeMem, 0, i64, "p4", "one", "s2"),
Valu("s4", OpStore, types.TypeMem, 0, i64, "p2", "zero", "s3"),
Goto("exit")),
Bloc("exit",
Exit("s4")))
CheckFunc(fun.f)
dse(fun.f)
CheckFunc(fun.f)
if op := fun.values["mem0"].Op; op != OpCopy {
t.Fatalf("dead Zero not removed: got %s, want OpCopy", op)
}
}
func TestShadowRanges(t *testing.T) {
t.Run("simple insert & contains", func(t *testing.T) {
var sr shadowRanges
sr.add(10, 20)
wantRanges(t, sr.ranges, [][2]uint16{{10, 20}})
if !sr.contains(12, 18) || !sr.contains(10, 20) {
t.Fatalf("contains failed after simple add")
}
if sr.contains(9, 11) || sr.contains(11, 21) {
t.Fatalf("contains erroneously true for non-contained range")
}
})
t.Run("merge overlapping", func(t *testing.T) {
var sr shadowRanges
sr.add(10, 20)
sr.add(15, 25)
wantRanges(t, sr.ranges, [][2]uint16{{10, 25}})
if !sr.contains(13, 24) {
t.Fatalf("contains should be true after merge")
}
})
t.Run("merge touching boundary", func(t *testing.T) {
var sr shadowRanges
sr.add(100, 150)
// touches at 150 - should coalesce
sr.add(150, 180)
wantRanges(t, sr.ranges, [][2]uint16{{100, 180}})
})
t.Run("union across several ranges", func(t *testing.T) {
var sr shadowRanges
sr.add(10, 20)
sr.add(30, 40)
// bridges second, not first
sr.add(25, 35)
wantRanges(t, sr.ranges, [][2]uint16{{10, 20}, {25, 40}})
// envelops everything
sr.add(5, 50)
wantRanges(t, sr.ranges, [][2]uint16{{5, 50}})
})
t.Run("disjoint intervals stay separate", func(t *testing.T) {
var sr shadowRanges
sr.add(10, 20)
sr.add(22, 30)
wantRanges(t, sr.ranges, [][2]uint16{{10, 20}, {22, 30}})
// spans both
if sr.contains(15, 25) {
t.Fatalf("contains across two disjoint ranges should be false")
}
})
t.Run("large uint16 offsets still work", func(t *testing.T) {
var sr shadowRanges
sr.add(40000, 45000)
if !sr.contains(42000, 43000) {
t.Fatalf("contains failed for large uint16 values")
}
})
t.Run("out-of-bounds inserts ignored", func(t *testing.T) {
var sr shadowRanges
sr.add(10, 20)
sr.add(-5, 5)
sr.add(70000, 70010)
wantRanges(t, sr.ranges, [][2]uint16{{10, 20}})
})
}
// canonicalise order for comparisons
func sortRanges(r []shadowRange) {
sort.Slice(r, func(i, j int) bool { return r[i].lo < r[j].lo })
}
// compare actual slice with expected pairs
func wantRanges(t *testing.T, got []shadowRange, want [][2]uint16) {
t.Helper()
sortRanges(got)
if len(got) != len(want) {
t.Fatalf("len(ranges)=%d, want %d (got=%v)", len(got), len(want), got)
}
for i, w := range want {
if got[i].lo != w[0] || got[i].hi != w[1] {
t.Fatalf("range %d = [%d,%d], want [%d,%d] (full=%v)",
i, got[i].lo, got[i].hi, w[0], w[1], got)
}
}
}
func BenchmarkDeadStore(b *testing.B) {
cfg := testConfig(b)
ptr := cfg.config.Types.BytePtr
f := cfg.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, cfg.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, cfg.config.Types.Bool, 1, nil),
Valu("a1", OpAddr, ptr, 0, nil, "sb"),
Valu("a2", OpAddr, ptr, 0, nil, "sb"),
Valu("a3", OpAddr, ptr, 0, nil, "sb"),
Valu("z1", OpZero, types.TypeMem, 1, cfg.config.Types.Bool, "a3", "start"),
Valu("s1", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a1", "v", "z1"),
Valu("s2", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a2", "v", "s1"),
Valu("s3", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a1", "v", "s2"),
Valu("s4", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a3", "v", "s3"),
Goto("exit")),
Bloc("exit",
Exit("s3")))
runBench(b, func() {
dse(f.f)
})
}
func BenchmarkDeadStorePhi(b *testing.B) {
cfg := testConfig(b)
ptr := cfg.config.Types.BytePtr
f := cfg.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, cfg.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, cfg.config.Types.Bool, 1, nil),
Valu("addr", OpAddr, ptr, 0, nil, "sb"),
Goto("loop")),
Bloc("loop",
Valu("phi", OpPhi, types.TypeMem, 0, nil, "start", "store"),
Valu("store", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "addr", "v", "phi"),
If("v", "loop", "exit")),
Bloc("exit",
Exit("store")))
runBench(b, func() {
dse(f.f)
})
}
func BenchmarkDeadStoreTypes(b *testing.B) {
cfg := testConfig(b)
t1 := cfg.config.Types.UInt64.PtrTo()
t2 := cfg.config.Types.UInt32.PtrTo()
f := cfg.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, cfg.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, cfg.config.Types.Bool, 1, nil),
Valu("a1", OpAddr, t1, 0, nil, "sb"),
Valu("a2", OpAddr, t2, 0, nil, "sb"),
Valu("s1", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a1", "v", "start"),
Valu("s2", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a2", "v", "s1"),
Goto("exit")),
Bloc("exit",
Exit("s2")))
cse(f.f)
runBench(b, func() {
dse(f.f)
})
}
func BenchmarkDeadStoreUnsafe(b *testing.B) {
cfg := testConfig(b)
ptr := cfg.config.Types.UInt64.PtrTo()
f := cfg.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, cfg.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, cfg.config.Types.Bool, 1, nil),
Valu("a1", OpAddr, ptr, 0, nil, "sb"),
Valu("s1", OpStore, types.TypeMem, 0, cfg.config.Types.Int64, "a1", "v", "start"),
Valu("s2", OpStore, types.TypeMem, 0, cfg.config.Types.Bool, "a1", "v", "s1"),
Goto("exit")),
Bloc("exit",
Exit("s2")))
cse(f.f)
runBench(b, func() {
dse(f.f)
})
}
func BenchmarkDeadStoreSmallStructInit(b *testing.B) {
cfg := testConfig(b)
ptr := cfg.config.Types.BytePtr
typ := types.NewStruct([]*types.Field{
types.NewField(src.NoXPos, &types.Sym{Name: "A"}, cfg.config.Types.Int),
types.NewField(src.NoXPos, &types.Sym{Name: "B"}, cfg.config.Types.Int),
})
tmp := cfg.Temp(typ)
f := cfg.Fun("entry",
Bloc("entry",
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sp", OpSP, cfg.config.Types.Uintptr, 0, nil),
Valu("zero", OpConst64, cfg.config.Types.Int, 0, nil),
Valu("v6", OpLocalAddr, ptr, 0, tmp, "sp", "start"),
Valu("v3", OpOffPtr, ptr, 8, nil, "v6"),
Valu("v22", OpOffPtr, ptr, 0, nil, "v6"),
Valu("s1", OpStore, types.TypeMem, 0, cfg.config.Types.Int, "v22", "zero", "start"),
Valu("s2", OpStore, types.TypeMem, 0, cfg.config.Types.Int, "v3", "zero", "s1"),
Valu("v8", OpLocalAddr, ptr, 0, tmp, "sp", "s2"),
Valu("v23", OpOffPtr, ptr, 8, nil, "v8"),
Valu("v25", OpOffPtr, ptr, 0, nil, "v8"),
Valu("s3", OpStore, types.TypeMem, 0, cfg.config.Types.Int, "v25", "zero", "s2"),
Valu("s4", OpStore, types.TypeMem, 0, cfg.config.Types.Int, "v23", "zero", "s3"),
Goto("exit")),
Bloc("exit",
Exit("s4")))
cse(f.f)
runBench(b, func() {
dse(f.f)
})
}
func BenchmarkDeadStoreLargeBlock(b *testing.B) {
// create a very large block with many shadowed stores
const (
addrCount = 128
// first 7 are dead
storesPerAddr = 8
)
cfg := testConfig(b)
ptrType := cfg.config.Types.BytePtr
boolType := cfg.config.Types.Bool
items := []interface{}{
Valu("start", OpInitMem, types.TypeMem, 0, nil),
Valu("sb", OpSB, cfg.config.Types.Uintptr, 0, nil),
Valu("v", OpConstBool, boolType, 1, nil),
}
for i := 0; i < addrCount; i++ {
items = append(items,
Valu(fmt.Sprintf("addr%d", i), OpAddr, ptrType, 0, nil, "sb"),
)
}
prev := "start"
for round := 0; round < storesPerAddr; round++ {
for i := 0; i < addrCount; i++ {
store := fmt.Sprintf("s_%03d_%d", i, round)
addr := fmt.Sprintf("addr%d", i)
items = append(items,
Valu(store, OpStore, types.TypeMem, 0, boolType, addr, "v", prev),
)
prev = store
}
}
items = append(items, Goto("exit"))
entryBlk := Bloc("entry", items...)
exitBlk := Bloc("exit", Exit(prev))
f := cfg.Fun("stress", entryBlk, exitBlk)
runBench(b, func() {
dse(f.f)
})
}
func runBench(b *testing.B, build func()) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
build()
}
}