Skip to content

Commit 769d4b6

Browse files
committed
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 <[email protected]> Run-TryBot: Than McIntosh <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Cherry Zhang <[email protected]> Reviewed-by: David Chase <[email protected]>
1 parent 4e27aa6 commit 769d4b6

File tree

12 files changed

+344
-41
lines changed

12 files changed

+344
-41
lines changed

src/cmd/compile/internal/ir/expr.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,13 @@ const (
157157
type CallExpr struct {
158158
miniExpr
159159
origNode
160-
X Node
161-
Args Nodes
162-
KeepAlive []*Name // vars to be kept alive until call returns
163-
IsDDD bool
164-
Use CallUse
165-
NoInline bool
160+
X Node
161+
Args Nodes
162+
KeepAlive []*Name // vars to be kept alive until call returns
163+
IsDDD bool
164+
Use CallUse
165+
NoInline bool
166+
PreserveClosure bool // disable directClosureCall for this call
166167
}
167168

168169
func NewCallExpr(pos src.XPos, op Op, fun Node, args []Node) *CallExpr {

src/cmd/compile/internal/walk/closure.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ func directClosureCall(n *ir.CallExpr) {
3737
return // leave for walkClosure to handle
3838
}
3939

40+
// If wrapGoDefer() in the order phase has flagged this call,
41+
// avoid eliminating the closure even if there is a direct call to
42+
// (the closure is needed to simplify the register ABI). See
43+
// wrapGoDefer for more details.
44+
if n.PreserveClosure {
45+
return
46+
}
47+
4048
// We are going to insert captured variables before input args.
4149
var params []*types.Field
4250
var decls []*ir.Name

src/cmd/compile/internal/walk/order.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"cmd/compile/internal/staticinit"
1515
"cmd/compile/internal/typecheck"
1616
"cmd/compile/internal/types"
17+
"cmd/internal/objabi"
1718
"cmd/internal/src"
1819
)
1920

@@ -731,6 +732,9 @@ func (o *orderState) stmt(n ir.Node) {
731732
t := o.markTemp()
732733
o.init(n.Call)
733734
o.call(n.Call)
735+
if objabi.Experiment.RegabiDefer {
736+
o.wrapGoDefer(n)
737+
}
734738
o.out = append(o.out, n)
735739
o.cleanTemp(t)
736740

@@ -1435,3 +1439,247 @@ func (o *orderState) as2ok(n *ir.AssignListStmt) {
14351439
o.out = append(o.out, n)
14361440
o.stmt(typecheck.Stmt(as))
14371441
}
1442+
1443+
var wrapGoDefer_prgen int
1444+
1445+
// wrapGoDefer wraps the target of a "go" or "defer" statement with a
1446+
// new "function with no arguments" closure. Specifically, it converts
1447+
//
1448+
// defer f(x, y)
1449+
//
1450+
// to
1451+
//
1452+
// x1, y1 := x, y
1453+
// defer func() { f(x1, y1) }()
1454+
//
1455+
// This is primarily to enable a quicker bringup of defers under the
1456+
// new register ABI; by doing this conversion, we can simplify the
1457+
// code in the runtime that invokes defers on the panic path.
1458+
func (o *orderState) wrapGoDefer(n *ir.GoDeferStmt) {
1459+
call := n.Call
1460+
1461+
var callX ir.Node // thing being called
1462+
var callArgs []ir.Node // call arguments
1463+
1464+
// A helper to recreate the call within the closure.
1465+
var mkNewCall func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node
1466+
1467+
// Defer calls come in many shapes and sizes; not all of them
1468+
// are ir.CallExpr's. Examine the type to see what we're dealing with.
1469+
switch x := call.(type) {
1470+
case *ir.CallExpr:
1471+
callX = x.X
1472+
callArgs = x.Args
1473+
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
1474+
newcall := ir.NewCallExpr(pos, op, fun, args)
1475+
newcall.IsDDD = x.IsDDD
1476+
return ir.Node(newcall)
1477+
}
1478+
case *ir.UnaryExpr: // ex: OCLOSE
1479+
callArgs = []ir.Node{x.X}
1480+
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
1481+
if len(args) != 1 {
1482+
panic("internal error, expecting single arg to close")
1483+
}
1484+
return ir.Node(ir.NewUnaryExpr(pos, op, args[0]))
1485+
}
1486+
case *ir.BinaryExpr: // ex: OCOPY
1487+
callArgs = []ir.Node{x.X, x.Y}
1488+
mkNewCall = func(pos src.XPos, op ir.Op, fun ir.Node, args []ir.Node) ir.Node {
1489+
if len(args) != 2 {
1490+
panic("internal error, expecting two args")
1491+
}
1492+
return ir.Node(ir.NewBinaryExpr(pos, op, args[0], args[1]))
1493+
}
1494+
default:
1495+
panic("unhandled op")
1496+
}
1497+
1498+
// No need to wrap if called func has no args. However in the case
1499+
// of "defer func() { ... }()" we need to protect against the
1500+
// possibility of directClosureCall rewriting things so that the
1501+
// call does have arguments.
1502+
if len(callArgs) == 0 {
1503+
if c, ok := call.(*ir.CallExpr); ok && callX != nil && callX.Op() == ir.OCLOSURE {
1504+
cloFunc := callX.(*ir.ClosureExpr).Func
1505+
cloFunc.SetClosureCalled(false)
1506+
c.PreserveClosure = true
1507+
}
1508+
return
1509+
}
1510+
1511+
if c, ok := call.(*ir.CallExpr); ok {
1512+
// To simplify things, turn f(a, b, []T{c, d, e}...) back
1513+
// into f(a, b, c, d, e) -- when the final call is run through the
1514+
// type checker below, it will rebuild the proper slice literal.
1515+
undoVariadic(c)
1516+
callX = c.X
1517+
callArgs = c.Args
1518+
}
1519+
1520+
// This is set to true if the closure we're generating escapes
1521+
// (needs heap allocation).
1522+
cloEscapes := func() bool {
1523+
if n.Op() == ir.OGO {
1524+
// For "go", assume that all closures escape (with an
1525+
// exception for the runtime, which doesn't permit
1526+
// heap-allocated closures).
1527+
return base.Ctxt.Pkgpath != "runtime"
1528+
}
1529+
// For defer, just use whatever result escape analysis
1530+
// has determined for the defer.
1531+
return n.Esc() != ir.EscNever
1532+
}()
1533+
1534+
// A helper for making a copy of an argument.
1535+
mkArgCopy := func(arg ir.Node) *ir.Name {
1536+
argCopy := o.copyExpr(arg)
1537+
// The value of 128 below is meant to be consistent with code
1538+
// in escape analysis that picks byval/byaddr based on size.
1539+
argCopy.SetByval(argCopy.Type().Size() <= 128 || cloEscapes)
1540+
return argCopy
1541+
}
1542+
1543+
unsafeArgs := make([]*ir.Name, len(callArgs))
1544+
origArgs := callArgs
1545+
1546+
// Copy the arguments to the function into temps.
1547+
pos := n.Pos()
1548+
outerfn := ir.CurFunc
1549+
var newNames []*ir.Name
1550+
for i := range callArgs {
1551+
arg := callArgs[i]
1552+
var argname *ir.Name
1553+
if arg.Op() == ir.OCONVNOP && arg.Type().IsUintptr() && arg.(*ir.ConvExpr).X.Type().IsUnsafePtr() {
1554+
// No need for copy here; orderState.call() above has already inserted one.
1555+
arg = arg.(*ir.ConvExpr).X
1556+
argname = arg.(*ir.Name)
1557+
unsafeArgs[i] = argname
1558+
} else {
1559+
argname = mkArgCopy(arg)
1560+
}
1561+
newNames = append(newNames, argname)
1562+
}
1563+
1564+
// Deal with cases where the function expression (what we're
1565+
// calling) is not a simple function symbol.
1566+
var fnExpr *ir.Name
1567+
var methSelectorExpr *ir.SelectorExpr
1568+
if callX != nil {
1569+
switch {
1570+
case callX.Op() == ir.ODOTMETH || callX.Op() == ir.ODOTINTER:
1571+
// Handle defer of a method call, e.g. "defer v.MyMethod(x, y)"
1572+
n := callX.(*ir.SelectorExpr)
1573+
n.X = mkArgCopy(n.X)
1574+
methSelectorExpr = n
1575+
case !(callX.Op() == ir.ONAME && callX.(*ir.Name).Class == ir.PFUNC):
1576+
// Deal with "defer returnsafunc()(x, y)" (for
1577+
// example) by copying the callee expression.
1578+
fnExpr = mkArgCopy(callX)
1579+
if callX.Op() == ir.OCLOSURE {
1580+
// For "defer func(...)", in addition to copying the
1581+
// closure into a temp, mark it as no longer directly
1582+
// called.
1583+
callX.(*ir.ClosureExpr).Func.SetClosureCalled(false)
1584+
}
1585+
}
1586+
}
1587+
1588+
// Create a new no-argument function that we'll hand off to defer.
1589+
var noFuncArgs []*ir.Field
1590+
noargst := ir.NewFuncType(base.Pos, nil, noFuncArgs, nil)
1591+
wrapGoDefer_prgen++
1592+
wrapname := fmt.Sprintf("%v·dwrap·%d", outerfn, wrapGoDefer_prgen)
1593+
sym := types.LocalPkg.Lookup(wrapname)
1594+
fn := typecheck.DeclFunc(sym, noargst)
1595+
fn.SetIsHiddenClosure(true)
1596+
fn.SetWrapper(true)
1597+
1598+
// helper for capturing reference to a var declared in an outer scope.
1599+
capName := func(pos src.XPos, fn *ir.Func, n *ir.Name) *ir.Name {
1600+
t := n.Type()
1601+
cv := ir.CaptureName(pos, fn, n)
1602+
cv.SetType(t)
1603+
return typecheck.Expr(cv).(*ir.Name)
1604+
}
1605+
1606+
// Call args (x1, y1) need to be captured as part of the newly
1607+
// created closure.
1608+
newCallArgs := []ir.Node{}
1609+
for i := range newNames {
1610+
var arg ir.Node
1611+
arg = capName(callArgs[i].Pos(), fn, newNames[i])
1612+
if unsafeArgs[i] != nil {
1613+
arg = ir.NewConvExpr(arg.Pos(), origArgs[i].Op(), origArgs[i].Type(), arg)
1614+
}
1615+
newCallArgs = append(newCallArgs, arg)
1616+
}
1617+
// Also capture the function or method expression (if needed) into
1618+
// the closure.
1619+
if fnExpr != nil {
1620+
callX = capName(callX.Pos(), fn, fnExpr)
1621+
}
1622+
if methSelectorExpr != nil {
1623+
methSelectorExpr.X = capName(callX.Pos(), fn, methSelectorExpr.X.(*ir.Name))
1624+
}
1625+
ir.FinishCaptureNames(pos, outerfn, fn)
1626+
1627+
// This flags a builtin as opposed to a regular call.
1628+
irregular := (call.Op() != ir.OCALLFUNC &&
1629+
call.Op() != ir.OCALLMETH &&
1630+
call.Op() != ir.OCALLINTER)
1631+
1632+
// Construct new function body: f(x1, y1)
1633+
op := ir.OCALL
1634+
if irregular {
1635+
op = call.Op()
1636+
}
1637+
newcall := mkNewCall(call.Pos(), op, callX, newCallArgs)
1638+
1639+
// Type-check the result.
1640+
if !irregular {
1641+
typecheck.Call(newcall.(*ir.CallExpr))
1642+
} else {
1643+
typecheck.Stmt(newcall)
1644+
}
1645+
1646+
// Finalize body, register function on the main decls list.
1647+
fn.Body = []ir.Node{newcall}
1648+
typecheck.FinishFuncBody()
1649+
typecheck.Func(fn)
1650+
typecheck.Target.Decls = append(typecheck.Target.Decls, fn)
1651+
1652+
// Create closure expr
1653+
clo := ir.NewClosureExpr(pos, fn)
1654+
fn.OClosure = clo
1655+
clo.SetType(fn.Type())
1656+
1657+
// Set escape properties for closure.
1658+
if n.Op() == ir.OGO {
1659+
// For "go", assume that the closure is going to escape
1660+
// (with an exception for the runtime, which doesn't
1661+
// permit heap-allocated closures).
1662+
if base.Ctxt.Pkgpath != "runtime" {
1663+
clo.SetEsc(ir.EscHeap)
1664+
}
1665+
} else {
1666+
// For defer, just use whatever result escape analysis
1667+
// has determined for the defer.
1668+
if n.Esc() == ir.EscNever {
1669+
clo.SetTransient(true)
1670+
clo.SetEsc(ir.EscNone)
1671+
}
1672+
}
1673+
1674+
// Create new top level call to closure over argless function.
1675+
topcall := ir.NewCallExpr(pos, ir.OCALL, clo, []ir.Node{})
1676+
typecheck.Call(topcall)
1677+
1678+
// Tag the call to insure that directClosureCall doesn't undo our work.
1679+
topcall.PreserveClosure = true
1680+
1681+
fn.SetClosureCalled(false)
1682+
1683+
// Finally, point the defer statement at the newly generated call.
1684+
n.Call = topcall
1685+
}

src/cmd/compile/internal/walk/stmt.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,7 @@ func wrapCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
263263

264264
// Turn f(a, b, []T{c, d, e}...) back into f(a, b, c, d, e).
265265
if !isBuiltinCall && n.IsDDD {
266-
last := len(n.Args) - 1
267-
if va := n.Args[last]; va.Op() == ir.OSLICELIT {
268-
va := va.(*ir.CompLitExpr)
269-
n.Args = append(n.Args[:last], va.List...)
270-
n.IsDDD = false
271-
}
266+
undoVariadic(n)
272267
}
273268

274269
wrapArgs := n.Args
@@ -325,3 +320,22 @@ func wrapCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
325320
call = ir.NewCallExpr(base.Pos, ir.OCALL, fn.Nname, wrapArgs)
326321
return walkExpr(typecheck.Stmt(call), init)
327322
}
323+
324+
// undoVariadic turns a call to a variadic function of the form
325+
//
326+
// f(a, b, []T{c, d, e}...)
327+
//
328+
// back into
329+
//
330+
// f(a, b, c, d, e)
331+
//
332+
func undoVariadic(call *ir.CallExpr) {
333+
if call.IsDDD {
334+
last := len(call.Args) - 1
335+
if va := call.Args[last]; va.Op() == ir.OSLICELIT {
336+
va := va.(*ir.CompLitExpr)
337+
call.Args = append(call.Args[:last], va.List...)
338+
call.IsDDD = false
339+
}
340+
}
341+
}

src/cmd/internal/objabi/util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ func init() {
158158
Experiment.RegabiWrappers = true
159159
Experiment.RegabiG = true
160160
Experiment.RegabiReflect = true
161+
Experiment.RegabiDefer = true
161162
// Not ready yet:
162-
//Experiment.RegabiDefer = true
163163
//Experiment.RegabiArgs = true
164164
}
165165
// Check regabi dependencies.

src/runtime/export_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,28 +147,40 @@ func RunSchedLocalQueueStealTest() {
147147
}
148148
}
149149

150+
// Temporary to enable register ABI bringup.
151+
// TODO(register args): convert back to local variables in RunSchedLocalQueueEmptyTest that
152+
// get passed to the "go" stmts there.
153+
var RunSchedLocalQueueEmptyState struct {
154+
done chan bool
155+
ready *uint32
156+
p *p
157+
}
158+
150159
func RunSchedLocalQueueEmptyTest(iters int) {
151160
// Test that runq is not spuriously reported as empty.
152161
// Runq emptiness affects scheduling decisions and spurious emptiness
153162
// can lead to underutilization (both runnable Gs and idle Ps coexist
154163
// for arbitrary long time).
155164
done := make(chan bool, 1)
165+
RunSchedLocalQueueEmptyState.done = done
156166
p := new(p)
167+
RunSchedLocalQueueEmptyState.p = p
157168
gs := make([]g, 2)
158169
ready := new(uint32)
170+
RunSchedLocalQueueEmptyState.ready = ready
159171
for i := 0; i < iters; i++ {
160172
*ready = 0
161173
next0 := (i & 1) == 0
162174
next1 := (i & 2) == 0
163175
runqput(p, &gs[0], next0)
164176
go func() {
165-
for atomic.Xadd(ready, 1); atomic.Load(ready) != 2; {
177+
for atomic.Xadd(RunSchedLocalQueueEmptyState.ready, 1); atomic.Load(RunSchedLocalQueueEmptyState.ready) != 2; {
166178
}
167-
if runqempty(p) {
168-
println("next:", next0, next1)
179+
if runqempty(RunSchedLocalQueueEmptyState.p) {
180+
//println("next:", next0, next1)
169181
throw("queue is empty")
170182
}
171-
done <- true
183+
RunSchedLocalQueueEmptyState.done <- true
172184
}()
173185
for atomic.Xadd(ready, 1); atomic.Load(ready) != 2; {
174186
}

0 commit comments

Comments
 (0)