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