diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/BasicBlock.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/BasicBlock.java index 51f7d51..d44eaf4 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/BasicBlock.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/BasicBlock.java @@ -158,6 +158,11 @@ public int whichPred(BasicBlock pred) { } throw new IllegalStateException(); } + public BasicBlock predecessor(int i) { + if (i >= predecessors.size()) + return null; + return predecessors.get(i); + } public int whichSucc(BasicBlock succ) { int i = 0; for (BasicBlock s: successors) { @@ -167,7 +172,17 @@ public int whichSucc(BasicBlock succ) { } throw new IllegalStateException(); } - + public void replaceInstruction(Instruction instruction, List replacements) { + int i; + for (i = 0; i < instructions.size(); i++) + if (instructions.get(i) == instruction) + break; + assert i < instructions.size(); + for (int j = replacements.size()-1; j >= 0; j--) { + instructions.add(i+1, replacements.get(j)); + } + instructions.remove(i); + } public static StringBuilder toStr(StringBuilder sb, BasicBlock bb, BitSet visited, boolean dumpLiveness) { if (visited.get(bb.bid)) diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSA.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSA.java index af66aff..bbf86e8 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSA.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSA.java @@ -4,279 +4,12 @@ /** * Converts from SSA form to non-SSA form. - * Implementation is based on description in - * 'Practical Improvements to the Construction and Destruction - * of Static Single Assignment Form' by Preston Briggs. - * - * The JikesRVM LeaveSSA implements a version of the - * same algorithm. */ public class ExitSSA { - - CompiledFunction function; - NameStack[] stacks; - DominatorTree tree; - public ExitSSA(CompiledFunction function, EnumSet options) { - this.function = function; - if (!function.isSSA) throw new IllegalStateException(); - function.livenessAnalysis(); - if (options.contains(Options.DUMP_SSA_LIVENESS)) function.dumpIR(true, "SSA Liveness Analysis"); - tree = new DominatorTree(function.entry); - if (options.contains(Options.DUMP_SSA_DOMTREE)) { - System.out.println("Pre SSA Dominator Tree"); - System.out.println(tree.generateDotOutput()); - } - initStack(); - insertCopies(function.entry); - removePhis(); - function.isSSA = false; - if (options.contains(Options.DUMP_POST_SSA_IR)) function.dumpIR(false, "After exiting SSA"); - } - - private void removePhis() { - for (BasicBlock block : tree.blocks) { - block.instructions.removeIf(instruction -> instruction instanceof Instruction.Phi); - } - } - - /* Algorithm for iterating through blocks to perform phi replacement */ - private void insertCopies(BasicBlock block) { - List pushed = new ArrayList<>(); - for (Instruction i: block.instructions) { - // replace all uses u with stacks[i] - replaceUses(i); - } - scheduleCopies(block, pushed); - for (BasicBlock c: block.dominatedChildren) { - insertCopies(c); - } - for (Integer name: pushed) { - stacks[name].pop(); - } - } - - /** - * replace all uses u with stacks[i] - */ - private void replaceUses(Instruction i) { - if (i instanceof Instruction.Phi) - // FIXME check this can never be valid - // tests 8/9 in TestInterpreter invoke on Phi but - // replacements are same as existing inputs - return; - var oldUses = i.uses(); - Register[] newUses = new Register[oldUses.size()]; - for (int u = 0; u < oldUses.size(); u++) { - Register use = oldUses.get(u); - if (!stacks[use.id].isEmpty()) - newUses[u] = stacks[use.id].top(); - else - newUses[u] = use; - } - i.replaceUses(newUses); - } - - static class CopyItem { - /** Phi input can be a register or a constant so we record the operand */ - final Operand src; - /** The phi destination */ - final Register dest; - /** The basic block where the phi was present */ - final BasicBlock destBlock; - boolean removed; - - public CopyItem(Operand src, Register dest, BasicBlock destBlock) { - this.src = src; - this.dest = dest; - this.destBlock = destBlock; - this.removed = false; - } - } - - private void scheduleCopies(BasicBlock block, List pushed) { - /* Pass 1 - Initialize data structures */ - /* In this pass we count the number of times a name is used by other phi-nodes */ - List copySet = new ArrayList<>(); - Map map = new HashMap<>(); - BitSet usedByAnother = new BitSet(function.registerPool.numRegisters()*2); - for (BasicBlock s: block.successors) { - int j = s.whichPred(block); - for (Instruction.Phi phi: s.phis()) { - Register dst = phi.value(); - Operand srcOperand = phi.input(j); // jth operand of phi node - if (srcOperand instanceof Operand.RegisterOperand srcRegisterOperand) { - Register src = srcRegisterOperand.reg; - map.put(src.id, src); - usedByAnother.set(src.id); - } - copySet.add(new CopyItem(srcOperand, dst, s)); - map.put(dst.id, dst); - } - } - - /* Pass 2: setup up the worklist of initial copies */ - /* In this pass we build a worklist of names that are not used in other phi nodes */ - List workList = new ArrayList<>(); - for (CopyItem copyItem: copySet) { - if (usedByAnother.get(copyItem.dest.id) != true) { - copyItem.removed = true; - workList.add(copyItem); - } - } - copySet.removeIf(copyItem -> copyItem.removed); - - /* Pass 3: iterate over the worklist, inserting copies */ - /* Copy operations whose destinations are not used by other copy operations can be scheduled immediately */ - /* Each time we insert a copy operation we add the source of that op to the worklist */ - while (!workList.isEmpty() || !copySet.isEmpty()) { - while (!workList.isEmpty()) { - final CopyItem copyItem = workList.remove(0); - final Operand src = copyItem.src; - final Register dest = copyItem.dest; - final BasicBlock destBlock = copyItem.destBlock; - /* Engineering a Compiler: We can avoid the lost copy - problem by checking the liveness of the target name - for each copy that we try to insert. When we discover - a copy target that is live, we must preserve the live - value in a temporary name and rewrite subsequent uses to - refer to the temporary name. - - This captures the cases when the result of a phi - in a control successor is live on exit of the current block. - This means that it is incorrect to simply insert a copy - of the destination in the current block. So we rename - the destination to a new temporary, and record the renaming - so that the dominator blocks get the new name. Comment adapted - from JikesRVM LeaveSSA - */ - if (block.liveOut.get(dest.id)) { - /* Insert a copy from dest to a new temp t at phi node defining dest */ - final Register t = addMoveToTempAfterPhi(destBlock, dest); - stacks[dest.id].push(t); // record the temp name - pushed.add(dest.id); - } - /* Insert a copy operation from map[src] to dest at end of BB */ - if (src instanceof Operand.RegisterOperand srcRegisterOperand) { - addMoveAtBBEnd(block, map.get(srcRegisterOperand.reg.id), dest); - map.put(srcRegisterOperand.reg.id, dest); - /* If src is the name of a dest in copySet add item to worklist */ - /* see comment on phi cycles below. */ - CopyItem item = isCycle(copySet, srcRegisterOperand.reg); - if (item != null) { - workList.add(item); - } - } - else if (src instanceof Operand.ConstantOperand srcConstantOperand) { - addMoveAtBBEnd(block, srcConstantOperand, dest); - } - } - /* Engineering a Compiler: To solve the swap problem - we can detect cases where phi functions reference the - targets of other phi functions in the same block. For each - cycle of references, it must insert a copy to a temporary - that breaks the cycle. Then we can schedule the copies to - respect the dependencies implied by the phi functions. - - An empty work list with work remaining in the copy set - implies a cycle in the dependencies amongst copies. To break - the cycle copy the destination of an arbitrary member of the - copy set to a temporary. This destination has therefore been - saved and can be safely overwritten. So then add the copy to the - work list. Comment adapted from JikesRVM LeaveSSA. - */ - if (!copySet.isEmpty()) { - CopyItem copyItem = copySet.remove(0); - /* Insert a copy from dst to new temp at the end of Block */ - Register t = addMoveToTempAtBBEnd(block, copyItem.dest); - map.put(copyItem.dest.id, t); - workList.add(copyItem); - } - } - } - - private void insertAtEnd(BasicBlock bb, Instruction i) { - assert bb.instructions.size() > 0; - // Last instruction is a branch - so new instruction will - // go before that - int pos = bb.instructions.size()-1; - bb.add(pos, i); - } - - private void insertAfterPhi(BasicBlock bb, Register phiDef, Instruction newInst) { - assert bb.instructions.size() > 0; - int insertionPos = -1; - for (int pos = 0; pos < bb.instructions.size(); pos++) { - Instruction i = bb.instructions.get(pos); - if (i instanceof Instruction.Phi phi) { - if (phi.value().id == phiDef.id) { - insertionPos = pos+1; // After phi - break; - } - } - } - if (insertionPos < 0) { - throw new IllegalStateException(); - } - bb.add(insertionPos, newInst); - } - - /* Insert a copy from dest to new temp at end of BB, and return temp */ - private Register addMoveToTempAtBBEnd(BasicBlock block, Register dest) { - var temp = function.registerPool.newTempReg(dest.name(), dest.type); - var inst = new Instruction.Move(new Operand.RegisterOperand(dest), new Operand.RegisterOperand(temp)); - insertAtEnd(block, inst); - return temp; - } - - /* If src is the name of a dest in copySet remove the item */ - private CopyItem isCycle(List copySet, Register src) { - for (int i = 0; i < copySet.size(); i++) { - CopyItem copyItem = copySet.get(i); - if (copyItem.dest.id == src.id) { - copySet.remove(i); - return copyItem; - } - } - return null; - } - - /* Insert a copy from src to dst at end of BB */ - private void addMoveAtBBEnd(BasicBlock block, Register src, Register dest) { - var inst = new Instruction.Move(new Operand.RegisterOperand(src), new Operand.RegisterOperand(dest)); - insertAtEnd(block, inst); - // If the copy instruction is followed by a cbr which uses the old var - // then we need to update the cbr instruction - // This is not specified in the Briggs paper but t - var brInst = block.instructions.getLast(); - if (brInst instanceof Instruction.ConditionalBranch cbr) { - cbr.replaceUse(src,dest); - } - } - /* Insert a copy from constant src to dst at end of BB */ - private void addMoveAtBBEnd(BasicBlock block, Operand.ConstantOperand src, Register dest) { - var inst = new Instruction.Move(src, new Operand.RegisterOperand(dest)); - insertAtEnd(block, inst); - } - /* Insert a copy dest to a new temp at phi node defining dest, return temp */ - private Register addMoveToTempAfterPhi(BasicBlock block, Register dest) { - var temp = function.registerPool.newTempReg(dest.name(), dest.type); - var inst = new Instruction.Move(new Operand.RegisterOperand(dest), new Operand.RegisterOperand(temp)); - insertAfterPhi(block, dest, inst); - return temp; - } - - private void initStack() { - stacks = new NameStack[function.registerPool.numRegisters()]; - for (int i = 0; i < stacks.length; i++) - stacks[i] = new NameStack(); - } - - static class NameStack { - List stack = new ArrayList<>(); - void push(Register r) { stack.add(r); } - Register top() { return stack.getLast(); } - void pop() { stack.removeLast(); } - boolean isEmpty() { return stack.isEmpty(); } + if (options.contains(Options.SSA_DESTRUCTION_BOISSINOT_NOCOALESCE)) + new ExitSSABoissinotNoCoalesce(function,options); + else + new ExitSSABriggs(function,options); } } diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABoissinotNoCoalesce.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABoissinotNoCoalesce.java new file mode 100644 index 0000000..633cf7e --- /dev/null +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABoissinotNoCoalesce.java @@ -0,0 +1,254 @@ +package com.compilerprogramming.ezlang.compiler; + +import java.util.*; + +/** + * Implement method to exit SSA by converting to conventional SSA, + * without coalescing. This is the basic form. + * + * This is essentially Method 1 described by + * Translating Out of Static Single Assignment Form + * Vugranam C. Sreedhar, Roy Dz-Ching Ju, David M. Gillies, and Vatsa Santhanam + * + * However, Sreedhar left out details such as using parallel copies + * and sequencing of parallel copies. + * + * Revisiting Out-of-SSA Translation for Correctness, Code Quality, and Efficiency + * Benoit Boissinot, Alain Darte, Fabrice Rastello, BenoƮt Dupont de Dinechin, Christophe Guillon + * + * The Boissinot paper gives a more correct description, discussing the need for parallel copy + * and sequencing the parallel copy, but the paper describes a + * more complex approach that performs coalescing. + * + * Engineering a Compiler, 3rd edition, describes the simpler form - omitting the coalescing part. + * + * Our implementation is similar to EaC. + * + * We do not use the sequencing algo described in Boissinot paper. Instead, we use: + * + * https://xavierleroy.org/publi/parallel-move.pdf + * Tilting at windmills with Coq: formal verification of a compilation algorithm for parallel moves + * Laurence Rideau, Bernard Paul Serpette, Xavier Leroy + */ +public class ExitSSABoissinotNoCoalesce { + + CompiledFunction function; + Map parallelCopies = new HashMap<>(); + List allBlocks; + + public ExitSSABoissinotNoCoalesce(CompiledFunction function, EnumSet options) { + this.function = function; + allBlocks = function.getBlocks(); + init(); + makeConventionalSSA(); + removePhis(); + sequenceParallelCopies(); + function.isSSA = false; + if (options.contains(Options.DUMP_POST_SSA_IR)) function.dumpIR(false, "After exiting SSA"); + } + + private void init() { + // We do not actually insert parallel copy instruction until needed + // but we create an auxiliary data structure to help us track these + for (BasicBlock block: allBlocks) { + parallelCopies.put(block,new PCopy(block)); + } + } + private void insertAtEnd(BasicBlock bb, Instruction i) { + assert bb.instructions.size() > 0; + // Last instruction is a branch - so new instruction will + // go before that + int pos = bb.instructions.size()-1; + bb.add(pos, i); + } + + private Instruction.ParallelCopyInstruction getParallelCopyAtEnd(BasicBlock block) { + PCopy pcopy = parallelCopies.get(block); + if (pcopy.pCopyEnd == null) { + pcopy.pCopyEnd = new Instruction.ParallelCopyInstruction(); + insertAtEnd(block,pcopy.pCopyEnd); + } + return pcopy.pCopyEnd; + } + + private void insertAfterPhis(BasicBlock bb, Instruction newInst) { + assert bb.instructions.size() > 0; + int insertionPos = -1; + for (int pos = 0; pos < bb.instructions.size(); pos++) { + Instruction i = bb.instructions.get(pos); + if (i instanceof Instruction.Phi) { + insertionPos = pos+1; // After phi + } + else + break; + } + if (insertionPos < 0) { + throw new IllegalStateException(); + } + bb.add(insertionPos, newInst); + } + + private Instruction.ParallelCopyInstruction getParallelCopyAtBegin(BasicBlock block) { + PCopy pcopy = parallelCopies.get(block); + if (pcopy.pCopyBegin == null) { + pcopy.pCopyBegin = new Instruction.ParallelCopyInstruction(); + insertAfterPhis(block,pcopy.pCopyBegin); + } + return pcopy.pCopyBegin; + } + + /** + * Isolate phi nodes to make SSA conventional. + * This is Phase 1 as described in Engineering a Compiler 3rd Edition, p490. + * It is also described as method 1 by Sreedhar, and explained in detail by Boissinot. + */ + private void makeConventionalSSA() { + var blocks = function.getBlocks(); + for (BasicBlock block: blocks) { + var phis = block.phis(); + if (phis.isEmpty()) + continue; + for (var phi: phis) { + for (int j = 0; j < phi.numInputs(); j++) { + BasicBlock pred = block.predecessor(j); + var pCopyBEnd = getParallelCopyAtEnd(pred); + var oldInput = phi.input(j); + var newInput = function.registerPool.newTempReg(oldInput.type); + pCopyBEnd.addCopy(oldInput,new Operand.RegisterOperand(newInput)); + phi.replaceInput(j,newInput); + } + var oldPhiVar = phi.value(); + var newPhiVar = function.registerPool.newTempReg(oldPhiVar.type); + phi.replaceValue(newPhiVar); + var pCopyBBegin = getParallelCopyAtBegin(block); + pCopyBBegin.addCopy(new Operand.RegisterOperand(newPhiVar),new Operand.RegisterOperand(oldPhiVar)); + } + } + } + + /** + * Phase 2 in Engineering a Compiler + */ + private void removePhis() { + var blocks = function.getBlocks(); + for (BasicBlock block: blocks) { + var phis = block.phis(); + if (phis.isEmpty()) + continue; + // Insert copy in predecessor, since we are in CSSA, this is + // a simple assignment from phi input to phi var + for (var phi: phis) { + for (int j = 0; j < phi.numInputs(); j++) { + BasicBlock pred = block.predecessor(j); + var phiInput = phi.input(j); + var phiVar = phi.value(); + insertAtEnd(pred,new Instruction.Move(phiInput,new Operand.RegisterOperand(phiVar))); + } + } + block.instructions.removeIf(instruction -> instruction instanceof Instruction.Phi); + } + } + + /** + * Phase 3 in Engineering a Compiler. + */ + private void sequenceParallelCopies() { + for (var block: function.getBlocks()) { + var pcopy = parallelCopies.get(block); + if (pcopy.pCopyBegin != null) + sequenceParallelCopy(block,pcopy.pCopyBegin); + if (pcopy.pCopyEnd != null) + sequenceParallelCopy(block,pcopy.pCopyEnd); + } + } + + private void replaceInstruction(BasicBlock block, Instruction.ParallelCopyInstruction pcopy, ArrayList instructions) { + block.replaceInstruction(pcopy, instructions); + } + + private boolean isEqual(Operand op1, Operand op2) { + if (op1 instanceof Operand.RegisterOperand reg1 && op2 instanceof Operand.RegisterOperand reg2) + return reg1.reg.id == reg2.reg.id; + return false; + } + + // The parallel move algo below is from + // https://xavierleroy.org/publi/parallel-move.pdf + // Tilting at windmills with Coq: + // formal verification of a compilation algorithm + // for parallel moves + // Laurence Rideau, Bernard Paul Serpette, Xavier Leroy + + enum MoveStatus { + TO_MOVE, BEING_MOVED, MOVED + } + + static final class MoveCtx { + Operand[] src; + Operand[] dst; + MoveStatus[] status; + ArrayList copyInstructions; + + MoveCtx(Instruction.ParallelCopyInstruction pcopy) { + src = pcopy.sourceOperands.toArray(new Operand[0]); + dst = pcopy.destOperands.toArray(new Operand[0]); + copyInstructions = new ArrayList(); + status = new MoveStatus[src.length]; + Arrays.fill(status, MoveStatus.TO_MOVE); + } + } + + private void moveOne(MoveCtx ctx, int i) { + Operand[] src = ctx.src; + Operand[] dst = ctx.dst; + if (!isEqual(src[i], dst[i])) { + ctx.status[i] = MoveStatus.BEING_MOVED; + for (int j = 0; j < src.length; j++) { + if (isEqual(src[j],dst[i])) { + // cycle found + switch (ctx.status[j]) { + case TO_MOVE: + moveOne(ctx, j); + break; + case BEING_MOVED: + var temp = new Operand.RegisterOperand(function.registerPool.newTempReg(src[j].type)); + ctx.copyInstructions.add(new Instruction.Move(src[j], temp)); + src[j] = temp; + break; + case MOVED: + break; + } + } + } + ctx.copyInstructions.add(new Instruction.Move(src[i], dst[i])); + ctx.status[i] = MoveStatus.MOVED; + } + } + + private void sequenceParallelCopy(BasicBlock block, Instruction.ParallelCopyInstruction parallelCopyInstruction) { + var ctx = new MoveCtx(parallelCopyInstruction); + for (int i = 0; i < ctx.src.length; i++) + if (ctx.status[i] == MoveStatus.TO_MOVE) + moveOne(ctx,i); + replaceInstruction(block,parallelCopyInstruction,ctx.copyInstructions); + } + + + static final class PCopy { + BasicBlock block; + /** + * Parallel copy instruction after any Phi instructions + * in the block, null if not present + */ + Instruction.ParallelCopyInstruction pCopyBegin = null; + /** + * Parallel copy instruction at the end of a block, + * null if not present + */ + Instruction.ParallelCopyInstruction pCopyEnd = null; + + public PCopy(BasicBlock block) { + this.block = block; + } + } +} diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java new file mode 100644 index 0000000..c7a4c9b --- /dev/null +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/ExitSSABriggs.java @@ -0,0 +1,282 @@ +package com.compilerprogramming.ezlang.compiler; + +import java.util.*; + +/** + * Converts from SSA form to non-SSA form. + * Implementation is based on description in + * 'Practical Improvements to the Construction and Destruction + * of Static Single Assignment Form' by Preston Briggs. + * + * The JikesRVM LeaveSSA implements a version of the + * same algorithm. + */ +public class ExitSSABriggs { + + CompiledFunction function; + NameStack[] stacks; + DominatorTree tree; + + public ExitSSABriggs(CompiledFunction function, EnumSet options) { + this.function = function; + if (!function.isSSA) throw new IllegalStateException(); + function.livenessAnalysis(); + if (options.contains(Options.DUMP_SSA_LIVENESS)) function.dumpIR(true, "SSA Liveness Analysis"); + tree = new DominatorTree(function.entry); + if (options.contains(Options.DUMP_SSA_DOMTREE)) { + System.out.println("Pre SSA Dominator Tree"); + System.out.println(tree.generateDotOutput()); + } + initStack(); + insertCopies(function.entry); + removePhis(); + function.isSSA = false; + if (options.contains(Options.DUMP_POST_SSA_IR)) function.dumpIR(false, "After exiting SSA"); + } + + private void removePhis() { + for (BasicBlock block : tree.blocks) { + block.instructions.removeIf(instruction -> instruction instanceof Instruction.Phi); + } + } + + /* Algorithm for iterating through blocks to perform phi replacement */ + private void insertCopies(BasicBlock block) { + List pushed = new ArrayList<>(); + for (Instruction i: block.instructions) { + // replace all uses u with stacks[i] + replaceUses(i); + } + scheduleCopies(block, pushed); + for (BasicBlock c: block.dominatedChildren) { + insertCopies(c); + } + for (Integer name: pushed) { + stacks[name].pop(); + } + } + + /** + * replace all uses u with stacks[i] + */ + private void replaceUses(Instruction i) { + if (i instanceof Instruction.Phi) + // FIXME check this can never be valid + // tests 8/9 in TestInterpreter invoke on Phi but + // replacements are same as existing inputs + return; + var oldUses = i.uses(); + Register[] newUses = new Register[oldUses.size()]; + for (int u = 0; u < oldUses.size(); u++) { + Register use = oldUses.get(u); + if (!stacks[use.id].isEmpty()) + newUses[u] = stacks[use.id].top(); + else + newUses[u] = use; + } + i.replaceUses(newUses); + } + + static class CopyItem { + /** Phi input can be a register or a constant so we record the operand */ + final Operand src; + /** The phi destination */ + final Register dest; + /** The basic block where the phi was present */ + final BasicBlock destBlock; + boolean removed; + + public CopyItem(Operand src, Register dest, BasicBlock destBlock) { + this.src = src; + this.dest = dest; + this.destBlock = destBlock; + this.removed = false; + } + } + + private void scheduleCopies(BasicBlock block, List pushed) { + /* Pass 1 - Initialize data structures */ + /* In this pass we count the number of times a name is used by other phi-nodes */ + List copySet = new ArrayList<>(); + Map map = new HashMap<>(); + BitSet usedByAnother = new BitSet(function.registerPool.numRegisters()*2); + for (BasicBlock s: block.successors) { + int j = s.whichPred(block); + for (Instruction.Phi phi: s.phis()) { + Register dst = phi.value(); + Operand srcOperand = phi.input(j); // jth operand of phi node + if (srcOperand instanceof Operand.RegisterOperand srcRegisterOperand) { + Register src = srcRegisterOperand.reg; + map.put(src.id, src); + usedByAnother.set(src.id); + } + copySet.add(new CopyItem(srcOperand, dst, s)); + map.put(dst.id, dst); + } + } + + /* Pass 2: setup up the worklist of initial copies */ + /* In this pass we build a worklist of names that are not used in other phi nodes */ + List workList = new ArrayList<>(); + for (CopyItem copyItem: copySet) { + if (usedByAnother.get(copyItem.dest.id) != true) { + copyItem.removed = true; + workList.add(copyItem); + } + } + copySet.removeIf(copyItem -> copyItem.removed); + + /* Pass 3: iterate over the worklist, inserting copies */ + /* Copy operations whose destinations are not used by other copy operations can be scheduled immediately */ + /* Each time we insert a copy operation we add the source of that op to the worklist */ + while (!workList.isEmpty() || !copySet.isEmpty()) { + while (!workList.isEmpty()) { + final CopyItem copyItem = workList.remove(0); + final Operand src = copyItem.src; + final Register dest = copyItem.dest; + final BasicBlock destBlock = copyItem.destBlock; + /* Engineering a Compiler: We can avoid the lost copy + problem by checking the liveness of the target name + for each copy that we try to insert. When we discover + a copy target that is live, we must preserve the live + value in a temporary name and rewrite subsequent uses to + refer to the temporary name. + + This captures the cases when the result of a phi + in a control successor is live on exit of the current block. + This means that it is incorrect to simply insert a copy + of the destination in the current block. So we rename + the destination to a new temporary, and record the renaming + so that the dominator blocks get the new name. Comment adapted + from JikesRVM LeaveSSA + */ + if (block.liveOut.get(dest.id)) { + /* Insert a copy from dest to a new temp t at phi node defining dest */ + final Register t = addMoveToTempAfterPhi(destBlock, dest); + stacks[dest.id].push(t); // record the temp name + pushed.add(dest.id); + } + /* Insert a copy operation from map[src] to dest at end of BB */ + if (src instanceof Operand.RegisterOperand srcRegisterOperand) { + addMoveAtBBEnd(block, map.get(srcRegisterOperand.reg.id), dest); + map.put(srcRegisterOperand.reg.id, dest); + /* If src is the name of a dest in copySet add item to worklist */ + /* see comment on phi cycles below. */ + CopyItem item = isCycle(copySet, srcRegisterOperand.reg); + if (item != null) { + workList.add(item); + } + } + else if (src instanceof Operand.ConstantOperand srcConstantOperand) { + addMoveAtBBEnd(block, srcConstantOperand, dest); + } + } + /* Engineering a Compiler: To solve the swap problem + we can detect cases where phi functions reference the + targets of other phi functions in the same block. For each + cycle of references, it must insert a copy to a temporary + that breaks the cycle. Then we can schedule the copies to + respect the dependencies implied by the phi functions. + + An empty work list with work remaining in the copy set + implies a cycle in the dependencies amongst copies. To break + the cycle copy the destination of an arbitrary member of the + copy set to a temporary. This destination has therefore been + saved and can be safely overwritten. So then add the copy to the + work list. Comment adapted from JikesRVM LeaveSSA. + */ + if (!copySet.isEmpty()) { + CopyItem copyItem = copySet.remove(0); + /* Insert a copy from dst to new temp at the end of Block */ + Register t = addMoveToTempAtBBEnd(block, copyItem.dest); + map.put(copyItem.dest.id, t); + workList.add(copyItem); + } + } + } + + private void insertAtEnd(BasicBlock bb, Instruction i) { + assert bb.instructions.size() > 0; + // Last instruction is a branch - so new instruction will + // go before that + int pos = bb.instructions.size()-1; + bb.add(pos, i); + } + + private void insertAfterPhi(BasicBlock bb, Register phiDef, Instruction newInst) { + assert bb.instructions.size() > 0; + int insertionPos = -1; + for (int pos = 0; pos < bb.instructions.size(); pos++) { + Instruction i = bb.instructions.get(pos); + if (i instanceof Instruction.Phi phi) { + if (phi.value().id == phiDef.id) { + insertionPos = pos+1; // After phi + break; + } + } + } + if (insertionPos < 0) { + throw new IllegalStateException(); + } + bb.add(insertionPos, newInst); + } + + /* Insert a copy from dest to new temp at end of BB, and return temp */ + private Register addMoveToTempAtBBEnd(BasicBlock block, Register dest) { + var temp = function.registerPool.newTempReg(dest.name(), dest.type); + var inst = new Instruction.Move(new Operand.RegisterOperand(dest), new Operand.RegisterOperand(temp)); + insertAtEnd(block, inst); + return temp; + } + + /* If src is the name of a dest in copySet remove the item */ + private CopyItem isCycle(List copySet, Register src) { + for (int i = 0; i < copySet.size(); i++) { + CopyItem copyItem = copySet.get(i); + if (copyItem.dest.id == src.id) { + copySet.remove(i); + return copyItem; + } + } + return null; + } + + /* Insert a copy from src to dst at end of BB */ + private void addMoveAtBBEnd(BasicBlock block, Register src, Register dest) { + var inst = new Instruction.Move(new Operand.RegisterOperand(src), new Operand.RegisterOperand(dest)); + insertAtEnd(block, inst); + // If the copy instruction is followed by a cbr which uses the old var + // then we need to update the cbr instruction + // This is not specified in the Briggs paper but t + var brInst = block.instructions.getLast(); + if (brInst instanceof Instruction.ConditionalBranch cbr) { + cbr.replaceUse(src,dest); + } + } + /* Insert a copy from constant src to dst at end of BB */ + private void addMoveAtBBEnd(BasicBlock block, Operand.ConstantOperand src, Register dest) { + var inst = new Instruction.Move(src, new Operand.RegisterOperand(dest)); + insertAtEnd(block, inst); + } + /* Insert a copy dest to a new temp at phi node defining dest, return temp */ + private Register addMoveToTempAfterPhi(BasicBlock block, Register dest) { + var temp = function.registerPool.newTempReg(dest.name(), dest.type); + var inst = new Instruction.Move(new Operand.RegisterOperand(dest), new Operand.RegisterOperand(temp)); + insertAfterPhi(block, dest, inst); + return temp; + } + + private void initStack() { + stacks = new NameStack[function.registerPool.numRegisters()]; + for (int i = 0; i < stacks.length; i++) + stacks[i] = new NameStack(); + } + + static class NameStack { + List stack = new ArrayList<>(); + void push(Register r) { stack.add(r); } + Register top() { return stack.getLast(); } + void pop() { stack.removeLast(); } + boolean isEmpty() { return stack.isEmpty(); } + } +} diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java index 3ea0f44..b4b506a 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Instruction.java @@ -25,6 +25,7 @@ public abstract class Instruction { static final int I_ARRAY_LOAD = 13; static final int I_FIELD_GET = 14; static final int I_FIELD_SET = 15; + static final int I_PARALLEL_COPY = 16; public final int opcode; protected Operand.RegisterOperand def; @@ -477,5 +478,67 @@ public StringBuilder toStr(StringBuilder sb) { } } + /** + * The parallel copy instruction is only used temporarily to exit + * SSA form. It represents the parallel copy semantics of phi instructions. + */ + public static class ParallelCopyInstruction extends Instruction { + + List sourceOperands = new ArrayList<>(); + List destOperands = new ArrayList<>(); + + protected ParallelCopyInstruction() { + super(I_PARALLEL_COPY); + } + + @Override + public StringBuilder toStr(StringBuilder sb) { + sb.append("("); + for (int i = 0; i < destOperands.size(); i++) { + if (i > 0) + sb.append(","); + sb.append(destOperands.get(i)); + } + sb.append(") = ("); + for (int i = 0; i < sourceOperands.size(); i++) { + if (i > 0) + sb.append(","); + sb.append(sourceOperands.get(i)); + } + sb.append(")"); + return sb; + } + + public void addCopy(Operand sourceOperand, Operand.RegisterOperand destOperand) { + sourceOperands.add(sourceOperand); + destOperands.add(destOperand); + } + + @Override + public Register def() { + throw new UnsupportedOperationException(); + } + @Override + public void replaceDef(Register newDef) { + throw new UnsupportedOperationException(); + } + @Override + public boolean definesVar() { + return false; + } + @Override + public List uses() { + return Collections.emptyList(); + } + @Override + public void replaceUses(Register[] newUses) { + throw new UnsupportedOperationException(); + } + @Override + public boolean replaceUse(Register source, Register target) { + throw new UnsupportedOperationException(); + } + } + public abstract StringBuilder toStr(StringBuilder sb); } diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java index f6af988..7759cbc 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Operand.java @@ -3,6 +3,8 @@ import com.compilerprogramming.ezlang.types.Symbol; import com.compilerprogramming.ezlang.types.EZType; +import java.util.Objects; + public class Operand { EZType type; @@ -17,6 +19,19 @@ public ConstantOperand(long value, EZType type) { public String toString() { return String.valueOf(value); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConstantOperand that = (ConstantOperand) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } public static class NullConstantOperand extends Operand { @@ -46,6 +61,19 @@ public RegisterOperand copy(Register register) { public String toString() { return reg.name(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegisterOperand operand = (RegisterOperand) o; + return Objects.equals(reg, operand.reg); + } + + @Override + public int hashCode() { + return Objects.hashCode(reg); + } } public static class LocalRegisterOperand extends RegisterOperand { diff --git a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Options.java b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Options.java index b53c8cd..2af20d9 100644 --- a/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Options.java +++ b/optvm/src/main/java/com/compilerprogramming/ezlang/compiler/Options.java @@ -5,6 +5,8 @@ public enum Options { ISSA, // Incremental SSA OPTIMIZE, + SSA_DESTRUCTION_BRIGGS, + SSA_DESTRUCTION_BOISSINOT_NOCOALESCE, SCCP, CCP, // constant comparison propagation REGALLOC,