summaryrefslogtreecommitdiffstats
path: root/src/cmd/compile/internal/escape/escape.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:25:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:25:22 +0000
commitf6ad4dcef54c5ce997a4bad5a6d86de229015700 (patch)
tree7cfa4e31ace5c2bd95c72b154d15af494b2bcbef /src/cmd/compile/internal/escape/escape.go
parentInitial commit. (diff)
downloadgolang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.tar.xz
golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.zip
Adding upstream version 1.22.1.upstream/1.22.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/compile/internal/escape/escape.go')
-rw-r--r--src/cmd/compile/internal/escape/escape.go509
1 files changed, 509 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/escape/escape.go b/src/cmd/compile/internal/escape/escape.go
new file mode 100644
index 0000000..7df367c
--- /dev/null
+++ b/src/cmd/compile/internal/escape/escape.go
@@ -0,0 +1,509 @@
+// Copyright 2018 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 escape
+
+import (
+ "fmt"
+
+ "cmd/compile/internal/base"
+ "cmd/compile/internal/ir"
+ "cmd/compile/internal/logopt"
+ "cmd/compile/internal/typecheck"
+ "cmd/compile/internal/types"
+ "cmd/internal/src"
+)
+
+// Escape analysis.
+//
+// Here we analyze functions to determine which Go variables
+// (including implicit allocations such as calls to "new" or "make",
+// composite literals, etc.) can be allocated on the stack. The two
+// key invariants we have to ensure are: (1) pointers to stack objects
+// cannot be stored in the heap, and (2) pointers to a stack object
+// cannot outlive that object (e.g., because the declaring function
+// returned and destroyed the object's stack frame, or its space is
+// reused across loop iterations for logically distinct variables).
+//
+// We implement this with a static data-flow analysis of the AST.
+// First, we construct a directed weighted graph where vertices
+// (termed "locations") represent variables allocated by statements
+// and expressions, and edges represent assignments between variables
+// (with weights representing addressing/dereference counts).
+//
+// Next we walk the graph looking for assignment paths that might
+// violate the invariants stated above. If a variable v's address is
+// stored in the heap or elsewhere that may outlive it, then v is
+// marked as requiring heap allocation.
+//
+// To support interprocedural analysis, we also record data-flow from
+// each function's parameters to the heap and to its result
+// parameters. This information is summarized as "parameter tags",
+// which are used at static call sites to improve escape analysis of
+// function arguments.
+
+// Constructing the location graph.
+//
+// Every allocating statement (e.g., variable declaration) or
+// expression (e.g., "new" or "make") is first mapped to a unique
+// "location."
+//
+// We also model every Go assignment as a directed edges between
+// locations. The number of dereference operations minus the number of
+// addressing operations is recorded as the edge's weight (termed
+// "derefs"). For example:
+//
+// p = &q // -1
+// p = q // 0
+// p = *q // 1
+// p = **q // 2
+//
+// p = **&**&q // 2
+//
+// Note that the & operator can only be applied to addressable
+// expressions, and the expression &x itself is not addressable, so
+// derefs cannot go below -1.
+//
+// Every Go language construct is lowered into this representation,
+// generally without sensitivity to flow, path, or context; and
+// without distinguishing elements within a compound variable. For
+// example:
+//
+// var x struct { f, g *int }
+// var u []*int
+//
+// x.f = u[0]
+//
+// is modeled simply as
+//
+// x = *u
+//
+// That is, we don't distinguish x.f from x.g, or u[0] from u[1],
+// u[2], etc. However, we do record the implicit dereference involved
+// in indexing a slice.
+
+// A batch holds escape analysis state that's shared across an entire
+// batch of functions being analyzed at once.
+type batch struct {
+ allLocs []*location
+ closures []closure
+
+ heapLoc location
+ mutatorLoc location
+ calleeLoc location
+ blankLoc location
+}
+
+// A closure holds a closure expression and its spill hole (i.e.,
+// where the hole representing storing into its closure record).
+type closure struct {
+ k hole
+ clo *ir.ClosureExpr
+}
+
+// An escape holds state specific to a single function being analyzed
+// within a batch.
+type escape struct {
+ *batch
+
+ curfn *ir.Func // function being analyzed
+
+ labels map[*types.Sym]labelState // known labels
+
+ // loopDepth counts the current loop nesting depth within
+ // curfn. It increments within each "for" loop and at each
+ // label with a corresponding backwards "goto" (i.e.,
+ // unstructured loop).
+ loopDepth int
+}
+
+func Funcs(all []*ir.Func) {
+ ir.VisitFuncsBottomUp(all, Batch)
+}
+
+// Batch performs escape analysis on a minimal batch of
+// functions.
+func Batch(fns []*ir.Func, recursive bool) {
+ var b batch
+ b.heapLoc.attrs = attrEscapes | attrPersists | attrMutates | attrCalls
+ b.mutatorLoc.attrs = attrMutates
+ b.calleeLoc.attrs = attrCalls
+
+ // Construct data-flow graph from syntax trees.
+ for _, fn := range fns {
+ if base.Flag.W > 1 {
+ s := fmt.Sprintf("\nbefore escape %v", fn)
+ ir.Dump(s, fn)
+ }
+ b.initFunc(fn)
+ }
+ for _, fn := range fns {
+ if !fn.IsHiddenClosure() {
+ b.walkFunc(fn)
+ }
+ }
+
+ // We've walked the function bodies, so we've seen everywhere a
+ // variable might be reassigned or have it's address taken. Now we
+ // can decide whether closures should capture their free variables
+ // by value or reference.
+ for _, closure := range b.closures {
+ b.flowClosure(closure.k, closure.clo)
+ }
+ b.closures = nil
+
+ for _, loc := range b.allLocs {
+ if why := HeapAllocReason(loc.n); why != "" {
+ b.flow(b.heapHole().addr(loc.n, why), loc)
+ }
+ }
+
+ b.walkAll()
+ b.finish(fns)
+}
+
+func (b *batch) with(fn *ir.Func) *escape {
+ return &escape{
+ batch: b,
+ curfn: fn,
+ loopDepth: 1,
+ }
+}
+
+func (b *batch) initFunc(fn *ir.Func) {
+ e := b.with(fn)
+ if fn.Esc() != escFuncUnknown {
+ base.Fatalf("unexpected node: %v", fn)
+ }
+ fn.SetEsc(escFuncPlanned)
+ if base.Flag.LowerM > 3 {
+ ir.Dump("escAnalyze", fn)
+ }
+
+ // Allocate locations for local variables.
+ for _, n := range fn.Dcl {
+ e.newLoc(n, true)
+ }
+
+ // Also for hidden parameters (e.g., the ".this" parameter to a
+ // method value wrapper).
+ if fn.OClosure == nil {
+ for _, n := range fn.ClosureVars {
+ e.newLoc(n.Canonical(), true)
+ }
+ }
+
+ // Initialize resultIndex for result parameters.
+ for i, f := range fn.Type().Results() {
+ e.oldLoc(f.Nname.(*ir.Name)).resultIndex = 1 + i
+ }
+}
+
+func (b *batch) walkFunc(fn *ir.Func) {
+ e := b.with(fn)
+ fn.SetEsc(escFuncStarted)
+
+ // Identify labels that mark the head of an unstructured loop.
+ ir.Visit(fn, func(n ir.Node) {
+ switch n.Op() {
+ case ir.OLABEL:
+ n := n.(*ir.LabelStmt)
+ if n.Label.IsBlank() {
+ break
+ }
+ if e.labels == nil {
+ e.labels = make(map[*types.Sym]labelState)
+ }
+ e.labels[n.Label] = nonlooping
+
+ case ir.OGOTO:
+ // If we visited the label before the goto,
+ // then this is a looping label.
+ n := n.(*ir.BranchStmt)
+ if e.labels[n.Label] == nonlooping {
+ e.labels[n.Label] = looping
+ }
+ }
+ })
+
+ e.block(fn.Body)
+
+ if len(e.labels) != 0 {
+ base.FatalfAt(fn.Pos(), "leftover labels after walkFunc")
+ }
+}
+
+func (b *batch) flowClosure(k hole, clo *ir.ClosureExpr) {
+ for _, cv := range clo.Func.ClosureVars {
+ n := cv.Canonical()
+ loc := b.oldLoc(cv)
+ if !loc.captured {
+ base.FatalfAt(cv.Pos(), "closure variable never captured: %v", cv)
+ }
+
+ // Capture by value for variables <= 128 bytes that are never reassigned.
+ n.SetByval(!loc.addrtaken && !loc.reassigned && n.Type().Size() <= 128)
+ if !n.Byval() {
+ n.SetAddrtaken(true)
+ if n.Sym().Name == typecheck.LocalDictName {
+ base.FatalfAt(n.Pos(), "dictionary variable not captured by value")
+ }
+ }
+
+ if base.Flag.LowerM > 1 {
+ how := "ref"
+ if n.Byval() {
+ how = "value"
+ }
+ base.WarnfAt(n.Pos(), "%v capturing by %s: %v (addr=%v assign=%v width=%d)", n.Curfn, how, n, loc.addrtaken, loc.reassigned, n.Type().Size())
+ }
+
+ // Flow captured variables to closure.
+ k := k
+ if !cv.Byval() {
+ k = k.addr(cv, "reference")
+ }
+ b.flow(k.note(cv, "captured by a closure"), loc)
+ }
+}
+
+func (b *batch) finish(fns []*ir.Func) {
+ // Record parameter tags for package export data.
+ for _, fn := range fns {
+ fn.SetEsc(escFuncTagged)
+
+ for i, param := range fn.Type().RecvParams() {
+ param.Note = b.paramTag(fn, 1+i, param)
+ }
+ }
+
+ for _, loc := range b.allLocs {
+ n := loc.n
+ if n == nil {
+ continue
+ }
+
+ if n.Op() == ir.ONAME {
+ n := n.(*ir.Name)
+ n.Opt = nil
+ }
+
+ // Update n.Esc based on escape analysis results.
+
+ // Omit escape diagnostics for go/defer wrappers, at least for now.
+ // Historically, we haven't printed them, and test cases don't expect them.
+ // TODO(mdempsky): Update tests to expect this.
+ goDeferWrapper := n.Op() == ir.OCLOSURE && n.(*ir.ClosureExpr).Func.Wrapper()
+
+ if loc.hasAttr(attrEscapes) {
+ if n.Op() == ir.ONAME {
+ if base.Flag.CompilingRuntime {
+ base.ErrorfAt(n.Pos(), 0, "%v escapes to heap, not allowed in runtime", n)
+ }
+ if base.Flag.LowerM != 0 {
+ base.WarnfAt(n.Pos(), "moved to heap: %v", n)
+ }
+ } else {
+ if base.Flag.LowerM != 0 && !goDeferWrapper {
+ base.WarnfAt(n.Pos(), "%v escapes to heap", n)
+ }
+ if logopt.Enabled() {
+ var e_curfn *ir.Func // TODO(mdempsky): Fix.
+ logopt.LogOpt(n.Pos(), "escape", "escape", ir.FuncName(e_curfn))
+ }
+ }
+ n.SetEsc(ir.EscHeap)
+ } else {
+ if base.Flag.LowerM != 0 && n.Op() != ir.ONAME && !goDeferWrapper {
+ base.WarnfAt(n.Pos(), "%v does not escape", n)
+ }
+ n.SetEsc(ir.EscNone)
+ if !loc.hasAttr(attrPersists) {
+ switch n.Op() {
+ case ir.OCLOSURE:
+ n := n.(*ir.ClosureExpr)
+ n.SetTransient(true)
+ case ir.OMETHVALUE:
+ n := n.(*ir.SelectorExpr)
+ n.SetTransient(true)
+ case ir.OSLICELIT:
+ n := n.(*ir.CompLitExpr)
+ n.SetTransient(true)
+ }
+ }
+ }
+
+ // If the result of a string->[]byte conversion is never mutated,
+ // then it can simply reuse the string's memory directly.
+ if base.Debug.ZeroCopy != 0 {
+ if n, ok := n.(*ir.ConvExpr); ok && n.Op() == ir.OSTR2BYTES && !loc.hasAttr(attrMutates) {
+ if base.Flag.LowerM >= 1 {
+ base.WarnfAt(n.Pos(), "zero-copy string->[]byte conversion")
+ }
+ n.SetOp(ir.OSTR2BYTESTMP)
+ }
+ }
+ }
+}
+
+// inMutualBatch reports whether function fn is in the batch of
+// mutually recursive functions being analyzed. When this is true,
+// fn has not yet been analyzed, so its parameters and results
+// should be incorporated directly into the flow graph instead of
+// relying on its escape analysis tagging.
+func (b *batch) inMutualBatch(fn *ir.Name) bool {
+ if fn.Defn != nil && fn.Defn.Esc() < escFuncTagged {
+ if fn.Defn.Esc() == escFuncUnknown {
+ base.FatalfAt(fn.Pos(), "graph inconsistency: %v", fn)
+ }
+ return true
+ }
+ return false
+}
+
+const (
+ escFuncUnknown = 0 + iota
+ escFuncPlanned
+ escFuncStarted
+ escFuncTagged
+)
+
+// Mark labels that have no backjumps to them as not increasing e.loopdepth.
+type labelState int
+
+const (
+ looping labelState = 1 + iota
+ nonlooping
+)
+
+func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
+ name := func() string {
+ if f.Nname != nil {
+ return f.Nname.Sym().Name
+ }
+ return fmt.Sprintf("arg#%d", narg)
+ }
+
+ // Only report diagnostics for user code;
+ // not for wrappers generated around them.
+ // TODO(mdempsky): Generalize this.
+ diagnose := base.Flag.LowerM != 0 && !(fn.Wrapper() || fn.Dupok())
+
+ if len(fn.Body) == 0 {
+ // Assume that uintptr arguments must be held live across the call.
+ // This is most important for syscall.Syscall.
+ // See golang.org/issue/13372.
+ // This really doesn't have much to do with escape analysis per se,
+ // but we are reusing the ability to annotate an individual function
+ // argument and pass those annotations along to importing code.
+ fn.Pragma |= ir.UintptrKeepAlive
+
+ if f.Type.IsUintptr() {
+ if diagnose {
+ base.WarnfAt(f.Pos, "assuming %v is unsafe uintptr", name())
+ }
+ return ""
+ }
+
+ if !f.Type.HasPointers() { // don't bother tagging for scalars
+ return ""
+ }
+
+ var esc leaks
+
+ // External functions are assumed unsafe, unless
+ // //go:noescape is given before the declaration.
+ if fn.Pragma&ir.Noescape != 0 {
+ if diagnose && f.Sym != nil {
+ base.WarnfAt(f.Pos, "%v does not escape", name())
+ }
+ esc.AddMutator(0)
+ esc.AddCallee(0)
+ } else {
+ if diagnose && f.Sym != nil {
+ base.WarnfAt(f.Pos, "leaking param: %v", name())
+ }
+ esc.AddHeap(0)
+ }
+
+ return esc.Encode()
+ }
+
+ if fn.Pragma&ir.UintptrEscapes != 0 {
+ if f.Type.IsUintptr() {
+ if diagnose {
+ base.WarnfAt(f.Pos, "marking %v as escaping uintptr", name())
+ }
+ return ""
+ }
+ if f.IsDDD() && f.Type.Elem().IsUintptr() {
+ // final argument is ...uintptr.
+ if diagnose {
+ base.WarnfAt(f.Pos, "marking %v as escaping ...uintptr", name())
+ }
+ return ""
+ }
+ }
+
+ if !f.Type.HasPointers() { // don't bother tagging for scalars
+ return ""
+ }
+
+ // Unnamed parameters are unused and therefore do not escape.
+ if f.Sym == nil || f.Sym.IsBlank() {
+ var esc leaks
+ return esc.Encode()
+ }
+
+ n := f.Nname.(*ir.Name)
+ loc := b.oldLoc(n)
+ esc := loc.paramEsc
+ esc.Optimize()
+
+ if diagnose && !loc.hasAttr(attrEscapes) {
+ b.reportLeaks(f.Pos, name(), esc, fn.Type())
+ }
+
+ return esc.Encode()
+}
+
+func (b *batch) reportLeaks(pos src.XPos, name string, esc leaks, sig *types.Type) {
+ warned := false
+ if x := esc.Heap(); x >= 0 {
+ if x == 0 {
+ base.WarnfAt(pos, "leaking param: %v", name)
+ } else {
+ // TODO(mdempsky): Mention level=x like below?
+ base.WarnfAt(pos, "leaking param content: %v", name)
+ }
+ warned = true
+ }
+ for i := 0; i < numEscResults; i++ {
+ if x := esc.Result(i); x >= 0 {
+ res := sig.Result(i).Nname.Sym().Name
+ base.WarnfAt(pos, "leaking param: %v to result %v level=%d", name, res, x)
+ warned = true
+ }
+ }
+
+ if base.Debug.EscapeMutationsCalls <= 0 {
+ if !warned {
+ base.WarnfAt(pos, "%v does not escape", name)
+ }
+ return
+ }
+
+ if x := esc.Mutator(); x >= 0 {
+ base.WarnfAt(pos, "mutates param: %v derefs=%v", name, x)
+ warned = true
+ }
+ if x := esc.Callee(); x >= 0 {
+ base.WarnfAt(pos, "calls param: %v derefs=%v", name, x)
+ warned = true
+ }
+
+ if !warned {
+ base.WarnfAt(pos, "%v does not escape, mutate, or call", name)
+ }
+}