cmd/compile: handle multiply-assigned func vars in escape analysis

Generalize FuncSingleAssignment to FuncAssignments, tracking all
non-zero assignments to func-typed variables. When multiple
callees are known, escape analysis merges their parameter escape
behaviors via teeHole.

compilebench results (this CL vs parent):

                         │  parent.txt  │             commit.txt             │
                         │    sec/op    │    sec/op     vs base              │
Template                   150.8m ± 12%   149.6m ± 12%       ~ (p=0.589 n=6)
Unicode                    120.2m ± 11%   117.2m ±  5%       ~ (p=0.699 n=6)
GoTypes                    890.6m ±  3%   884.8m ±  2%       ~ (p=0.310 n=6)
Compiler                   161.2m ± 13%   154.4m ±  4%       ~ (p=0.180 n=6)
SSA                         7.023 ±  1%    7.021 ±  1%       ~ (p=0.485 n=6)
Flate                      162.6m ±  6%   162.0m ±  6%       ~ (p=0.699 n=6)
GoParser                   176.9m ±  7%   178.0m ±  6%       ~ (p=0.699 n=6)
Reflect                    393.2m ±  4%   389.5m ±  5%       ~ (p=0.699 n=6)
Tar                        171.5m ± 14%   172.9m ±  5%       ~ (p=0.937 n=6)
XML                        197.1m ±  6%   197.7m ±  2%       ~ (p=0.818 n=6)
LinkCompiler               650.5m ±  4%   637.0m ±  2%       ~ (p=0.394 n=6)
ExternalLinkCompiler        2.196 ±  1%    2.206 ±  1%       ~ (p=0.240 n=6)
LinkWithoutDebugCompiler   421.2m ±  6%   423.4m ±  7%       ~ (p=0.699 n=6)
StdCmd                      29.21 ±  2%    29.27 ±  1%       ~ (p=0.937 n=6)
geomean                    525.6m         522.3m        -0.63%

                         │  parent.txt  │             commit.txt             │
                         │ user-sec/op  │ user-sec/op   vs base              │
Template                   728.1m ±  5%   686.9m ± 11%       ~ (p=0.180 n=6)
Unicode                    158.9m ± 12%   160.9m ±  8%       ~ (p=0.937 n=6)
GoTypes                     4.921 ±  4%    4.919 ±  2%       ~ (p=0.937 n=6)
Compiler                   502.4m ±  9%   505.3m ± 10%       ~ (p=0.818 n=6)
SSA                         37.77 ±  2%    37.06 ±  4%       ~ (p=0.699 n=6)
Flate                      751.3m ± 11%   759.7m ±  8%       ~ (p=0.937 n=6)
GoParser                   677.4m ±  5%   674.4m ±  8%       ~ (p=0.394 n=6)
Reflect                     1.888 ±  2%    1.901 ±  3%       ~ (p=0.589 n=6)
Tar                        772.1m ± 14%   789.7m ±  5%       ~ (p=0.937 n=6)
XML                        911.8m ±  9%   916.0m ± 12%       ~ (p=0.937 n=6)
LinkCompiler                1.077 ±  4%    1.059 ±  5%       ~ (p=0.310 n=6)
ExternalLinkCompiler        2.539 ±  2%    2.531 ±  3%       ~ (p=1.000 n=6)
LinkWithoutDebugCompiler   489.7m ±  4%   485.3m ±  5%       ~ (p=1.000 n=6)
geomean                     1.192          1.187        -0.36%

          │  parent.txt  │              commit.txt              │
          │  text-bytes  │  text-bytes   vs base                │
HelloSize   1.110Mi ± 0%   1.110Mi ± 0%       ~ (p=1.000 n=6) ¹
CmdGoSize   14.14Mi ± 0%   14.14Mi ± 0%       ~ (p=1.000 n=6) ¹
geomean     3.961Mi        3.961Mi       +0.00%
¹ all samples are equal

          │  parent.txt  │              commit.txt              │
          │  data-bytes  │  data-bytes   vs base                │
HelloSize   27.54Ki ± 0%   27.54Ki ± 0%       ~ (p=1.000 n=6) ¹
CmdGoSize   431.4Ki ± 0%   431.4Ki ± 0%       ~ (p=1.000 n=6) ¹
geomean     109.0Ki        109.0Ki       +0.00%
¹ all samples are equal

          │  parent.txt  │              commit.txt              │
          │  bss-bytes   │  bss-bytes    vs base                │
HelloSize   213.9Ki ± 0%   213.9Ki ± 0%       ~ (p=1.000 n=6) ¹
CmdGoSize   32.27Mi ± 0%   32.27Mi ± 0%       ~ (p=1.000 n=6) ¹
geomean     2.597Mi        2.597Mi       +0.00%
¹ all samples are equal

          │  parent.txt  │              commit.txt              │
          │  exe-bytes   │  exe-bytes    vs base                │
HelloSize   1.782Mi ± 0%   1.782Mi ± 0%       ~ (p=1.000 n=6) ¹
CmdGoSize   21.29Mi ± 0%   21.29Mi ± 0%       ~ (p=1.000 n=6) ¹
geomean     6.158Mi        6.158Mi       +0.00%
¹ all samples are equal

Fixes #73132

Change-Id: Ice3ed672db28dbfd4bdc788019111d5d3092c5bb
Reviewed-on: https://go-review.googlesource.com/c/go/+/771500
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
This commit is contained in:
Jake Bailey 2026-05-07 19:40:28 -07:00 committed by Keith Randall
parent 3c05d2a519
commit 5af294bac7
3 changed files with 142 additions and 28 deletions

View file

@ -47,12 +47,7 @@ func (e *escape) call(ks []hole, call ir.Node) {
if fn := ir.StaticCalleeName(v); fn != nil {
fns = []*ir.Name{fn}
} else if name, ok := v.(*ir.Name); ok {
orig := name.Canonical()
if as := ir.FuncSingleAssignment(orig); as != nil {
if callee := ir.StaticCalleeName(as.Y); callee != nil {
fns = []*ir.Name{callee}
}
}
fns = resolveAssignedCallees(ir.FuncAssignments(name.Canonical()))
}
}
@ -429,6 +424,27 @@ func (e *escape) tagHole(ks []hole, fn *ir.Name, param *types.Field) hole {
return e.teeHole(tagKs...)
}
// resolveAssignedCallees resolves all assignment RHS values to static
// callee names, skipping zero-value assignments since nil panics on
// call and can't cause escape.
func resolveAssignedCallees(assigns []*ir.AssignStmt) []*ir.Name {
fns := make([]*ir.Name, 0, len(assigns))
for _, as := range assigns {
if ir.IsZero(as.Y) {
continue // zero value panics on call; skip
}
callee := ir.StaticCalleeName(as.Y)
if callee == nil {
return nil
}
if callee.Func != nil && callee.Func.Pragma&(ir.UintptrKeepAlive|ir.UintptrEscapes) != 0 {
return nil
}
fns = append(fns, callee)
}
return fns
}
func isEscapeNonString(fns []*ir.Name, fntype *types.Type) bool {
return len(fns) == 1 &&
fns[0].Sym().Pkg.Path == "internal/abi" &&

View file

@ -1020,19 +1020,18 @@ func Reassigned(name *Name) bool {
return Any(name.Curfn, do)
}
// FuncSingleAssignment returns the sole OAS *AssignStmt that assigns a
// non-zero value to name, if name is a func-typed local variable (PAUTO)
// with exactly one such assignment. Zero-value assignments (nil, bare
// declarations) are ignored since nil panics on call. Returns nil if the
// variable is not PAUTO, not func-typed, address-taken, has multiple
// non-zero assignments, or has any complex assignments (OAS2, ORANGE).
// Assignments inside nested closures are accepted because this is only
// used for escape analysis callee resolution: the only alternative value
// is nil, which panics on call.
// FuncAssignments returns all simple (OAS) assignments of non-zero
// values to name, if name is a func-typed local variable (PAUTO).
// Zero-value assignments (nil, bare declarations) are ignored since
// nil panics on call. Returns nil if the variable is not PAUTO, not
// func-typed, address-taken, has any complex assignment (OAS2, ORANGE),
// or has too many assignments. Assignments inside nested closures are
// accepted because this is only used for escape analysis callee
// resolution: the only alternative value is nil, which panics on call.
//
// TODO: fold this into [ReassignOracle] so it can share the single
// walk with StaticValue and Reassigned.
func FuncSingleAssignment(name *Name) *AssignStmt {
func FuncAssignments(name *Name) []*AssignStmt {
if name.Class != PAUTO {
return nil
}
@ -1043,12 +1042,9 @@ func FuncSingleAssignment(name *Name) *AssignStmt {
if name.Type().Kind() != types.TFUNC {
return nil
}
// Reject variables with non-zero defining assignments we can't
// analyze (e.g., type switch case variables whose Defn is a
// TypeSwitchGuard, not an AssignStmt).
var found []*AssignStmt
if name.Defn != nil {
as, ok := name.Defn.(*AssignStmt)
if !ok || !isNilAssign(as) {
if _, ok := name.Defn.(*AssignStmt); !ok {
return nil
}
}
@ -1061,8 +1057,6 @@ func FuncSingleAssignment(name *Name) *AssignStmt {
return ok && n.Canonical() == name
}
var found *AssignStmt
var do func(n Node) bool
do = func(n Node) bool {
switch n.Op() {
@ -1072,11 +1066,7 @@ func FuncSingleAssignment(name *Name) *AssignStmt {
if isNilAssign(as) {
break
}
if found != nil {
found = nil
return true
}
found = as
found = append(found, as)
}
case OAS2, OAS2FUNC, OAS2MAPR, OAS2DOTTYPE, OAS2RECV, OSELRECV2:
as := n.(*AssignListStmt)

View file

@ -282,3 +282,111 @@ func ClosureIndirectNilReassign() {
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectMultiAssign(b bool) {
var f func(p *int)
if b {
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
} else {
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
}
f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectMultiAssignNamed(b bool) {
var f func(*int)
if b {
f = nopFunc
} else {
f = nopFunc
}
f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectMultiAssignResult(b bool) *int {
var f func(p *int) *int
if b {
f = func(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0" "func literal does not escape"
} else {
f = func(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0" "func literal does not escape"
}
return f(new(int)) // ERROR "new\(int\) escapes to heap"
}
func ClosureIndirectMultiAssignSafe(b bool) int {
var f func(p *int) int
if b {
f = func(p *int) int { return *p } // ERROR "p does not escape" "func literal does not escape"
} else {
f = func(p *int) int { return 42 } // ERROR "p does not escape" "func literal does not escape"
}
return f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectTripleAssign(x int) {
var f func(p *int)
switch x {
case 1:
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
case 2:
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
default:
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
}
f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectReassignInit(b bool) {
f := func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
if b {
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
}
f(new(int)) // ERROR "new\(int\) does not escape"
}
func ClosureIndirectNestedMultiAssign(b bool) {
var f func(p *int)
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
func() { // ERROR "func literal does not escape"
f = func(p *int) {} // ERROR "p does not escape" "func literal escapes to heap"
}()
f(new(int)) // ERROR "new\(int\) does not escape"
}
type myFloat struct{ v float64 }
func (f *myFloat) add(p *myFloat) *myFloat { // ERROR "leaking param: f to result ~r0 level=0" "p does not escape"
f.v += p.v
return f
}
func (f *myFloat) sub(p *myFloat) *myFloat { // ERROR "leaking param: f to result ~r0 level=0" "p does not escape"
f.v -= p.v
return f
}
func ClosureIndirectMethodExpr(b bool) {
var op func(*myFloat, *myFloat) *myFloat
if b {
op = (*myFloat).add
} else {
op = (*myFloat).sub
}
f := &myFloat{1.0} // ERROR "&myFloat{...} does not escape"
g := &myFloat{2.0} // ERROR "&myFloat{...} does not escape"
op(f, g)
}
func ClosureIndirectMethodExprMixed(b bool) {
var op func(*myFloat, *myFloat) *myFloat
if b {
op = (*myFloat).add
} else {
op = func(f, g *myFloat) *myFloat { // ERROR "f does not escape" "g does not escape" "func literal does not escape"
return nil
}
}
f := &myFloat{1.0} // ERROR "&myFloat{...} does not escape"
g := &myFloat{2.0} // ERROR "&myFloat{...} does not escape"
op(f, g)
}