1
0
mirror of https://github.com/golang/go synced 2024-11-19 04:04:47 -07:00

cmd/compile: use proper work queue for escape graph walking

The old escape analysis code used to repeatedly walk the entire flow
graph until it reached a fixed point. With escape.go, I wanted to
avoid this if possible, so I structured the walking code with two
constraints:

1. Always walk from the heap location last.

2. If an object escapes, ensure it has flow edge to the heap location.

This works, but it precludes some graph construction
optimizations. E.g., if there's an assignment "heap = &x", then we can
immediately tell that 'x' escapes without needing to visit it during
the graph walk. Similarly, if there's a later assignment "x = &y", we
could immediately tell that 'y' escapes too. However, the natural way
to implement this optimization ends up violating the constraints
above.

Further, the constraints above don't guarantee that the 'transient'
flag is handled correctly. Today I think that's handled correctly
because of the order that locations happen to be constructed and
visited based on the AST, but I've felt uneasy about it for a little
while.

This CL changes walkAll to use a proper work queue (technically a work
stack) to track locations that need to be visited, and allows walkOne
to request that a location be re-visited.

Passes toolstash-check.

Change-Id: Iaa6f4d3fe4719c04d67009fb9a2a3e4930b3d7c2
Reviewed-on: https://go-review.googlesource.com/c/go/+/196958
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Matthew Dempsky 2019-09-20 15:27:14 -07:00
parent fad0a14d92
commit 867ea9c17f

View File

@ -100,11 +100,15 @@ type EscLocation struct {
edges []EscEdge // incoming edges edges []EscEdge // incoming edges
loopDepth int // loopDepth at declaration loopDepth int // loopDepth at declaration
// derefs and walkgen are used during walk to track the // derefs and walkgen are used during walkOne to track the
// minimal dereferences from the walk root. // minimal dereferences from the walk root.
derefs int // >= -1 derefs int // >= -1
walkgen uint32 walkgen uint32
// queued is used by walkAll to track whether this location is
// in the walk queue.
queued bool
// escapes reports whether the represented variable's address // escapes reports whether the represented variable's address
// escapes; that is, whether the variable must be heap // escapes; that is, whether the variable must be heap
// allocated. // allocated.
@ -1070,30 +1074,45 @@ func (e *Escape) discardHole() EscHole { return e.blankLoc.asHole() }
// walkAll computes the minimal dereferences between all pairs of // walkAll computes the minimal dereferences between all pairs of
// locations. // locations.
func (e *Escape) walkAll() { func (e *Escape) walkAll() {
var walkgen uint32 // We use a work queue to keep track of locations that we need
// to visit, and repeatedly walk until we reach a fixed point.
for _, loc := range e.allLocs { var todo []*EscLocation // LIFO queue
walkgen++ enqueue := func(loc *EscLocation) {
e.walkOne(loc, walkgen) if !loc.queued {
todo = append(todo, loc)
loc.queued = true
}
} }
// Walk the heap last so that we catch any edges to the heap enqueue(&e.heapLoc)
// added during walkOne. for _, loc := range e.allLocs {
enqueue(loc)
}
var walkgen uint32
for len(todo) > 0 {
root := todo[len(todo)-1]
todo = todo[:len(todo)-1]
root.queued = false
walkgen++ walkgen++
e.walkOne(&e.heapLoc, walkgen) e.walkOne(root, walkgen, enqueue)
}
} }
// walkOne computes the minimal number of dereferences from root to // walkOne computes the minimal number of dereferences from root to
// all other locations. // all other locations.
func (e *Escape) walkOne(root *EscLocation, walkgen uint32) { func (e *Escape) walkOne(root *EscLocation, walkgen uint32, enqueue func(*EscLocation)) {
// The data flow graph has negative edges (from addressing // The data flow graph has negative edges (from addressing
// operations), so we use the Bellman-Ford algorithm. However, // operations), so we use the Bellman-Ford algorithm. However,
// we don't have to worry about infinite negative cycles since // we don't have to worry about infinite negative cycles since
// we bound intermediate dereference counts to 0. // we bound intermediate dereference counts to 0.
root.walkgen = walkgen root.walkgen = walkgen
root.derefs = 0 root.derefs = 0
todo := []*EscLocation{root} todo := []*EscLocation{root} // LIFO queue
for len(todo) > 0 { for len(todo) > 0 {
l := todo[len(todo)-1] l := todo[len(todo)-1]
todo = todo[:len(todo)-1] todo = todo[:len(todo)-1]
@ -1112,9 +1131,9 @@ func (e *Escape) walkOne(root *EscLocation, walkgen uint32) {
// If l's address flows to a non-transient // If l's address flows to a non-transient
// location, then l can't be transiently // location, then l can't be transiently
// allocated. // allocated.
if !root.transient { if !root.transient && l.transient {
l.transient = false l.transient = false
// TODO(mdempsky): Should we re-walk from l now? enqueue(l)
} }
} }
@ -1131,6 +1150,7 @@ func (e *Escape) walkOne(root *EscLocation, walkgen uint32) {
// TODO(mdempsky): Better way to handle this? // TODO(mdempsky): Better way to handle this?
if root != &e.heapLoc { if root != &e.heapLoc {
e.flow(e.heapHole(), l) e.flow(e.heapHole(), l)
enqueue(&e.heapLoc)
} }
} }