| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package bloop | |
| // This file contains support routines for keeping | |
| // statements alive | |
| // in such loops (example): | |
| // | |
| // for b.Loop() { | |
| // var a, b int | |
| // a = 5 | |
| // b = 6 | |
| // f(a, b) | |
| // } | |
| // | |
| // The results of a, b and f(a, b) will be kept alive. | |
| // | |
| // Formally, the lhs (if they are [ir.Name]-s) of | |
| // [ir.AssignStmt], [ir.AssignListStmt], | |
| // [ir.AssignOpStmt], and the results of [ir.CallExpr] | |
| // or its args if it doesn't return a value will be kept | |
| // alive. | |
| // | |
| // The keep alive logic is implemented with as wrapping a | |
| // runtime.KeepAlive around the Name. | |
| // | |
| // TODO: currently this is implemented with KeepAlive | |
| // because it will prevent DSE and DCE which is probably | |
| // what we want right now. And KeepAlive takes an ssa | |
| // value instead of a symbol, which is easier to manage. | |
| // But since KeepAlive's context was mainly in the runtime | |
| // and GC, should we implement a new intrinsic that lowers | |
| // to OpVarLive? Peeling out the symbols is a bit tricky | |
| // and also VarLive seems to assume that there exists a | |
| // VarDef on the same symbol that dominates it. | |
| import ( | |
| "cmd/compile/internal/base" | |
| "cmd/compile/internal/ir" | |
| "cmd/compile/internal/typecheck" | |
| "cmd/compile/internal/types" | |
| "cmd/internal/src" | |
| ) | |
| // getNameFromNode tries to iteratively peel down the node to | |
| // get the name. | |
| func getNameFromNode(n ir.Node) *ir.Name { | |
| // Tries to iteratively peel down the node to get the names. | |
| for n != nil { | |
| switch n.Op() { | |
| case ir.ONAME: | |
| // Found the name, stop the loop. | |
| return n.(*ir.Name) | |
| case ir.OSLICE, ir.OSLICE3: | |
| n = n.(*ir.SliceExpr).X | |
| case ir.ODOT: | |
| n = n.(*ir.SelectorExpr).X | |
| case ir.OCONV, ir.OCONVIFACE, ir.OCONVNOP: | |
| n = n.(*ir.ConvExpr).X | |
| case ir.OADDR: | |
| n = n.(*ir.AddrExpr).X | |
| case ir.ODOTPTR: | |
| n = n.(*ir.SelectorExpr).X | |
| case ir.OINDEX, ir.OINDEXMAP: | |
| n = n.(*ir.IndexExpr).X | |
| default: | |
| n = nil | |
| } | |
| } | |
| return nil | |
| } | |
| // isIgnoredName reports whether name is a compiler-synthesized | |
| // bookkeeping variable introduced by the rangefunc rewrite (see | |
| // cmd/compile/internal/rangefunc), e.g. "#state1", "#yield1", "#next". | |
| // These variables are not part of any benchmark, and normally should | |
| // be optimized away whenever possible. | |
| func isIgnoredName(name *ir.Name) bool { | |
| if name == nil || name.Sym() == nil { | |
| return false | |
| } | |
| n := name.Sym().Name | |
| if n == "" { | |
| return false | |
| } | |
| return n[0] == '#' | |
| } | |
| // isIgnoredCall reports whether n is a call to | |
| // runtime.panicrangestate or runtime.KeepAlive. | |
| // These calls do not need their inputs preserved; | |
| // Panicrangestate only receives bookkeeping values, | |
| // and KeepAlive already preserves its value (i.e., | |
| // not ignoring it, would only result in a second | |
| // KeepAlive). | |
| func isIgnoredCall(n *ir.CallExpr) bool { | |
| name, ok := n.Fun.(*ir.Name) | |
| if !ok || name.Class != ir.PFUNC || name.Sym() == nil { | |
| return false | |
| } | |
| sym := name.Sym() | |
| return sym.Pkg == ir.Pkgs.Runtime && | |
| (sym.Name == "panicrangestate" || sym.Name == "KeepAlive") | |
| } | |
| // getAddressableNameFromNode is like getNameFromNode | |
| // but returns nil if the node is not addressable, | |
| // and true if the node is one of the ignored bookkeeping names. | |
| func getAddressableNameFromNode(n ir.Node) (name *ir.Name, ignored bool) { | |
| name = getNameFromNode(n) | |
| if name == nil { | |
| return | |
| } | |
| ignored = isIgnoredName(name) | |
| if !ir.IsAddressable(name) { | |
| name = nil | |
| } | |
| return | |
| } | |
| // getKeepAliveNodes analyzes an IR node and returns a list of nodes that must be kept alive. | |
| func getKeepAliveNodes(pos src.XPos, n ir.Node) ir.Nodes { | |
| name, ignored := getAddressableNameFromNode(n) | |
| if ignored { | |
| return nil | |
| } | |
| if name != nil { | |
| debugName(name, pos) | |
| return ir.Nodes{name} | |
| } else if deref, ok := n.(*ir.StarExpr); ok && deref != nil { | |
| if base.Flag.LowerM > 1 { | |
| base.WarnfAt(pos, "dereference will be kept alive") | |
| } | |
| return ir.Nodes{deref} | |
| } else if base.Flag.LowerM > 1 { | |
| base.WarnfAt(pos, "expr is unknown to bloop pass") | |
| } | |
| return nil | |
| } | |
| // keepAliveAt returns a statement that is either curNode, or a | |
| // block containing curNode followed by a call to runtime.KeepAlive for each | |
| // node in ns. These calls ensure that nodes in ns will be live until | |
| // after curNode's execution. | |
| func keepAliveAt(curfunc *ir.Func, ns ir.Nodes, curNode ir.Node) ir.Node { | |
| if len(ns) == 0 { | |
| return curNode | |
| } | |
| pos := curNode.Pos() | |
| calls := ir.Nodes{curNode} | |
| for _, n := range ns { | |
| if n == nil || n.Sym() == nil || n.Sym().IsBlank() { | |
| continue | |
| } | |
| if !ir.IsAddressable(n) { | |
| base.FatalfAt(n.Pos(), "keepAliveAt: node %v is not addressable", n) | |
| } | |
| arg := ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TUNSAFEPTR], typecheck.NodAddr(curfunc, n)) | |
| callExpr := typecheck.Call(curfunc, pos, typecheck.LookupRuntime("KeepAlive"), ir.Nodes{arg}, false).(*ir.CallExpr) | |
| callExpr.IsCompilerVarLive = true | |
| callExpr.NoInline = true | |
| calls = append(calls, callExpr) | |
| } | |
| return ir.NewBlockStmt(pos, calls) | |
| } | |
| func debugName(name *ir.Name, pos src.XPos) { | |
| if base.Flag.LowerM > 1 { | |
| if name.Linksym() != nil { | |
| base.WarnfAt(pos, "%s will be kept alive", name.Linksym().Name) | |
| } else { | |
| base.WarnfAt(pos, "expr will be kept alive") | |
| } | |
| } | |
| } | |
| // preserveCallResults assigns the results of a call statement to temporary variables to ensure they remain alive. | |
| func preserveCallResults(curFn *ir.Func, call *ir.CallExpr) ir.Node { | |
| var ns ir.Nodes | |
| lhs := make(ir.Nodes, call.Fun.Type().NumResults()) | |
| for i, res := range call.Fun.Type().Results() { | |
| tmp := typecheck.TempAt(call.Pos(), curFn, res.Type) | |
| lhs[i] = tmp | |
| ns = append(ns, tmp) | |
| } | |
| if base.Flag.LowerM > 1 { | |
| plural := "" | |
| if call.Fun.Type().NumResults() > 1 { | |
| plural = "s" | |
| } | |
| base.WarnfAt(call.Pos(), "function result%s will be kept alive", plural) | |
| } | |
| assign := typecheck.AssignExpr(curFn, ir.NewAssignListStmt(call.Pos(), ir.OAS2, lhs, ir.Nodes{call})).(*ir.AssignListStmt) | |
| assign.Def = true | |
| for _, tmp := range lhs { | |
| // Place temp declarations in the loop body to help escape analysis. | |
| assign.PtrInit().Append(typecheck.Stmt(curFn, ir.NewDecl(assign.Pos(), ir.ODCL, tmp.(*ir.Name)))) | |
| } | |
| return keepAliveAt(curFn, ns, assign) | |
| } | |
| // preserveCallArgs ensures the arguments of a call statement are kept alive by transforming them into temporaries if necessary. | |
| func preserveCallArgs(curFn *ir.Func, call *ir.CallExpr) ir.Node { | |
| var argTmps ir.Nodes | |
| var names ir.Nodes | |
| preserveTmp := func(pos src.XPos, n ir.Node) ir.Node { | |
| tmp := typecheck.TempAt(pos, curFn, n.Type()) | |
| assign := ir.NewAssignStmt(pos, tmp, n) | |
| assign.Def = true | |
| // Place temp declarations in the loop body to help escape analysis. | |
| assign.PtrInit().Append(typecheck.Stmt(curFn, ir.NewDecl(assign.Pos(), ir.ODCL, tmp))) | |
| argTmps = append(argTmps, typecheck.AssignExpr(curFn, assign)) | |
| names = append(names, tmp) | |
| if base.Flag.LowerM > 1 { | |
| base.WarnfAt(call.Pos(), "function arg will be kept alive") | |
| } | |
| return tmp | |
| } | |
| for i, a := range call.Args { | |
| name, ignored := getAddressableNameFromNode(a) | |
| if ignored { | |
| continue | |
| } | |
| if name != nil { | |
| // If they are name, keep them alive directly. | |
| debugName(name, call.Pos()) | |
| names = append(names, name) | |
| } else if a.Op() == ir.OSLICELIT { | |
| // variadic args are encoded as slice literal. | |
| s := a.(*ir.CompLitExpr) | |
| var ns ir.Nodes | |
| for i, elem := range s.List { | |
| name, ignored := getAddressableNameFromNode(elem) | |
| if ignored { | |
| continue | |
| } | |
| if name != nil { | |
| debugName(name, call.Pos()) | |
| ns = append(ns, name) | |
| } else { | |
| // We need a temporary to save this arg. | |
| s.List[i] = preserveTmp(elem.Pos(), elem) | |
| } | |
| } | |
| names = append(names, ns...) | |
| } else { | |
| // expressions, we need to assign them to temps and change the original arg to reference them. | |
| call.Args[i] = preserveTmp(call.Pos(), a) | |
| } | |
| } | |
| if len(argTmps) > 0 { | |
| argTmps = append(argTmps, call) | |
| return keepAliveAt(curFn, names, ir.NewBlockStmt(call.Pos(), argTmps)) | |
| } | |
| return keepAliveAt(curFn, names, call) | |
| } | |
| // preserveStmt transforms stmt so that any names defined/assigned within it | |
| // are used after stmt's execution, preventing their dead code elimination | |
| // and dead store elimination. The return value is the transformed statement. | |
| func preserveStmt(curFn *ir.Func, stmt ir.Node) ir.Node { | |
| switch n := stmt.(type) { | |
| case *ir.AssignStmt: | |
| // If the left hand side is blank, we need to assign it to a temp | |
| // so that it can be kept alive. | |
| if ir.IsBlank(n.X) { | |
| tmp := typecheck.TempAt(n.Pos(), curFn, n.Y.Type()) | |
| n.X = tmp | |
| n.Def = true | |
| n.PtrInit().Append(typecheck.Stmt(curFn, ir.NewDecl(n.Pos(), ir.ODCL, tmp))) | |
| stmt = typecheck.AssignExpr(curFn, n) | |
| n = stmt.(*ir.AssignStmt) | |
| } | |
| return keepAliveAt(curFn, getKeepAliveNodes(n.Pos(), n.X), n) | |
| case *ir.AssignListStmt: | |
| var ns ir.Nodes | |
| hasBlank := false | |
| for i, lhs := range n.Lhs { | |
| if ir.IsBlank(lhs) { | |
| // If the left hand side has blanks, we need to assign them to temps | |
| // so that they can be kept alive. | |
| var typ *types.Type | |
| // AssignListStmt can have tuple or a list of expressions on the right hand side. | |
| if len(n.Rhs) == 1 && n.Rhs[0].Type() != nil && | |
| n.Rhs[0].Type().IsTuple() && | |
| len(n.Lhs) == n.Rhs[0].Type().NumFields() { | |
| typ = n.Rhs[0].Type().Field(i).Type | |
| } else if len(n.Rhs) == len(n.Lhs) { | |
| typ = n.Rhs[i].Type() | |
| } else { | |
| // Unrecognized shapes, skip? | |
| base.WarnfAt(n.Pos(), "unrecognized shape for assign list stmt for blank assignment") | |
| continue | |
| } | |
| tmp := typecheck.TempAt(n.Pos(), curFn, typ) | |
| n.Lhs[i] = tmp | |
| n.PtrInit().Append(typecheck.Stmt(curFn, ir.NewDecl(n.Pos(), ir.ODCL, tmp))) | |
| hasBlank = true | |
| } | |
| ns = append(ns, getKeepAliveNodes(n.Pos(), n.Lhs[i])...) | |
| } | |
| if hasBlank { | |
| // blank nodes are rewritten to temps, we need to typecheck the node again. | |
| n.Def = true | |
| stmt = typecheck.AssignExpr(curFn, n) | |
| n = stmt.(*ir.AssignListStmt) | |
| } | |
| return keepAliveAt(curFn, ns, n) | |
| case *ir.AssignOpStmt: | |
| return keepAliveAt(curFn, getKeepAliveNodes(n.Pos(), n.X), n) | |
| case *ir.CallExpr: | |
| if isIgnoredCall(n) { | |
| // KeepAlive is self-preserving, panicRangeState is for rangefunc bookkeeping. | |
| // These do-not-need/should-not-be preserved. | |
| return stmt | |
| } | |
| // The function's results are not assigned, preserve them. | |
| if n.Fun != nil && n.Fun.Type() != nil && n.Fun.Type().NumResults() != 0 { | |
| return preserveCallResults(curFn, n) | |
| } | |
| // This function doesn't return anything, keep its args alive. | |
| return preserveCallArgs(curFn, n) | |
| } | |
| return stmt | |
| } | |
| func preserveStmts(curFn *ir.Func, list ir.Nodes) { | |
| for i := range list { | |
| list[i] = preserveStmt(curFn, list[i]) | |
| } | |
| } | |
| // isTestingBLoop returns true if it matches the node as a | |
| // testing.(*B).Loop. See issue #61515. | |
| func isTestingBLoop(t ir.Node) bool { | |
| if t.Op() != ir.OFOR { | |
| return false | |
| } | |
| nFor, ok := t.(*ir.ForStmt) | |
| if !ok || nFor.Cond == nil || nFor.Cond.Op() != ir.OCALLFUNC { | |
| return false | |
| } | |
| n, ok := nFor.Cond.(*ir.CallExpr) | |
| if !ok || n.Fun == nil || n.Fun.Op() != ir.OMETHEXPR { | |
| return false | |
| } | |
| name := ir.MethodExprName(n.Fun) | |
| if name == nil { | |
| return false | |
| } | |
| if fSym := name.Sym(); fSym != nil && name.Class == ir.PFUNC && fSym.Pkg != nil && | |
| fSym.Name == "(*B).Loop" && fSym.Pkg.Path == "testing" { | |
| // Attempting to match a function call to testing.(*B).Loop | |
| return true | |
| } | |
| return false | |
| } | |
| type editor struct { | |
| inBloop bool | |
| curFn *ir.Func | |
| } | |
| func (e editor) edit(n ir.Node) ir.Node { | |
| e.inBloop = isTestingBLoop(n) || e.inBloop | |
| // It's in bloop, mark the stmts with bodies. | |
| ir.EditChildren(n, e.edit) | |
| if e.inBloop { | |
| switch n := n.(type) { | |
| case *ir.ForStmt: | |
| preserveStmts(e.curFn, n.Body) | |
| case *ir.IfStmt: | |
| preserveStmts(e.curFn, n.Body) | |
| preserveStmts(e.curFn, n.Else) | |
| case *ir.BlockStmt: | |
| preserveStmts(e.curFn, n.List) | |
| case *ir.CaseClause: | |
| preserveStmts(e.curFn, n.Body) | |
| case *ir.CommClause: | |
| preserveStmts(e.curFn, n.Body) | |
| case *ir.RangeStmt: | |
| preserveStmts(e.curFn, n.Body) | |
| case *ir.ClosureExpr: | |
| // Closures (including the yield-function closures | |
| // synthesized by the rangefunc rewrite for range-over-func | |
| // loops, see cmd/compile/internal/rangefunc) are compiled | |
| // as their own separate *ir.Func, appended independently to | |
| // pkg.Funcs. ClosureExpr's generic editChildren does not | |
| // descend into Func.Body, so without this case, statements | |
| // inside a closure called from a b.Loop() body would never | |
| // be visited by this walk. | |
| // | |
| // Recurse in explicitly, scoping new temps to the | |
| // closure's own Func. | |
| if n.Func != nil { | |
| ce := editor{inBloop: true, curFn: n.Func} | |
| ir.EditChildren(n.Func, ce.edit) | |
| preserveStmts(n.Func, n.Func.Body) | |
| } | |
| } | |
| } | |
| return n | |
| } | |
| // Walk performs a walk on all functions in the package | |
| // if it imports testing and wrap the results of all qualified | |
| // statements in a runtime.KeepAlive intrinsic call. See package | |
| // doc for more details. | |
| // | |
| // for b.Loop() {...} | |
| // | |
| // loop's body. | |
| func Walk(pkg *ir.Package) { | |
| hasTesting := false | |
| for _, i := range pkg.Imports { | |
| if i.Path == "testing" { | |
| hasTesting = true | |
| break | |
| } | |
| } | |
| if !hasTesting { | |
| return | |
| } | |
| for _, fn := range pkg.Funcs { | |
| e := editor{false, fn} | |
| ir.EditChildren(fn, e.edit) | |
| if ir.MatchAstDump(fn, "bloop") { | |
| ir.AstDump(fn, "bloop, "+ir.FuncName(fn)) | |
| } | |
| } | |
| } |