mirror of
https://github.com/golang/go.git
synced 2026-06-27 03:11:23 +00:00
cmd/compile: treat singly-assigned func vars as static in escape analysis
When a function variable is declared without an initializer and then
assigned exactly once, escape analysis cannot identify the callee:
var f func(*int)
f = func(p *int) {} // callee not visible to StaticValue
f(new(int)) // new(int) conservatively escapes
This pattern is common for recursive closures:
var visit func(*Node)
visit = func(n *Node) {
for _, child := range n.Children {
visit(child)
}
}
visit(root)
There is a trick we can use here: a nil func cannot be called, because
it panics, and a panic cannot cause anything to escape. So for
variables declared without an initializer and assigned exactly once, we
can treat that single assignment as the static value for escape
analysis purposes, since the only other possible value (nil) is a dead
end.
Add ir.FuncSingleAssignment to find such singly-assigned func variables,
and use it as a fallback in escape analysis when StaticCalleeName fails.
This is restricted to func-typed variables only: tracking all local
variables is too expensive, and only func-typed variables benefit from
callee resolution. A future CL will instead do this in the "oracle",
but I am splitting that up as I think it is wasteful to do that and
then not use it to replace StaticValue, but apparently that has its
own issues.
This optimization resolves callees 7 times in std, 100 times in cmd,
51 times in github.com/microsoft/typescript-go, 276 times in x/tools,
and 547 times in x/tools/gopls. Most resolutions improve parameter
escape precision; of those, 5 heap allocations are eliminated in
std/cmd, 6 in x/tools, 5 in x/tools/gopls, and 16 in typescript-go.
compilebench results (this CL vs parent):
│ sec/op │ sec/op vs base │
Template 157.8m ± 6% 157.9m ± 10% ~ (p=0.699 n=6)
Unicode 121.7m ± 12% 122.1m ± 4% ~ (p=0.589 n=6)
GoTypes 897.4m ± 5% 902.5m ± 3% ~ (p=0.937 n=6)
Compiler 163.1m ± 5% 162.4m ± 2% ~ (p=0.818 n=6)
SSA 7.269 ± 2% 7.177 ± 1% ~ (p=0.699 n=6)
Flate 167.6m ± 5% 167.7m ± 14% ~ (p=1.000 n=6)
GoParser 180.5m ± 5% 179.1m ± 5% ~ (p=1.000 n=6)
Reflect 399.5m ± 2% 411.2m ± 7% ~ (p=0.310 n=6)
Tar 172.2m ± 7% 180.0m ± 9% ~ (p=0.699 n=6)
XML 201.1m ± 6% 197.4m ± 4% ~ (p=0.699 n=6)
LinkCompiler 675.7m ± 2% 672.7m ± 2% ~ (p=0.485 n=6)
ExternalLinkCompiler 2.313 ± 2% 2.330 ± 1% ~ (p=0.394 n=6)
LinkWithoutDebugCompiler 430.5m ± 3% 430.9m ± 6% ~ (p=0.818 n=6)
StdCmd 30.43 ± 5% 30.31 ± 2% ~ (p=0.699 n=6)
geomean 539.1m 540.6m +0.28%
Updates #59708
Fixes #70171
Change-Id: I6b701748f05f62376a46bc811181698d8ef76d47
Reviewed-on: https://go-review.googlesource.com/c/go/+/771160
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Mateusz Poliwczak <mpoliwczak34@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
parent
1a7e601d07
commit
f4bfb1a9c6
3 changed files with 204 additions and 0 deletions
|
|
@ -43,6 +43,20 @@ func (e *escape) call(ks []hole, call ir.Node) {
|
|||
// TODO(thepudds): use an ir.ReassignOracle here.
|
||||
v := ir.StaticValue(call.Fun)
|
||||
fn = ir.StaticCalleeName(v)
|
||||
if fn == nil {
|
||||
// If the variable is declared without an initializer
|
||||
// and assigned exactly once, we can use that
|
||||
// assignment to identify the callee. The only
|
||||
// alternative value is the zero value (nil for
|
||||
// func types), which panics on call and so can't
|
||||
// cause any escape.
|
||||
if name, ok := v.(*ir.Name); ok {
|
||||
orig := name.Canonical()
|
||||
if as := ir.FuncSingleAssignment(orig); as != nil {
|
||||
fn = ir.StaticCalleeName(as.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// argumentParam handles escape analysis of assigning a call
|
||||
|
|
|
|||
|
|
@ -1020,6 +1020,105 @@ 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.
|
||||
//
|
||||
// TODO: fold this into [ReassignOracle] so it can share the single
|
||||
// walk with StaticValue and Reassigned.
|
||||
func FuncSingleAssignment(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
|
||||
}
|
||||
// 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).
|
||||
if name.Defn != nil {
|
||||
as, ok := name.Defn.(*AssignStmt)
|
||||
if !ok || !isNilAssign(as) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
isName := func(x Node) bool {
|
||||
if x == nil {
|
||||
return false
|
||||
}
|
||||
n, ok := OuterValue(x).(*Name)
|
||||
return ok && n.Canonical() == name
|
||||
}
|
||||
|
||||
var found *AssignStmt
|
||||
|
||||
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
|
||||
}
|
||||
if found != nil {
|
||||
found = nil
|
||||
return true
|
||||
}
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -191,3 +191,94 @@ func ClosureIndirect2() {
|
|||
}
|
||||
|
||||
func nopFunc2(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0"
|
||||
|
||||
func ClosureIndirectDeclAssign() {
|
||||
var f func(p *int)
|
||||
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
|
||||
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
||||
func ClosureIndirectDeclAssign2() {
|
||||
var f func(p *int) *int
|
||||
f = func(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0" "func literal does not escape"
|
||||
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
||||
func ClosureIndirectDeclAssign3() {
|
||||
var f func(p *int)
|
||||
f = func(p *int) { // ERROR "leaking param: p" "func literal does not escape"
|
||||
sink = p
|
||||
}
|
||||
f(new(int)) // ERROR "new\(int\) escapes to heap"
|
||||
}
|
||||
|
||||
func ClosureIndirectDeclReassign() {
|
||||
var f func(p *int)
|
||||
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
|
||||
f = func(p *int) { // ERROR "leaking param: p" "func literal does not escape"
|
||||
sink = p
|
||||
}
|
||||
f(new(int)) // ERROR "new\(int\) escapes to heap"
|
||||
}
|
||||
|
||||
func ClosureIndirectDeclRecursive() {
|
||||
var visit func(p *int)
|
||||
visit = func(p *int) { // ERROR "p does not escape" "func literal does not escape"
|
||||
visit(p)
|
||||
}
|
||||
visit(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
||||
func ClosureIndirectNilInit() {
|
||||
var f func(p *int) = nil
|
||||
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
|
||||
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
||||
func ClosureIndirectTypedNilInit() {
|
||||
var f func(p *int) = (func(*int))(nil)
|
||||
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
|
||||
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
||||
func ClosureIndirectNestedAssign() {
|
||||
var f func(p *int)
|
||||
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"
|
||||
}
|
||||
|
||||
func ClosureIndirectNestedAssignLeak() {
|
||||
var f func(p *int)
|
||||
func() { // ERROR "func literal does not escape"
|
||||
f = func(p *int) { // ERROR "leaking param: p" "func literal escapes to heap"
|
||||
sink = p
|
||||
}
|
||||
}()
|
||||
f(new(int)) // ERROR "new\(int\) escapes to heap"
|
||||
}
|
||||
|
||||
func ClosureIndirectTypeSwitch() {
|
||||
foo := any(func(a *int) { sink = a }) // ERROR "func literal does not escape" "leaking param: a"
|
||||
switch foo := foo.(type) {
|
||||
case func(a *int):
|
||||
foo(new(int)) // ERROR "new\(int\) escapes to heap"
|
||||
}
|
||||
}
|
||||
|
||||
func ClosureIndirectTypeSwitchReassign() {
|
||||
foo := any(func(a *int) { sink = a }) // ERROR "func literal does not escape" "leaking param: a"
|
||||
switch foo := foo.(type) {
|
||||
case func(a *int):
|
||||
foo = func(a *int) {} // ERROR "func literal does not escape" "a does not escape"
|
||||
foo(new(int)) // ERROR "new\(int\) escapes to heap"
|
||||
}
|
||||
}
|
||||
|
||||
func ClosureIndirectNilReassign() {
|
||||
var f func(p *int)
|
||||
f = nil
|
||||
f = func(p *int) {} // ERROR "p does not escape" "func literal does not escape"
|
||||
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue