go/ssa: eliminate dead φ-nodes in cycles
The previous "dead φ" check was simple and naive but left cycles of dead φ-nodes. This confused some downstream static analysis tools. This change makes the φ-nodes liveness check transitive. + Test. Also, number phi nodes so they're not all called t0 during debugging. Reduces memory consumption by 1%. Increases execution time by <1%. Change-Id: I2908662c1478d455fdf4a179f4a12d6184a456c0 Reviewed-on: https://go-review.googlesource.com/37157 Reviewed-by: Robert Griesemer <gri@golang.org>
This commit is contained in:
parent
8e779ee0a4
commit
219e654bb7
|
@ -11,6 +11,7 @@ import (
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
"go/types"
|
"go/types"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -417,3 +418,83 @@ var (
|
||||||
t.Errorf("want func: %q: %q", fn, descr)
|
t.Errorf("want func: %q: %q", fn, descr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPhiElimination ensures that dead phis, including those that
|
||||||
|
// participate in a cycle, are properly eliminated.
|
||||||
|
func TestPhiElimination(t *testing.T) {
|
||||||
|
const input = `
|
||||||
|
package p
|
||||||
|
|
||||||
|
func f() error
|
||||||
|
|
||||||
|
func g(slice []int) {
|
||||||
|
for {
|
||||||
|
for range slice {
|
||||||
|
// e should not be lifted to a dead φ-node.
|
||||||
|
e := f()
|
||||||
|
h(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func h(error)
|
||||||
|
`
|
||||||
|
// The SSA code for this function should look something like this:
|
||||||
|
// 0:
|
||||||
|
// jump 1
|
||||||
|
// 1:
|
||||||
|
// t0 = len(slice)
|
||||||
|
// jump 2
|
||||||
|
// 2:
|
||||||
|
// t1 = phi [1: -1:int, 3: t2]
|
||||||
|
// t2 = t1 + 1:int
|
||||||
|
// t3 = t2 < t0
|
||||||
|
// if t3 goto 3 else 1
|
||||||
|
// 3:
|
||||||
|
// t4 = f()
|
||||||
|
// t5 = h(t4)
|
||||||
|
// jump 2
|
||||||
|
//
|
||||||
|
// But earlier versions of the SSA construction algorithm would
|
||||||
|
// additionally generate this cycle of dead phis:
|
||||||
|
//
|
||||||
|
// 1:
|
||||||
|
// t7 = phi [0: nil:error, 2: t8] #e
|
||||||
|
// ...
|
||||||
|
// 2:
|
||||||
|
// t8 = phi [1: t7, 3: t4] #e
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
var conf loader.Config
|
||||||
|
f, err := conf.ParseFile("<input>", input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
conf.CreateFromFiles("p", f)
|
||||||
|
|
||||||
|
// Load
|
||||||
|
lprog, err := conf.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and build SSA
|
||||||
|
prog := ssautil.CreateProgram(lprog, 0)
|
||||||
|
p := prog.Package(lprog.Package("p").Pkg)
|
||||||
|
p.Build()
|
||||||
|
g := p.Func("g")
|
||||||
|
|
||||||
|
phis := 0
|
||||||
|
for _, b := range g.Blocks {
|
||||||
|
for _, instr := range b.Instrs {
|
||||||
|
if _, ok := instr.(*ssa.Phi); ok {
|
||||||
|
phis++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if phis != 1 {
|
||||||
|
g.WriteTo(os.Stderr)
|
||||||
|
t.Errorf("expected a single Phi (for the range index), got %d", phis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
123
go/ssa/lift.go
123
go/ssa/lift.go
|
@ -36,9 +36,6 @@ package ssa
|
||||||
// Consider exploiting liveness information to avoid creating dead
|
// Consider exploiting liveness information to avoid creating dead
|
||||||
// φ-nodes which we then immediately remove.
|
// φ-nodes which we then immediately remove.
|
||||||
//
|
//
|
||||||
// Integrate lifting with scalar replacement of aggregates (SRA) since
|
|
||||||
// the two are synergistic.
|
|
||||||
//
|
|
||||||
// Also see many other "TODO: opt" suggestions in the code.
|
// Also see many other "TODO: opt" suggestions in the code.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -49,8 +46,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// If true, perform sanity checking and show diagnostic information at
|
// If true, show diagnostic information at each step of lifting.
|
||||||
// each step of lifting. Very verbose.
|
// Very verbose.
|
||||||
const debugLifting = false
|
const debugLifting = false
|
||||||
|
|
||||||
// domFrontier maps each block to the set of blocks in its dominance
|
// domFrontier maps each block to the set of blocks in its dominance
|
||||||
|
@ -122,7 +119,7 @@ func removeInstr(refs []Instruction, instr Instruction) []Instruction {
|
||||||
return refs[:i]
|
return refs[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// lift attempts to replace local and new Allocs accessed only with
|
// lift replaces local and new Allocs accessed only with
|
||||||
// load/store by SSA registers, inserting φ-nodes where necessary.
|
// load/store by SSA registers, inserting φ-nodes where necessary.
|
||||||
// The result is a program in classical pruned SSA form.
|
// The result is a program in classical pruned SSA form.
|
||||||
//
|
//
|
||||||
|
@ -178,6 +175,11 @@ func lift(fn *Function) {
|
||||||
// instructions.
|
// instructions.
|
||||||
usesDefer := false
|
usesDefer := false
|
||||||
|
|
||||||
|
// A counter used to generate ~unique ids for Phi nodes, as an
|
||||||
|
// aid to debugging. We use large numbers to make them highly
|
||||||
|
// visible. All nodes are renumbered later.
|
||||||
|
fresh := 1000
|
||||||
|
|
||||||
// Determine which allocs we can lift and number them densely.
|
// Determine which allocs we can lift and number them densely.
|
||||||
// The renaming phase uses this numbering for compact maps.
|
// The renaming phase uses this numbering for compact maps.
|
||||||
numAllocs := 0
|
numAllocs := 0
|
||||||
|
@ -188,7 +190,7 @@ func lift(fn *Function) {
|
||||||
switch instr := instr.(type) {
|
switch instr := instr.(type) {
|
||||||
case *Alloc:
|
case *Alloc:
|
||||||
index := -1
|
index := -1
|
||||||
if liftAlloc(df, instr, newPhis) {
|
if liftAlloc(df, instr, newPhis, &fresh) {
|
||||||
index = numAllocs
|
index = numAllocs
|
||||||
numAllocs++
|
numAllocs++
|
||||||
}
|
}
|
||||||
|
@ -211,29 +213,13 @@ func lift(fn *Function) {
|
||||||
// Renaming.
|
// Renaming.
|
||||||
rename(fn.Blocks[0], renaming, newPhis)
|
rename(fn.Blocks[0], renaming, newPhis)
|
||||||
|
|
||||||
// Eliminate dead new phis, then prepend the live ones to each block.
|
// Eliminate dead φ-nodes.
|
||||||
for _, b := range fn.Blocks {
|
removeDeadPhis(newPhis)
|
||||||
|
|
||||||
// Compress the newPhis slice to eliminate unused phis.
|
// Prepend remaining live φ-nodes to each block.
|
||||||
// TODO(adonovan): opt: compute liveness to avoid
|
for _, b := range fn.Blocks {
|
||||||
// placing phis in blocks for which the alloc cell is
|
|
||||||
// not live.
|
|
||||||
nps := newPhis[b]
|
nps := newPhis[b]
|
||||||
j := 0
|
j := len(nps)
|
||||||
for _, np := range nps {
|
|
||||||
if !phiIsLive(np.phi) {
|
|
||||||
// discard it, first removing it from referrers
|
|
||||||
for _, newval := range np.phi.Edges {
|
|
||||||
if refs := newval.Referrers(); refs != nil {
|
|
||||||
*refs = removeInstr(*refs, np.phi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nps[j] = np
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
nps = nps[:j]
|
|
||||||
|
|
||||||
rundefersToKill := b.rundefers
|
rundefersToKill := b.rundefers
|
||||||
if usesDefer {
|
if usesDefer {
|
||||||
|
@ -245,8 +231,8 @@ func lift(fn *Function) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact nps + non-nil Instrs into a new slice.
|
// Compact nps + non-nil Instrs into a new slice.
|
||||||
// TODO(adonovan): opt: compact in situ if there is
|
// TODO(adonovan): opt: compact in situ (rightwards)
|
||||||
// sufficient space or slack in the slice.
|
// if Instrs has sufficient space or slack.
|
||||||
dst := make([]Instruction, len(b.Instrs)+j-b.gaps-rundefersToKill)
|
dst := make([]Instruction, len(b.Instrs)+j-b.gaps-rundefersToKill)
|
||||||
for i, np := range nps {
|
for i, np := range nps {
|
||||||
dst[i] = np.phi
|
dst[i] = np.phi
|
||||||
|
@ -263,9 +249,6 @@ func lift(fn *Function) {
|
||||||
dst[j] = instr
|
dst[j] = instr
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
for i, np := range nps {
|
|
||||||
dst[i] = np.phi
|
|
||||||
}
|
|
||||||
b.Instrs = dst
|
b.Instrs = dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,16 +267,68 @@ func lift(fn *Function) {
|
||||||
fn.Locals = fn.Locals[:j]
|
fn.Locals = fn.Locals[:j]
|
||||||
}
|
}
|
||||||
|
|
||||||
func phiIsLive(phi *Phi) bool {
|
// removeDeadPhis removes φ-nodes not transitively needed by a
|
||||||
|
// non-Phi, non-DebugRef instruction.
|
||||||
|
func removeDeadPhis(newPhis newPhiMap) {
|
||||||
|
// First pass: compute reachability from non-Phi/DebugRef instructions.
|
||||||
|
livePhis := make(map[*Phi]bool)
|
||||||
|
for _, npList := range newPhis {
|
||||||
|
for _, np := range npList {
|
||||||
|
phi := np.phi
|
||||||
|
if !livePhis[phi] && phiHasDirectReferrer(phi) {
|
||||||
|
markLivePhi(livePhis, phi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: eliminate unused phis from newPhis.
|
||||||
|
for block, npList := range newPhis {
|
||||||
|
j := 0
|
||||||
|
for _, np := range npList {
|
||||||
|
if livePhis[np.phi] {
|
||||||
|
npList[j] = np
|
||||||
|
j++
|
||||||
|
} else {
|
||||||
|
// discard it, first removing it from referrers
|
||||||
|
for _, val := range np.phi.Edges {
|
||||||
|
if refs := val.Referrers(); refs != nil {
|
||||||
|
*refs = removeInstr(*refs, np.phi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This may leave DebugRef instructions referring to
|
||||||
|
// Phis that aren't in the control flow graph.
|
||||||
|
// TODO(adonovan): we should delete them.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPhis[block] = npList[:j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// markLivePhi marks phi, and all φ-nodes transitively reachable via
|
||||||
|
// its Operands, live.
|
||||||
|
func markLivePhi(livePhis map[*Phi]bool, phi *Phi) {
|
||||||
|
livePhis[phi] = true
|
||||||
|
for _, rand := range phi.Operands(nil) {
|
||||||
|
if q, ok := (*rand).(*Phi); ok {
|
||||||
|
if !livePhis[q] {
|
||||||
|
markLivePhi(livePhis, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phiHasDirectReferrer reports whether phi is directly referred to by
|
||||||
|
// a non-Phi, non-DebugRef instruction. Such instructions are the
|
||||||
|
// roots of the liveness traversal.
|
||||||
|
func phiHasDirectReferrer(phi *Phi) bool {
|
||||||
for _, instr := range *phi.Referrers() {
|
for _, instr := range *phi.Referrers() {
|
||||||
if instr == phi {
|
switch instr.(type) {
|
||||||
continue // self-refs don't count
|
case *Phi, *DebugRef:
|
||||||
}
|
// ignore
|
||||||
if _, ok := instr.(*DebugRef); ok {
|
default:
|
||||||
continue // debug refs don't count
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +372,9 @@ type newPhiMap map[*BasicBlock][]newPhi
|
||||||
// and if so, it populates newPhis with all the φ-nodes it may require
|
// and if so, it populates newPhis with all the φ-nodes it may require
|
||||||
// and returns true.
|
// and returns true.
|
||||||
//
|
//
|
||||||
func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap) bool {
|
// fresh is a source of fresh ids for phi nodes.
|
||||||
|
//
|
||||||
|
func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap, fresh *int) bool {
|
||||||
// Don't lift aggregates into registers, because we don't have
|
// Don't lift aggregates into registers, because we don't have
|
||||||
// a way to express their zero-constants.
|
// a way to express their zero-constants.
|
||||||
switch deref(alloc.Type()).Underlying().(type) {
|
switch deref(alloc.Type()).Underlying().(type) {
|
||||||
|
@ -420,6 +457,10 @@ func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap) bool {
|
||||||
Edges: make([]Value, len(v.Preds)),
|
Edges: make([]Value, len(v.Preds)),
|
||||||
Comment: alloc.Comment,
|
Comment: alloc.Comment,
|
||||||
}
|
}
|
||||||
|
// This is merely a debugging aid:
|
||||||
|
phi.setNum(*fresh)
|
||||||
|
*fresh++
|
||||||
|
|
||||||
phi.pos = alloc.Pos()
|
phi.pos = alloc.Pos()
|
||||||
phi.setType(deref(alloc.Type()))
|
phi.setType(deref(alloc.Type()))
|
||||||
phi.block = v
|
phi.block = v
|
||||||
|
|
Loading…
Reference in New Issue