cmd/compile: move FuncAssignments into ReassignOracle

The standalone ir.FuncAssignments walked the entire enclosing
function on every call. Move it into ReassignOracle, which collects
the same information in a single walk during Init alongside the
existing singleDef analysis.

compilebench results (this CL vs parent):

                         │  parent.txt  │             commit.txt             │
                         │    sec/op    │    sec/op     vs base              │
Template                   153.1m ± 16%   149.5m ±  7%       ~ (p=0.699 n=6)
Unicode                    115.0m ± 15%   116.1m ±  8%       ~ (p=0.310 n=6)
GoTypes                    876.2m ±  2%   872.1m ±  2%       ~ (p=0.699 n=6)
Compiler                   156.4m ±  9%   158.2m ±  8%       ~ (p=0.818 n=6)
SSA                         7.044 ±  1%    7.044 ±  1%       ~ (p=0.937 n=6)
Flate                      172.5m ±  4%   165.7m ± 11%       ~ (p=0.132 n=6)
GoParser                   168.0m ±  3%   169.5m ± 13%       ~ (p=0.937 n=6)
Reflect                    394.2m ±  3%   391.3m ±  4%       ~ (p=0.937 n=6)
Tar                        167.1m ± 13%   167.9m ± 15%       ~ (p=0.589 n=6)
XML                        200.0m ±  7%   197.5m ± 15%       ~ (p=1.000 n=6)
LinkCompiler               638.3m ±  3%   631.2m ±  5%       ~ (p=0.699 n=6)
ExternalLinkCompiler        2.197 ±  2%    2.192 ±  1%       ~ (p=0.699 n=6)
LinkWithoutDebugCompiler   422.2m ±  4%   425.8m ±  3%       ~ (p=0.093 n=6)
StdCmd                      29.01 ±  2%    28.73 ±  1%       ~ (p=0.394 n=6)
geomean                    522.0m         519.4m        -0.50%

                         │  parent.txt  │             commit.txt             │
                         │ user-sec/op  │ user-sec/op   vs base              │
Template                   727.0m ± 15%   692.6m ±  8%       ~ (p=1.000 n=6)
Unicode                    159.7m ±  6%   161.9m ±  6%       ~ (p=0.589 n=6)
GoTypes                     4.884 ±  1%    4.907 ±  1%       ~ (p=0.937 n=6)
Compiler                   470.5m ±  9%   476.9m ± 11%       ~ (p=0.699 n=6)
SSA                         37.17 ±  2%    37.40 ±  2%       ~ (p=0.132 n=6)
Flate                      776.9m ±  9%   749.1m ± 16%       ~ (p=0.699 n=6)
GoParser                   688.5m ±  5%   676.7m ±  6%       ~ (p=0.937 n=6)
Reflect                     1.861 ±  2%    1.910 ±  3%       ~ (p=0.065 n=6)
Tar                        750.6m ± 11%   762.4m ± 23%       ~ (p=0.937 n=6)
XML                        947.8m ± 13%   926.4m ± 12%       ~ (p=0.485 n=6)
LinkCompiler                1.035 ±  6%    1.037 ±  5%       ~ (p=0.818 n=6)
ExternalLinkCompiler        2.505 ±  2%    2.506 ±  2%       ~ (p=0.699 n=6)
LinkWithoutDebugCompiler   479.0m ±  6%   490.4m ±  3%       ~ (p=0.394 n=6)
geomean                     1.181          1.180        -0.15%

          │  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

Change-Id: Iba7c593a31946fb17450106d1ee0ba9e28eedba3
Reviewed-on: https://go-review.googlesource.com/c/go/+/771161
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
This commit is contained in:
Jake Bailey 2026-04-27 09:53:53 -07:00 committed by Keith Randall
parent 5af294bac7
commit 84e0c4965a
3 changed files with 74 additions and 106 deletions

View file

@ -47,7 +47,8 @@ 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 {
fns = resolveAssignedCallees(ir.FuncAssignments(name.Canonical()))
ro := e.reassignOracle(e.curfn)
fns = resolveAssignedCallees(ro.FuncAssignments(name.Canonical()))
}
}

View file

@ -1020,95 +1020,6 @@ func Reassigned(name *Name) bool {
return Any(name.Curfn, do)
}
// 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 FuncAssignments(name *Name) []*AssignStmt {
if name.Class != PAUTO {
return nil
}
base.AssertfAt(name.Curfn != nil, name.Pos(), "PAUTO %v has nil Curfn", name)
if name.Addrtaken() {
return nil
}
if name.Type().Kind() != types.TFUNC {
return nil
}
var found []*AssignStmt
if name.Defn != nil {
if _, ok := name.Defn.(*AssignStmt); !ok {
return nil
}
}
isName := func(x Node) bool {
if x == nil {
return false
}
n, ok := OuterValue(x).(*Name)
return ok && n.Canonical() == name
}
var do func(n Node) bool
do = func(n Node) bool {
switch n.Op() {
case OAS:
as := n.(*AssignStmt)
if isName(as.X) {
if isNilAssign(as) {
break
}
found = append(found, as)
}
case OAS2, OAS2FUNC, OAS2MAPR, OAS2DOTTYPE, OAS2RECV, OSELRECV2:
as := n.(*AssignListStmt)
for _, p := range as.Lhs {
if isName(p) {
found = nil
return true
}
}
case ORANGE:
rs := n.(*RangeStmt)
if isName(rs.Key) || isName(rs.Value) {
found = nil
return true
}
case OCLOSURE:
n := n.(*ClosureExpr)
if Any(n.Func, do) {
return true
}
}
return false
}
if Any(name.Curfn, do) {
return nil
}
return found
}
// isNilAssign reports whether as has a nil or absent RHS.
func isNilAssign(as *AssignStmt) bool {
if as.Y == nil {
return true
}
y := as.Y
for y.Op() == OCONVNOP {
y = y.(*ConvExpr).X
}
return IsNil(y)
}
// StaticCalleeName returns the ONAME/PFUNC for n, if known.
func StaticCalleeName(n Node) *Name {
switch n.Op() {

View file

@ -6,6 +6,7 @@ package ir
import (
"cmd/compile/internal/base"
"cmd/compile/internal/types"
)
// A ReassignOracle efficiently answers queries about whether local
@ -21,6 +22,15 @@ type ReassignOracle struct {
// maps candidate name to its defining assignment (or
// for params, defining func).
singleDef map[*Name]Node
// funcAssigns tracks all known simple assignments (OAS) to
// func-typed PAUTO variables. Only func-typed variables are
// tracked because this data is used exclusively for callee
// resolution in escape analysis. Deletion means the candidate was
// invalidated (e.g., addr-taken, non-simple assignment form, or too
// many assignments). Assignments inside nested closures are accepted
// because the only alternative value is nil, which panics on call.
funcAssigns map[*Name][]*AssignStmt
}
// Init initializes the oracle based on the IR in function fn, laying
@ -33,6 +43,7 @@ func (ro *ReassignOracle) Init(fn *Func) {
// Collect candidate map. Start by adding function parameters
// explicitly.
ro.singleDef = make(map[*Name]Node)
ro.funcAssigns = make(map[*Name][]*AssignStmt)
sig := fn.Type()
numParams := sig.NumRecvs() + sig.NumParams()
for _, param := range fn.Dcl[:numParams] {
@ -48,8 +59,21 @@ func (ro *ReassignOracle) Init(fn *Func) {
var findLocals func(n Node) bool
findLocals = func(n Node) bool {
if nn, ok := n.(*Name); ok {
if nn.Defn != nil && !nn.Addrtaken() && nn.Class == PAUTO {
ro.singleDef[nn] = nn.Defn
if nn.Class == PAUTO && !nn.Addrtaken() {
isFunc := nn.Type().Kind() == types.TFUNC
if nn.Defn == nil {
// Bare declaration (e.g., "var f func()").
if isFunc {
ro.funcAssigns[nn] = nil
}
} else if _, ok := nn.Defn.(*AssignStmt); ok {
ro.singleDef[nn] = nn.Defn
if isFunc {
ro.funcAssigns[nn] = nil
}
} else {
ro.singleDef[nn] = nn.Defn
}
}
} else if nn, ok := n.(*ClosureExpr); ok {
Any(nn.Func, findLocals)
@ -71,26 +95,35 @@ func (ro *ReassignOracle) Init(fn *Func) {
// pruneIfNeeded examines node nn appearing on the left hand side
// of assignment statement asn to see if it contains a reassignment
// to any nodes in our candidate map ro.singleDef; if a reassignment
// is found, the corresponding name is deleted from singleDef.
// to any nodes in our candidate maps; if a reassignment is found,
// the corresponding name is deleted.
pruneIfNeeded := func(nn Node, asn Node) {
oname := outerName(nn)
if oname == nil {
return
}
defn, ok := ro.singleDef[oname]
if !ok {
return
if defn, ok := ro.singleDef[oname]; ok {
// any assignment to a param invalidates the entry.
paramAssigned := oname.Class == PPARAM
// assignment to local ok iff assignment is its orig def.
localAssigned := (oname.Class == PAUTO && asn != defn)
if paramAssigned || localAssigned {
// We found an assignment to name N that doesn't
// correspond to its original definition; remove
// from candidates.
delete(ro.singleDef, oname)
}
}
// any assignment to a param invalidates the entry.
paramAssigned := oname.Class == PPARAM
// assignment to local ok iff assignment is its orig def.
localAssigned := (oname.Class == PAUTO && asn != defn)
if paramAssigned || localAssigned {
// We found an assignment to name N that doesn't
// correspond to its original definition; remove
// from candidates.
delete(ro.singleDef, oname)
if _, ok := ro.funcAssigns[oname]; ok {
as, isOAS := asn.(*AssignStmt)
if isOAS && isNilAssign(as) {
// Zero-value assignment (nil, bare decl), skip.
} else if !isOAS {
// Not a simple assignment: invalidate.
delete(ro.funcAssigns, oname)
} else {
ro.funcAssigns[oname] = append(ro.funcAssigns[oname], as)
}
}
}
@ -203,3 +236,26 @@ func (ro *ReassignOracle) Reassigned(n *Name) bool {
}
return result
}
// FuncAssignments returns all known simple assignments to a func-typed
// variable. For variables defined with := and a non-zero value, the
// defining assignment is included. Returns nil if the variable is not
// func-typed, was invalidated (addr-taken, non-simple assignment,
// too many assignments), or has no tracked assignments. Assignments
// inside nested closures are accepted because the only alternative
// value is nil, which panics on call.
func (ro *ReassignOracle) FuncAssignments(name *Name) []*AssignStmt {
return ro.funcAssigns[name.Canonical()]
}
// isNilAssign reports whether as has a nil or absent RHS.
func isNilAssign(as *AssignStmt) bool {
if as.Y == nil {
return true
}
y := as.Y
for y.Op() == OCONVNOP {
y = y.(*ConvExpr).X
}
return IsNil(y)
}