cmd/compile: wrap/desugar defer calls for register abi

Adds code to the compiler's "order" phase to rewrite go and defer
statements to always be argument-less. E.g.

 defer f(x,y)       =>     x1, y1 := x, y
			   defer func() { f(x1, y1) }

This transformation is not beneficial on its own, but it helps
simplify runtime defer handling for the new register ABI (when
invoking deferred functions on the panic path, the runtime doesn't
need to manage the complexity of determining which args to pass in
register vs memory).

This feature is currently enabled by default if GOEXPERIMENT=regabi or
GOEXPERIMENT=regabidefer is in effect.

Included in this CL are some workarounds in the runtime to insure that
"go" statement targets in the runtime are argument-less already (since
wrapping them can potentially introduce heap-allocated closures, which
are currently not allowed). The expectation is that these workarounds
will be temporary, and can go away once we either A) change the rules
about heap-allocated closures, or B) implement some other scheme for
handling go statements.

Change-Id: I01060d79a6b140c6f0838d6e6813f807ccdca319
Reviewed-on: https://go-review.googlesource.com/c/go/+/298669
Trust: Than McIntosh <thanm@google.com>
Run-TryBot: Than McIntosh <thanm@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Than McIntosh 2021-02-24 12:55:52 -05:00
parent 4e27aa6cd2
commit 769d4b68ef
12 changed files with 344 additions and 41 deletions

View file

@ -157,12 +157,13 @@ const (
type CallExpr struct {
miniExpr
origNode
X Node
Args Nodes
KeepAlive []*Name // vars to be kept alive until call returns
IsDDD bool
Use CallUse
NoInline bool
X Node
Args Nodes
KeepAlive []*Name // vars to be kept alive until call returns
IsDDD bool
Use CallUse
NoInline bool
PreserveClosure bool // disable directClosureCall for this call
}
func NewCallExpr(pos src.XPos, op Op, fun Node, args []Node) *CallExpr {

View file

@ -37,6 +37,14 @@ func directClosureCall(n *ir.CallExpr) {
return // leave for walkClosure to handle
}
// If wrapGoDefer() in the order phase has flagged this call,
// avoid eliminating the closure even if there is a direct call to
// (the closure is needed to simplify the register ABI). See
// wrapGoDefer for more details.
if n.PreserveClosure {
return
}
// We are going to insert captured variables before input args.
var params []*types.Field
var decls []*ir.Name

View file

@ -14,6 +14,7 @@ import (
"cmd/compile/internal/staticinit"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/objabi"
"cmd/internal/src"
)
@ -731,6 +732,9 @@ func (o *orderState) stmt(n ir.Node) {
t := o.markTemp()
o.init(n.Call)
o.call(n.Call)
if objabi.Experiment.RegabiDefer {
o.wrapGoDefer(n)
}
o.out = append(o.out, n)
o.cleanTemp(t)
@ -1435,3 +1439,247 @@ func (o *orderState) as2ok(n *ir.AssignListStmt) {
o.out = append(o.out, n)
o.stmt(typecheck.Stmt(as))
}
var wrapGoDefer_prgen int
// wrapGoDefer wraps the target of a "go" or "defer" statement with a
// new "function with no arguments" closure. Specifically, it converts
//
// defer f(x, y)
//
// to
//
// x1, y1 := x, y
// defer func() { f(x1, y1) }()
//
// This is primarily to enable a quicker bringup of defers under the
// new register ABI; by doing this conversion, we can simplify the
// code in the runtime that invokes defers on the panic path.
func (o *orderState) wrapGoDefer(n *ir.GoDeferStmt) {
call := n.Call
var callX ir.Node // thing being called
var callArgs []ir.Node // call arguments
// A helper to recreate the call within the closure.
var mkNewCall func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node
// Defer calls come in many shapes and sizes; not all of them
// are ir.CallExpr's. Examine the type to see what we're dealing with.
switch x := call.(type) {
case *ir.CallExpr:
callX = x.X
callArgs = x.Args
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
newcall := ir.NewCallExpr(pos, op, fun, args)
newcall.IsDDD = x.IsDDD
return ir.Node(newcall)
}
case *ir.UnaryExpr: // ex: OCLOSE
callArgs = []ir.Node{x.X}
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
if len(args) != 1 {
panic("internal error, expecting single arg to close")
}
return ir.Node(ir.NewUnaryExpr(pos, op, args[0]))
}
case *ir.BinaryExpr: // ex: OCOPY
callArgs = []ir.Node{x.X, x.Y}
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
if len(args) != 2 {
panic("internal error, expecting two args")
}
return ir.Node(ir.NewBinaryExpr(pos, op, args[0], args[1]))
}
default:
panic("unhandled op")
}
// No need to wrap if called func has no args. However in the case
// of "defer func() { ... }()" we need to protect against the
// possibility of directClosureCall rewriting things so that the
// call does have arguments.
if len(callArgs) == 0 {
if c, ok := call.(*ir.CallExpr); ok && callX != nil && callX.Op() == ir.OCLOSURE {
cloFunc := callX.(*ir.ClosureExpr).Func
cloFunc.SetClosureCalled(false)
c.PreserveClosure = true
}
return
}
if c, ok := call.(*ir.CallExpr); ok {
// To simplify things, turn f(a, b, []T{c, d, e}...) back
// into f(a, b, c, d, e) -- when the final call is run through the
// type checker below, it will rebuild the proper slice literal.
undoVariadic(c)
callX = c.X
callArgs = c.Args
}
// This is set to true if the closure we're generating escapes
// (needs heap allocation).
cloEscapes := func() bool {
if n.Op() == ir.OGO {
// For "go", assume that all closures escape (with an
// exception for the runtime, which doesn't permit
// heap-allocated closures).
return base.Ctxt.Pkgpath != "runtime"
}
// For defer, just use whatever result escape analysis
// has determined for the defer.
return n.Esc() != ir.EscNever
}()
// A helper for making a copy of an argument.
mkArgCopy := func(arg ir.Node) *ir.Name {
argCopy := o.copyExpr(arg)
// The value of 128 below is meant to be consistent with code
// in escape analysis that picks byval/byaddr based on size.
argCopy.SetByval(argCopy.Type().Size() <= 128 || cloEscapes)
return argCopy
}
unsafeArgs := make([]*ir.Name, len(callArgs))
origArgs := callArgs
// Copy the arguments to the function into temps.
pos := n.Pos()
outerfn := ir.CurFunc
var newNames []*ir.Name
for i := range callArgs {
arg := callArgs[i]
var argname *ir.Name
if arg.Op() == ir.OCONVNOP && arg.Type().IsUintptr() && arg.(*ir.ConvExpr).X.Type().IsUnsafePtr() {
// No need for copy here; orderState.call() above has already inserted one.
arg = arg.(*ir.ConvExpr).X
argname = arg.(*ir.Name)
unsafeArgs[i] = argname
} else {
argname = mkArgCopy(arg)
}
newNames = append(newNames, argname)
}
// Deal with cases where the function expression (what we're
// calling) is not a simple function symbol.
var fnExpr *ir.Name
var methSelectorExpr *ir.SelectorExpr
if callX != nil {
switch {
case callX.Op() == ir.ODOTMETH || callX.Op() == ir.ODOTINTER:
// Handle defer of a method call, e.g. "defer v.MyMethod(x, y)"
n := callX.(*ir.SelectorExpr)
n.X = mkArgCopy(n.X)
methSelectorExpr = n
case !(callX.Op() == ir.ONAME && callX.(*ir.Name).Class == ir.PFUNC):
// Deal with "defer returnsafunc()(x, y)" (for
// example) by copying the callee expression.
fnExpr = mkArgCopy(callX)
if callX.Op() == ir.OCLOSURE {
// For "defer func(...)", in addition to copying the
// closure into a temp, mark it as no longer directly
// called.
callX.(*ir.ClosureExpr).Func.SetClosureCalled(false)
}
}
}
// Create a new no-argument function that we'll hand off to defer.
var noFuncArgs []*ir.Field
noargst := ir.NewFuncType(base.Pos, nil, noFuncArgs, nil)
wrapGoDefer_prgen++
wrapname := fmt.Sprintf("%v·dwrap·%d", outerfn, wrapGoDefer_prgen)
sym := types.LocalPkg.Lookup(wrapname)
fn := typecheck.DeclFunc(sym, noargst)
fn.SetIsHiddenClosure(true)
fn.SetWrapper(true)
// helper for capturing reference to a var declared in an outer scope.
capName := func(pos src.XPos, fn *ir.Func, n *ir.Name) *ir.Name {
t := n.Type()
cv := ir.CaptureName(pos, fn, n)
cv.SetType(t)
return typecheck.Expr(cv).(*ir.Name)
}
// Call args (x1, y1) need to be captured as part of the newly
// created closure.
newCallArgs := []ir.Node{}
for i := range newNames {
var arg ir.Node
arg = capName(callArgs[i].Pos(), fn, newNames[i])
if unsafeArgs[i] != nil {
arg = ir.NewConvExpr(arg.Pos(), origArgs[i].Op(), origArgs[i].Type(), arg)
}
newCallArgs = append(newCallArgs, arg)
}
// Also capture the function or method expression (if needed) into
// the closure.
if fnExpr != nil {
callX = capName(callX.Pos(), fn, fnExpr)
}
if methSelectorExpr != nil {
methSelectorExpr.X = capName(callX.Pos(), fn, methSelectorExpr.X.(*ir.Name))
}
ir.FinishCaptureNames(pos, outerfn, fn)
// This flags a builtin as opposed to a regular call.
irregular := (call.Op() != ir.OCALLFUNC &&
call.Op() != ir.OCALLMETH &&
call.Op() != ir.OCALLINTER)
// Construct new function body: f(x1, y1)
op := ir.OCALL
if irregular {
op = call.Op()
}
newcall := mkNewCall(call.Pos(), op, callX, newCallArgs)
// Type-check the result.
if !irregular {
typecheck.Call(newcall.(*ir.CallExpr))
} else {
typecheck.Stmt(newcall)
}
// Finalize body, register function on the main decls list.
fn.Body = []ir.Node{newcall}
typecheck.FinishFuncBody()
typecheck.Func(fn)
typecheck.Target.Decls = append(typecheck.Target.Decls, fn)
// Create closure expr
clo := ir.NewClosureExpr(pos, fn)
fn.OClosure = clo
clo.SetType(fn.Type())
// Set escape properties for closure.
if n.Op() == ir.OGO {
// For "go", assume that the closure is going to escape
// (with an exception for the runtime, which doesn't
// permit heap-allocated closures).
if base.Ctxt.Pkgpath != "runtime" {
clo.SetEsc(ir.EscHeap)
}
} else {
// For defer, just use whatever result escape analysis
// has determined for the defer.
if n.Esc() == ir.EscNever {
clo.SetTransient(true)
clo.SetEsc(ir.EscNone)
}
}
// Create new top level call to closure over argless function.
topcall := ir.NewCallExpr(pos, ir.OCALL, clo, []ir.Node{})
typecheck.Call(topcall)
// Tag the call to insure that directClosureCall doesn't undo our work.
topcall.PreserveClosure = true
fn.SetClosureCalled(false)
// Finally, point the defer statement at the newly generated call.
n.Call = topcall
}

View file

@ -263,12 +263,7 @@ func wrapCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
// Turn f(a, b, []T{c, d, e}...) back into f(a, b, c, d, e).
if !isBuiltinCall && n.IsDDD {
last := len(n.Args) - 1
if va := n.Args[last]; va.Op() == ir.OSLICELIT {
va := va.(*ir.CompLitExpr)
n.Args = append(n.Args[:last], va.List...)
n.IsDDD = false
}
undoVariadic(n)
}
wrapArgs := n.Args
@ -325,3 +320,22 @@ func wrapCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
call = ir.NewCallExpr(base.Pos, ir.OCALL, fn.Nname, wrapArgs)
return walkExpr(typecheck.Stmt(call), init)
}
// undoVariadic turns a call to a variadic function of the form
//
// f(a, b, []T{c, d, e}...)
//
// back into
//
// f(a, b, c, d, e)
//
func undoVariadic(call *ir.CallExpr) {
if call.IsDDD {
last := len(call.Args) - 1
if va := call.Args[last]; va.Op() == ir.OSLICELIT {
va := va.(*ir.CompLitExpr)
call.Args = append(call.Args[:last], va.List...)
call.IsDDD = false
}
}
}

View file

@ -158,8 +158,8 @@ func init() {
Experiment.RegabiWrappers = true
Experiment.RegabiG = true
Experiment.RegabiReflect = true
Experiment.RegabiDefer = true
// Not ready yet:
//Experiment.RegabiDefer = true
//Experiment.RegabiArgs = true
}
// Check regabi dependencies.

View file

@ -147,28 +147,40 @@ func RunSchedLocalQueueStealTest() {
}
}
// Temporary to enable register ABI bringup.
// TODO(register args): convert back to local variables in RunSchedLocalQueueEmptyTest that
// get passed to the "go" stmts there.
var RunSchedLocalQueueEmptyState struct {
done chan bool
ready *uint32
p *p
}
func RunSchedLocalQueueEmptyTest(iters int) {
// Test that runq is not spuriously reported as empty.
// Runq emptiness affects scheduling decisions and spurious emptiness
// can lead to underutilization (both runnable Gs and idle Ps coexist
// for arbitrary long time).
done := make(chan bool, 1)
RunSchedLocalQueueEmptyState.done = done
p := new(p)
RunSchedLocalQueueEmptyState.p = p
gs := make([]g, 2)
ready := new(uint32)
RunSchedLocalQueueEmptyState.ready = ready
for i := 0; i < iters; i++ {
*ready = 0
next0 := (i & 1) == 0
next1 := (i & 2) == 0
runqput(p, &gs[0], next0)
go func() {
for atomic.Xadd(ready, 1); atomic.Load(ready) != 2; {
for atomic.Xadd(RunSchedLocalQueueEmptyState.ready, 1); atomic.Load(RunSchedLocalQueueEmptyState.ready) != 2; {
}
if runqempty(p) {
println("next:", next0, next1)
if runqempty(RunSchedLocalQueueEmptyState.p) {
//println("next:", next0, next1)
throw("queue is empty")
}
done <- true
RunSchedLocalQueueEmptyState.done <- true
}()
for atomic.Xadd(ready, 1); atomic.Load(ready) != 2; {
}

View file

@ -207,17 +207,22 @@ func readgogc() int32 {
return 100
}
// Temporary in order to enable register ABI work.
// TODO(register args): convert back to local chan in gcenabled, passed to "go" stmts.
var gcenable_setup chan int
// gcenable is called after the bulk of the runtime initialization,
// just before we're about to start letting user code run.
// It kicks off the background sweeper goroutine, the background
// scavenger goroutine, and enables GC.
func gcenable() {
// Kick off sweeping and scavenging.
c := make(chan int, 2)
go bgsweep(c)
go bgscavenge(c)
<-c
<-c
gcenable_setup = make(chan int, 2)
go bgsweep()
go bgscavenge()
<-gcenable_setup
<-gcenable_setup
gcenable_setup = nil
memstats.enablegc = true // now that runtime is initialized, GC is okay
}

View file

@ -249,7 +249,7 @@ func scavengeSleep(ns int64) int64 {
// The background scavenger maintains the RSS of the application below
// the line described by the proportional scavenging statistics in
// the mheap struct.
func bgscavenge(c chan int) {
func bgscavenge() {
scavenge.g = getg()
lockInit(&scavenge.lock, lockRankScavenge)
@ -261,7 +261,7 @@ func bgscavenge(c chan int) {
wakeScavenger()
}
c <- 1
gcenable_setup <- 1
goparkunlock(&scavenge.lock, waitReasonGCScavengeWait, traceEvGoBlock, 1)
// Exponentially-weighted moving average of the fraction of time this

View file

@ -153,13 +153,13 @@ func finishsweep_m() {
nextMarkBitArenaEpoch()
}
func bgsweep(c chan int) {
func bgsweep() {
sweep.g = getg()
lockInit(&sweep.lock, lockRankSweep)
lock(&sweep.lock)
sweep.parked = true
c <- 1
gcenable_setup <- 1
goparkunlock(&sweep.lock, waitReasonGCSweepWait, traceEvGoBlock, 1)
for {

View file

@ -106,6 +106,8 @@ var tests = []struct {
{"simple", "run", "", "atexit_sleep_ms=0", `
package main
import "time"
var xptr *int
var donechan chan bool
func main() {
done := make(chan bool)
x := 0
@ -117,32 +119,34 @@ func store(x *int, v int) {
*x = v
}
func startRacer(x *int, done chan bool) {
go racer(x, done)
xptr = x
donechan = done
go racer()
}
func racer(x *int, done chan bool) {
func racer() {
time.Sleep(10*time.Millisecond)
store(x, 42)
done <- true
store(xptr, 42)
donechan <- true
}
`, []string{`==================
WARNING: DATA RACE
Write at 0x[0-9,a-f]+ by goroutine [0-9]:
main\.store\(\)
.+/main\.go:12 \+0x[0-9,a-f]+
.+/main\.go:14 \+0x[0-9,a-f]+
main\.racer\(\)
.+/main\.go:19 \+0x[0-9,a-f]+
.+/main\.go:23 \+0x[0-9,a-f]+
Previous write at 0x[0-9,a-f]+ by main goroutine:
main\.store\(\)
.+/main\.go:12 \+0x[0-9,a-f]+
.+/main\.go:14 \+0x[0-9,a-f]+
main\.main\(\)
.+/main\.go:8 \+0x[0-9,a-f]+
.+/main\.go:10 \+0x[0-9,a-f]+
Goroutine [0-9] \(running\) created at:
main\.startRacer\(\)
.+/main\.go:15 \+0x[0-9,a-f]+
.+/main\.go:19 \+0x[0-9,a-f]+
main\.main\(\)
.+/main\.go:7 \+0x[0-9,a-f]+
.+/main\.go:9 \+0x[0-9,a-f]+
==================
Found 1 data race\(s\)
exit status 66
@ -239,15 +243,15 @@ func main() {
package main
var x int
var c chan int
func main() {
c := make(chan int)
go f(c)
c = make(chan int)
go f()
x = 1
<-c
}
func f(c chan int) {
func f() {
g(c)
}

View file

@ -1,8 +1,9 @@
// errorcheckwithauto -0 -l -live -wb=0 -d=ssa/insert_resched_checks/off
// +build !ppc64,!ppc64le
// +build !ppc64,!ppc64le,!goexperiment.regabi,!goexperiment.regabidefer
// ppc64 needs a better tighten pass to make f18 pass
// rescheduling checks need to be turned off because there are some live variables across the inserted check call
// TODO(register args): temporarily disabled when GOEXPERIMENT=regabi due to additional temporaries live at "go" statements when regabi is in effect.
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style

View file

@ -438,6 +438,16 @@ func (ctxt *context) match(name string) bool {
}
}
exp := os.Getenv("GOEXPERIMENT")
if exp != "" {
experiments := strings.Split(exp, ",")
for _, e := range experiments {
if name == "goexperiment."+e {
return true
}
}
}
if name == ctxt.GOOS || name == ctxt.GOARCH || name == "gc" {
return true
}