// 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() } }