src/cmd/compile/internal/escape/escape.go GO 678 lines View on github.com → Search inside
1// Copyright 2018 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package escape67import (8	"fmt"9	"go/constant"10	"go/token"11	"internal/goexperiment"1213	"cmd/compile/internal/base"14	"cmd/compile/internal/ir"15	"cmd/compile/internal/logopt"16	"cmd/compile/internal/typecheck"17	"cmd/compile/internal/types"18	"cmd/internal/src"19)2021// Escape analysis.22//23// Here we analyze functions to determine which Go variables24// (including implicit allocations such as calls to "new" or "make",25// composite literals, etc.) can be allocated on the stack. The two26// key invariants we have to ensure are: (1) pointers to stack objects27// cannot be stored in the heap, and (2) pointers to a stack object28// cannot outlive that object (e.g., because the declaring function29// returned and destroyed the object's stack frame, or its space is30// reused across loop iterations for logically distinct variables).31//32// We implement this with a static data-flow analysis of the AST.33// First, we construct a directed weighted graph where vertices34// (termed "locations") represent variables allocated by statements35// and expressions, and edges represent assignments between variables36// (with weights representing addressing/dereference counts).37//38// Next we walk the graph looking for assignment paths that might39// violate the invariants stated above. If a variable v's address is40// stored in the heap or elsewhere that may outlive it, then v is41// marked as requiring heap allocation.42//43// To support interprocedural analysis, we also record data-flow from44// each function's parameters to the heap and to its result45// parameters. This information is summarized as "parameter tags",46// which are used at static call sites to improve escape analysis of47// function arguments.4849// Constructing the location graph.50//51// Every allocating statement (e.g., variable declaration) or52// expression (e.g., "new" or "make") is first mapped to a unique53// "location."54//55// We also model every Go assignment as a directed edges between56// locations. The number of dereference operations minus the number of57// addressing operations is recorded as the edge's weight (termed58// "derefs"). For example:59//60//     p = &q    // -161//     p = q     //  062//     p = *q    //  163//     p = **q   //  264//65//     p = **&**&q  // 266//67// Note that the & operator can only be applied to addressable68// expressions, and the expression &x itself is not addressable, so69// derefs cannot go below -1.70//71// Every Go language construct is lowered into this representation,72// generally without sensitivity to flow, path, or context; and73// without distinguishing elements within a compound variable. For74// example:75//76//     var x struct { f, g *int }77//     var u []*int78//79//     x.f = u[0]80//81// is modeled simply as82//83//     x = *u84//85// That is, we don't distinguish x.f from x.g, or u[0] from u[1],86// u[2], etc. However, we do record the implicit dereference involved87// in indexing a slice.8889// A batch holds escape analysis state that's shared across an entire90// batch of functions being analyzed at once.91type batch struct {92	allLocs         []*location93	closures        []closure94	reassignOracles map[*ir.Func]*ir.ReassignOracle9596	heapLoc    location97	mutatorLoc location98	calleeLoc  location99	blankLoc   location100}101102// A closure holds a closure expression and its spill hole (i.e.,103// where the hole representing storing into its closure record).104type closure struct {105	k   hole106	clo *ir.ClosureExpr107}108109// An escape holds state specific to a single function being analyzed110// within a batch.111type escape struct {112	*batch113114	curfn *ir.Func // function being analyzed115116	labels map[*types.Sym]labelState // known labels117118	// loopDepth counts the current loop nesting depth within119	// curfn. It increments within each "for" loop and at each120	// label with a corresponding backwards "goto" (i.e.,121	// unstructured loop).122	loopDepth int123}124125func Funcs(all []*ir.Func) {126	// Make a cache of ir.ReassignOracles. The cache is lazily populated.127	// TODO(thepudds): consider adding a field on ir.Func instead. We might also be able128	// to use that field elsewhere, like in walk. See discussion in https://go.dev/cl/688075.129	reassignOracles := make(map[*ir.Func]*ir.ReassignOracle)130131	ir.VisitFuncsBottomUp(all, func(list []*ir.Func, recursive bool) {132		Batch(list, reassignOracles)133	})134}135136// Batch performs escape analysis on a minimal batch of137// functions.138func Batch(fns []*ir.Func, reassignOracles map[*ir.Func]*ir.ReassignOracle) {139	var b batch140	b.heapLoc.attrs = attrEscapes | attrPersists | attrMutates | attrCalls141	b.mutatorLoc.attrs = attrMutates142	b.calleeLoc.attrs = attrCalls143	b.reassignOracles = reassignOracles144145	// Construct data-flow graph from syntax trees.146	for _, fn := range fns {147		if base.Flag.W > 1 {148			s := fmt.Sprintf("\nbefore escape %v", fn)149			ir.Dump(s, fn)150		}151		b.initFunc(fn)152	}153	for _, fn := range fns {154		if !fn.IsClosure() {155			b.walkFunc(fn)156		}157	}158159	// We've walked the function bodies, so we've seen everywhere a160	// variable might be reassigned or have its address taken. Now we161	// can decide whether closures should capture their free variables162	// by value or reference.163	for _, closure := range b.closures {164		b.flowClosure(closure.k, closure.clo)165	}166	b.closures = nil167168	for _, loc := range b.allLocs {169		// Try to replace some non-constant expressions with literals.170		b.rewriteWithLiterals(loc.n, loc.curfn)171172		// Check if the node must be heap allocated for certain reasons173		// such as OMAKESLICE for a large slice.174		if why := HeapAllocReason(loc.n); why != "" {175			b.flow(b.heapHole().addr(loc.n, why), loc)176		}177	}178179	b.walkAll()180	b.finish(fns)181}182183func (b *batch) with(fn *ir.Func) *escape {184	return &escape{185		batch:     b,186		curfn:     fn,187		loopDepth: 1,188	}189}190191func (b *batch) initFunc(fn *ir.Func) {192	e := b.with(fn)193	if fn.Esc() != escFuncUnknown {194		base.Fatalf("unexpected node: %v", fn)195	}196	fn.SetEsc(escFuncPlanned)197	if base.Flag.LowerM > 3 {198		ir.Dump("escAnalyze", fn)199	}200201	// Allocate locations for local variables.202	for _, n := range fn.Dcl {203		e.newLoc(n, true)204	}205206	// Also for hidden parameters (e.g., the ".this" parameter to a207	// method value wrapper).208	if fn.OClosure == nil {209		for _, n := range fn.ClosureVars {210			e.newLoc(n.Canonical(), true)211		}212	}213214	// Initialize resultIndex for result parameters.215	for i, f := range fn.Type().Results() {216		e.oldLoc(f.Nname.(*ir.Name)).resultIndex = 1 + i217	}218}219220func (b *batch) walkFunc(fn *ir.Func) {221	e := b.with(fn)222	fn.SetEsc(escFuncStarted)223224	// Identify labels that mark the head of an unstructured loop.225	ir.Visit(fn, func(n ir.Node) {226		switch n.Op() {227		case ir.OLABEL:228			n := n.(*ir.LabelStmt)229			if n.Label.IsBlank() {230				break231			}232			if e.labels == nil {233				e.labels = make(map[*types.Sym]labelState)234			}235			e.labels[n.Label] = nonlooping236237		case ir.OGOTO:238			// If we visited the label before the goto,239			// then this is a looping label.240			n := n.(*ir.BranchStmt)241			if e.labels[n.Label] == nonlooping {242				e.labels[n.Label] = looping243			}244		}245	})246247	e.block(fn.Body)248249	if len(e.labels) != 0 {250		base.FatalfAt(fn.Pos(), "leftover labels after walkFunc")251	}252}253254func (b *batch) flowClosure(k hole, clo *ir.ClosureExpr) {255	for _, cv := range clo.Func.ClosureVars {256		n := cv.Canonical()257		loc := b.oldLoc(cv)258		if !loc.captured {259			base.FatalfAt(cv.Pos(), "closure variable never captured: %v", cv)260		}261262		// Capture by value for variables <= 128 bytes that are never reassigned.263		n.SetByval(!loc.addrtaken && !loc.reassigned && n.Type().Size() <= 128)264		if !n.Byval() {265			n.SetAddrtaken(true)266			if n.Sym().Name == typecheck.LocalDictName {267				base.FatalfAt(n.Pos(), "dictionary variable not captured by value")268			}269		}270271		if base.Flag.LowerM > 1 {272			how := "ref"273			if n.Byval() {274				how = "value"275			}276			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())277		}278279		// Flow captured variables to closure.280		k := k281		if !cv.Byval() {282			k = k.addr(cv, "reference")283		}284		b.flow(k.note(cv, "captured by a closure"), loc)285	}286}287288func (b *batch) finish(fns []*ir.Func) {289	// Record parameter tags for package export data.290	for _, fn := range fns {291		fn.SetEsc(escFuncTagged)292293		for i, param := range fn.Type().RecvParams() {294			param.Note = b.paramTag(fn, 1+i, param)295		}296	}297298	for _, loc := range b.allLocs {299		n := loc.n300		if n == nil {301			continue302		}303304		if n.Op() == ir.ONAME {305			n := n.(*ir.Name)306			n.Opt = nil307		}308309		// Update n.Esc based on escape analysis results.310311		// Omit escape diagnostics for go/defer wrappers, at least for now.312		// Historically, we haven't printed them, and test cases don't expect them.313		// TODO(mdempsky): Update tests to expect this.314		goDeferWrapper := n.Op() == ir.OCLOSURE && n.(*ir.ClosureExpr).Func.Wrapper()315316		if loc.hasAttr(attrEscapes) {317			if n.Op() == ir.ONAME {318				if base.Flag.CompilingRuntime {319					base.ErrorfAt(n.Pos(), 0, "%v escapes to heap, not allowed in runtime", n)320				}321				if base.Flag.LowerM != 0 {322					base.WarnfAt(n.Pos(), "moved to heap: %v", n)323				}324			} else {325				if base.Flag.LowerM != 0 && !goDeferWrapper {326					if n.Op() == ir.OAPPEND {327						base.WarnfAt(n.Pos(), "append escapes to heap")328					} else {329						base.WarnfAt(n.Pos(), "%v escapes to heap", n)330					}331				}332				if logopt.Enabled() {333					var e_curfn *ir.Func // TODO(mdempsky): Fix.334					logopt.LogOpt(n.Pos(), "escape", "escape", ir.FuncName(e_curfn))335				}336			}337			n.SetEsc(ir.EscHeap)338		} else {339			if base.Flag.LowerM != 0 && n.Op() != ir.ONAME && !goDeferWrapper {340				if n.Op() == ir.OAPPEND {341					base.WarnfAt(n.Pos(), "append does not escape")342				} else {343					base.WarnfAt(n.Pos(), "%v does not escape", n)344				}345			}346			n.SetEsc(ir.EscNone)347			if !loc.hasAttr(attrPersists) {348				switch n.Op() {349				case ir.OCLOSURE:350					n := n.(*ir.ClosureExpr)351					n.SetTransient(true)352				case ir.OMETHVALUE:353					n := n.(*ir.SelectorExpr)354					n.SetTransient(true)355				case ir.OSLICELIT:356					n := n.(*ir.CompLitExpr)357					n.SetTransient(true)358				}359			}360		}361362		// If the result of a string->[]byte conversion is never mutated,363		// then it can simply reuse the string's memory directly.364		if base.Debug.ZeroCopy != 0 {365			if n, ok := n.(*ir.ConvExpr); ok && n.Op() == ir.OSTR2BYTES && !loc.hasAttr(attrMutates) {366				if base.Flag.LowerM >= 1 {367					base.WarnfAt(n.Pos(), "zero-copy string->[]byte conversion")368				}369				n.SetOp(ir.OSTR2BYTESTMP)370			}371		}372	}373374	if goexperiment.RuntimeFreegc {375		// Look for specific patterns of usage, such as appends376		// to slices that we can prove are not aliased.377		for _, fn := range fns {378			a := aliasAnalysis{}379			a.analyze(fn)380		}381	}382383	for _, fn := range fns {384		if ir.MatchAstDump(fn, "escape") {385			ir.AstDump(fn, "escape, "+ir.FuncName(fn))386		}387	}388}389390// inMutualBatch reports whether function fn is in the batch of391// mutually recursive functions being analyzed. When this is true,392// fn has not yet been analyzed, so its parameters and results393// should be incorporated directly into the flow graph instead of394// relying on its escape analysis tagging.395func (b *batch) inMutualBatch(fn *ir.Name) bool {396	if fn.Defn != nil && fn.Defn.Esc() < escFuncTagged {397		if fn.Defn.Esc() == escFuncUnknown {398			base.FatalfAt(fn.Pos(), "graph inconsistency: %v", fn)399		}400		return true401	}402	return false403}404405const (406	escFuncUnknown = 0 + iota407	escFuncPlanned408	escFuncStarted409	escFuncTagged410)411412// Mark labels that have no backjumps to them as not increasing e.loopdepth.413type labelState int414415const (416	looping labelState = 1 + iota417	nonlooping418)419420func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {421	name := func() string {422		if f.Nname != nil {423			return f.Nname.Sym().Name424		}425		return fmt.Sprintf("arg#%d", narg)426	}427428	// Only report diagnostics for user code;429	// not for wrappers generated around them.430	// TODO(mdempsky): Generalize this.431	diagnose := base.Flag.LowerM != 0 && !(fn.Wrapper() || fn.Dupok())432433	if len(fn.Body) == 0 {434		// Assume that uintptr arguments must be held live across the call.435		// This is most important for syscall.Syscall.436		// See golang.org/issue/13372.437		// This really doesn't have much to do with escape analysis per se,438		// but we are reusing the ability to annotate an individual function439		// argument and pass those annotations along to importing code.440		fn.Pragma |= ir.UintptrKeepAlive441442		if f.Type.IsUintptr() {443			if diagnose {444				base.WarnfAt(f.Pos, "assuming %v is unsafe uintptr", name())445			}446			return ""447		}448449		if !f.Type.HasPointers() { // don't bother tagging for scalars450			return ""451		}452453		var esc leaks454455		// External functions are assumed unsafe, unless456		// //go:noescape is given before the declaration.457		if fn.Pragma&ir.Noescape != 0 {458			if diagnose && f.Sym != nil {459				base.WarnfAt(f.Pos, "%v does not escape", name())460			}461			esc.AddMutator(0)462			esc.AddCallee(0)463		} else {464			if diagnose && f.Sym != nil {465				base.WarnfAt(f.Pos, "leaking param: %v", name())466			}467			esc.AddHeap(0)468		}469470		return esc.Encode()471	}472473	if fn.Pragma&ir.UintptrEscapes != 0 {474		if f.Type.IsUintptr() {475			if diagnose {476				base.WarnfAt(f.Pos, "marking %v as escaping uintptr", name())477			}478			return ""479		}480		if f.IsDDD() && f.Type.Elem().IsUintptr() {481			// final argument is ...uintptr.482			if diagnose {483				base.WarnfAt(f.Pos, "marking %v as escaping ...uintptr", name())484			}485			return ""486		}487	}488489	if !f.Type.HasPointers() { // don't bother tagging for scalars490		return ""491	}492493	// Unnamed parameters are unused and therefore do not escape.494	if f.Sym == nil || f.Sym.IsBlank() {495		var esc leaks496		return esc.Encode()497	}498499	n := f.Nname.(*ir.Name)500	loc := b.oldLoc(n)501	esc := loc.paramEsc502	esc.Optimize()503504	if diagnose && !loc.hasAttr(attrEscapes) {505		b.reportLeaks(f.Pos, name(), esc, fn.Type())506	}507508	return esc.Encode()509}510511func (b *batch) reportLeaks(pos src.XPos, name string, esc leaks, sig *types.Type) {512	warned := false513	if x := esc.Heap(); x >= 0 {514		if x == 0 {515			base.WarnfAt(pos, "leaking param: %v", name)516		} else {517			// TODO(mdempsky): Mention level=x like below?518			base.WarnfAt(pos, "leaking param content: %v", name)519		}520		warned = true521	}522	for i := 0; i < numEscResults; i++ {523		if x := esc.Result(i); x >= 0 {524			res := sig.Result(i).Nname.Sym().Name525			base.WarnfAt(pos, "leaking param: %v to result %v level=%d", name, res, x)526			warned = true527		}528	}529530	if base.Debug.EscapeMutationsCalls <= 0 {531		if !warned {532			base.WarnfAt(pos, "%v does not escape", name)533		}534		return535	}536537	if x := esc.Mutator(); x >= 0 {538		base.WarnfAt(pos, "mutates param: %v derefs=%v", name, x)539		warned = true540	}541	if x := esc.Callee(); x >= 0 {542		base.WarnfAt(pos, "calls param: %v derefs=%v", name, x)543		warned = true544	}545546	if !warned {547		base.WarnfAt(pos, "%v does not escape, mutate, or call", name)548	}549}550551// rewriteWithLiterals attempts to replace certain non-constant expressions552// within n with a literal if possible.553func (b *batch) rewriteWithLiterals(n ir.Node, fn *ir.Func) {554	if n == nil || fn == nil {555		return556	}557558	assignTemp := func(pos src.XPos, n ir.Node, init *ir.Nodes) {559		// Preserve any side effects of n by assigning it to an otherwise unused temp.560		tmp := typecheck.TempAt(pos, fn, n.Type())561		init.Append(typecheck.Stmt(ir.NewDecl(pos, ir.ODCL, tmp)))562		init.Append(typecheck.Stmt(ir.NewAssignStmt(pos, tmp, n)))563	}564565	switch n.Op() {566	case ir.OMAKESLICE:567		// Check if we can replace a non-constant argument to make with568		// a literal to allow for this slice to be stack allocated if otherwise allowed.569		n := n.(*ir.MakeExpr)570571		r := &n.Cap572		if n.Cap == nil {573			r = &n.Len574		}575576		if (*r).Op() != ir.OLITERAL {577			// Look up a cached ReassignOracle for the function, lazily computing one if needed.578			ro := b.reassignOracle(fn)579			if ro == nil {580				base.Fatalf("no ReassignOracle for function %v with closure parent %v", fn, fn.ClosureParent)581			}582583			s := ro.StaticValue(*r)584			switch s.Op() {585			case ir.OLITERAL:586				lit, ok := s.(*ir.BasicLit)587				if !ok || lit.Val().Kind() != constant.Int {588					base.Fatalf("unexpected BasicLit Kind")589				}590				if constant.Compare(lit.Val(), token.GEQ, constant.MakeInt64(0)) {591					if !base.LiteralAllocHash.MatchPos(n.Pos(), nil) {592						// De-selected by literal alloc optimizations debug hash.593						return594					}595					// Preserve any side effects of the original expression, then replace it.596					assignTemp(n.Pos(), *r, n.PtrInit())597					*r = ir.NewBasicLit(n.Pos(), (*r).Type(), lit.Val())598				}599			case ir.OLEN:600				x := ro.StaticValue(s.(*ir.UnaryExpr).X)601				if x.Op() == ir.OSLICELIT {602					x := x.(*ir.CompLitExpr)603					// Preserve any side effects of the original expression, then update the value.604					assignTemp(n.Pos(), *r, n.PtrInit())605					*r = ir.NewBasicLit(n.Pos(), types.Types[types.TINT], constant.MakeInt64(x.Len))606				}607			}608		}609	case ir.OCONVIFACE:610		// Check if we can replace a non-constant expression in an interface conversion with611		// a literal to avoid heap allocating the underlying interface value.612		conv := n.(*ir.ConvExpr)613		if conv.X.Op() != ir.OLITERAL && !conv.X.Type().IsInterface() {614			// TODO(thepudds): likely could avoid some work by tightening the check of conv.X's type.615			// Look up a cached ReassignOracle for the function, lazily computing one if needed.616			ro := b.reassignOracle(fn)617			if ro == nil {618				base.Fatalf("no ReassignOracle for function %v with closure parent %v", fn, fn.ClosureParent)619			}620			v := ro.StaticValue(conv.X)621			if v != nil && v.Op() == ir.OLITERAL && ir.ValidTypeForConst(conv.X.Type(), v.Val()) {622				if !base.LiteralAllocHash.MatchPos(n.Pos(), nil) {623					// De-selected by literal alloc optimizations debug hash.624					return625				}626				if base.Debug.EscapeDebug >= 3 {627					base.WarnfAt(n.Pos(), "rewriting OCONVIFACE value from %v (%v) to %v (%v)", conv.X, conv.X.Type(), v, v.Type())628				}629				// Preserve any side effects of the original expression, then replace it.630				assignTemp(conv.Pos(), conv.X, conv.PtrInit())631				v := v.(*ir.BasicLit)632				conv.X = ir.NewBasicLit(conv.Pos(), conv.X.Type(), v.Val())633				typecheck.Expr(conv)634			}635		}636	}637}638639// reassignOracle returns an initialized *ir.ReassignOracle for fn.640// If fn is a closure, it returns the ReassignOracle for the ultimate parent.641//642// A new ReassignOracle is initialized lazily if needed, and the result643// is cached to reduce duplicative work of preparing a ReassignOracle.644func (b *batch) reassignOracle(fn *ir.Func) *ir.ReassignOracle {645	if ro, ok := b.reassignOracles[fn]; ok {646		return ro // Hit.647	}648649	// For closures, we want the ultimate parent's ReassignOracle,650	// so walk up the parent chain, if any.651	f := fn652	for f.ClosureParent != nil && !f.ClosureParent.IsPackageInit() {653		f = f.ClosureParent654	}655656	if f != fn {657		// We found a parent.658		ro := b.reassignOracles[f]659		if ro != nil {660			// Hit, via a parent. Before returning, store this ro for the original fn as well.661			b.reassignOracles[fn] = ro662			return ro663		}664	}665666	// Miss. We did not find a ReassignOracle for fn or a parent, so lazily create one.667	ro := &ir.ReassignOracle{}668	ro.Init(f)669670	// Cache the answer for the original fn.671	b.reassignOracles[fn] = ro672	if f != fn {673		// Cache for the parent as well.674		b.reassignOracles[f] = ro675	}676	return ro677}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.