From c7ccbddf22884f54885fd23143d1b2087ab6e53c Mon Sep 17 00:00:00 2001 From: Youlin Feng Date: Thu, 11 Sep 2025 23:57:38 +0800 Subject: [PATCH] cmd/compile/internal/ssa: more aggressive on dead auto elim MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Propagate "unread" across OpMoves. If the addr of this auto is only used by an OpMove as its source arg, and the OpMove's target arg is the addr of another auto. If the 2nd auto can be eliminated, this one can also be eliminated. This CL eliminates unnecessary memory copies and makes the frame smaller in the following code snippet: func contains(m map[string][16]int, k string) bool { _, ok := m[k] return ok } These are the benchmark results followed by the benchmark code: goos: linux goarch: amd64 cpu: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ Map1Access2Ok-8 9.582n ± 2% 9.226n ± 0% -3.72% (p=0.000 n=20) Map2Access2Ok-8 13.79n ± 1% 10.24n ± 1% -25.77% (p=0.000 n=20) Map3Access2Ok-8 68.68n ± 1% 12.65n ± 1% -81.58% (p=0.000 n=20) package main_test import "testing" var ( m1 = map[int]int{} m2 = map[int][16]int{} m3 = map[int][256]int{} ) func init() { for i := range 1000 { m1[i] = i m2[i] = [16]int{15:i} m3[i] = [256]int{255:i} } } func BenchmarkMap1Access2Ok(b *testing.B) { for i := range b.N { _, ok := m1[i%1000] if !ok { b.Errorf("%d not found", i) } } } func BenchmarkMap2Access2Ok(b *testing.B) { for i := range b.N { _, ok := m2[i%1000] if !ok { b.Errorf("%d not found", i) } } } func BenchmarkMap3Access2Ok(b *testing.B) { for i := range b.N { _, ok := m3[i%1000] if !ok { b.Errorf("%d not found", i) } } } Fixes #75398 Change-Id: If75e9caaa50d460efc31a94565b9ba28c8158771 Reviewed-on: https://go-review.googlesource.com/c/go/+/702875 Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Auto-Submit: Keith Randall Reviewed-by: Keith Randall Reviewed-by: Michael Pratt --- src/cmd/compile/internal/ssa/deadstore.go | 69 +++++++++++++++------- src/cmd/compile/internal/test/move_test.go | 55 +++++++++++++++++ test/codegen/maps.go | 22 +++++++ 3 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 src/cmd/compile/internal/test/move_test.go diff --git a/src/cmd/compile/internal/ssa/deadstore.go b/src/cmd/compile/internal/ssa/deadstore.go index d0adff788c..cdf290e2aa 100644 --- a/src/cmd/compile/internal/ssa/deadstore.go +++ b/src/cmd/compile/internal/ssa/deadstore.go @@ -203,9 +203,27 @@ func (sr shadowRange) merge(lo, hi int64) shadowRange { // reaches stores then we delete all the stores. The other operations will then // be eliminated by the dead code elimination pass. func elimDeadAutosGeneric(f *Func) { - addr := make(map[*Value]*ir.Name) // values that the address of the auto reaches - elim := make(map[*Value]*ir.Name) // values that could be eliminated if the auto is - var used ir.NameSet // used autos that must be kept + addr := make(map[*Value]*ir.Name) // values that the address of the auto reaches + elim := make(map[*Value]*ir.Name) // values that could be eliminated if the auto is + move := make(map[*ir.Name]ir.NameSet) // for a (Move &y &x _) and y is unused, move[y].Add(x) + var used ir.NameSet // used autos that must be kept + + // Adds a name to used and, when it is the target of a move, also + // propagates the used state to its source. + var usedAdd func(n *ir.Name) bool + usedAdd = func(n *ir.Name) bool { + if used.Has(n) { + return false + } + used.Add(n) + if s := move[n]; s != nil { + delete(move, n) + for n := range s { + usedAdd(n) + } + } + return true + } // visit the value and report whether any of the maps are updated visit := func(v *Value) (changed bool) { @@ -244,10 +262,7 @@ func elimDeadAutosGeneric(f *Func) { if !ok || (n.Class != ir.PAUTO && !isABIInternalParam(f, n)) { return } - if !used.Has(n) { - used.Add(n) - changed = true - } + changed = usedAdd(n) || changed return case OpStore, OpMove, OpZero: // v should be eliminated if we eliminate the auto. @@ -279,10 +294,22 @@ func elimDeadAutosGeneric(f *Func) { if v.Type.IsMemory() || v.Type.IsFlags() || v.Op == OpPhi || v.MemoryArg() != nil { for _, a := range args { if n, ok := addr[a]; ok { - if !used.Has(n) { - used.Add(n) - changed = true + // If the addr of n is used by an OpMove as its source arg, + // and the OpMove's target arg is the addr of a unused name, + // then temporarily treat n as unused, and record in move map. + if nam, ok := elim[v]; ok && v.Op == OpMove && !used.Has(nam) { + if used.Has(n) { + continue + } + s := move[nam] + if s == nil { + s = ir.NameSet{} + move[nam] = s + } + s.Add(n) + continue } + changed = usedAdd(n) || changed } } return @@ -291,17 +318,21 @@ func elimDeadAutosGeneric(f *Func) { // Propagate any auto addresses through v. var node *ir.Name for _, a := range args { - if n, ok := addr[a]; ok && !used.Has(n) { + if n, ok := addr[a]; ok { if node == nil { - node = n - } else if node != n { + if !used.Has(n) { + node = n + } + } else { + if node == n { + continue + } // Most of the time we only see one pointer // reaching an op, but some ops can take // multiple pointers (e.g. NeqPtr, Phi etc.). // This is rare, so just propagate the first // value to keep things simple. - used.Add(n) - changed = true + changed = usedAdd(n) || changed } } } @@ -316,8 +347,7 @@ func elimDeadAutosGeneric(f *Func) { } if addr[v] != node { // This doesn't happen in practice, but catch it just in case. - used.Add(node) - changed = true + changed = usedAdd(node) || changed } return } @@ -336,9 +366,8 @@ func elimDeadAutosGeneric(f *Func) { } // keep the auto if its address reaches a control value for _, c := range b.ControlValues() { - if n, ok := addr[c]; ok && !used.Has(n) { - used.Add(n) - changed = true + if n, ok := addr[c]; ok { + changed = usedAdd(n) || changed } } } diff --git a/src/cmd/compile/internal/test/move_test.go b/src/cmd/compile/internal/test/move_test.go new file mode 100644 index 0000000000..f361a86539 --- /dev/null +++ b/src/cmd/compile/internal/test/move_test.go @@ -0,0 +1,55 @@ +// Copyright 2025 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 test + +import "testing" + +var ( + n = [16]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + m = [16]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32} +) + +func TestEqual(t *testing.T) { + if r := move2(n, m, 0); r != n { + t.Fatalf("%v != %v", r, n) + } + if r := move2(n, m, 1); r != m { + t.Fatalf("%v != %v", r, m) + } + if r := move2p(n, m, 0); r != n { + t.Fatalf("%v != %v", r, n) + } + if r := move2p(n, m, 1); r != m { + t.Fatalf("%v != %v", r, m) + } +} + +//go:noinline +func move2(a, b [16]int, c int) [16]int { + e := a + f := b + var d [16]int + if c%2 == 0 { + d = e + } else { + d = f + } + r := d + return r +} + +//go:noinline +func move2p(a, b [16]int, c int) [16]int { + e := a + f := b + var p *[16]int + if c%2 == 0 { + p = &e + } else { + p = &f + } + r := *p + return r +} diff --git a/test/codegen/maps.go b/test/codegen/maps.go index fe38c99cb8..48438eb90c 100644 --- a/test/codegen/maps.go +++ b/test/codegen/maps.go @@ -37,6 +37,28 @@ func AccessString2(m map[string]int) bool { return ok } +func AccessStringIntArray2(m map[string][16]int, k string) bool { + // amd64:-"MOVUPS" + _, ok := m[k] + return ok +} + +type Struct struct { + A, B, C, D, E, F, G, H, I, J int +} + +func AccessStringStruct2(m map[string]Struct, k string) bool { + // amd64:-"MOVUPS" + _, ok := m[k] + return ok +} + +func AccessIntArrayLarge2(m map[int][512]int, k int) bool { + // amd64:-"REP",-"MOVSQ" + _, ok := m[k] + return ok +} + // ------------------- // // String Conversion // // ------------------- // -- 2.52.0