package org.openquark.cal.internal.javamodel; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.HashSet; import java.util.Set; import java.util.Stack; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicInterpreter; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.Frame; import org.objectweb.asm.tree.analysis.Value; /** * A class adapter that rewrites method bytecode so that variables * are nulled whenever they are statically determined to be dead. * This aids the JVM's bytecode interpreter's GC, since it will not * otherwise perform this analysis and so will consider these variables * to be live references. Once the JIT is invoked, the nulling * assignments are removed, which should result in minimal speed penalty. * * Overview of method used: * <ol> * <li>The control flow graph is built by the ASM library. * <li>Forward dataflow analysis is performed to determine when variables are * statically known to be null. (Part of this functionality is provided by * the ASM library.) * <li>Liveness analysis is performed to determine when variables are statically * known to be dead. * <li>Non-nulling assignments to dead variables are removed. * <li>Backwards dataflow analysis is performed from allocations to determine when * nulling a variable would be useful; that is, when nulling it might cause it * to be null during an allocation where it would otherwise be non-null. * <li>Nullings are inserted where live variables become dead and we have determined * that nulling the variable at that point is potentially useful. * </ol> * * For a detailed description of the method, see the comments in visitEnd. * * @author Malcolm Sharpe */ public class NullingClassAdapter extends ClassAdapter { /** * A flag to control the nulling of the 'this' pointer. */ public static boolean nullThis = true; /** * Constructs a NullingClassAdapter from the given ClassVisitor. * @param cv the ClassVisitor to wrap. */ public NullingClassAdapter(ClassVisitor cv) { super(cv); } /** * Visit a class header, remembering its internal name for later use. */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); internalName = name; } /** The internal name of the class currently being visited. */ private String internalName; /** * Visit a method, converting it to ASM's tree format, and insert nullings. */ @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return new MethodNode(access, name, desc, signature, exceptions) { @Override public void visitEnd() { // Build the control flow graph and determine when variables are known to be // null. // Analyzer does not initialize some frames before newControlFlowExceptionEdge // is called, so remember the edges for later use. final ArrayList<Integer> edgeInsns = new ArrayList<Integer>(); final ArrayList<Integer> edgeSuccessors = new ArrayList<Integer>(); Analyzer a = new Analyzer(new IsNullInterpreter()) { @Override protected Frame newFrame(int nLocals, int nStack) { return new Node(nLocals, nStack); } @Override protected Frame newFrame(Frame src) { return new Node(src); } @Override protected void newControlFlowEdge(int insn, int successor) { edgeInsns.add(insn); edgeSuccessors.add(successor); } @Override protected boolean newControlFlowExceptionEdge(int insn, int successor) { newControlFlowEdge(insn, successor); return true; } }; Frame[] frames; try { frames = a.analyze(internalName, this); } catch (AnalyzerException e) { throw new RuntimeException(e); } assert frames.length == instructions.size(); // Use stored edges to build the graph. for (int i = 0; i < edgeInsns.size(); i++) { int insn = edgeInsns.get(i); int successor = edgeSuccessors.get(i); Node n = (Node)a.getFrames()[insn]; Node m = (Node)a.getFrames()[successor]; n.addSucc(m); m.addPred(n); } edgeInsns.clear(); edgeSuccessors.clear(); // Retrieve nodes from the node array. Node[] nodes = new Node[frames.length]; for (int i = 0; i < nodes.length; i++) { nodes[i] = (Node)frames[i]; // Tell the node which index it has. if (nodes[i] != null) nodes[i].setIndex(i); } // Initialize the 'def', 'use', and 'alloc' sets for each node. AbstractInsnNode insn = instructions.getFirst(); for (int i = 0; i < nodes.length; i++) { if (nodes[i] != null) nodes[i].initInfoSets(insn); insn = insn.getNext(); } assert insn == null; // Iterate liveness analysis until sets are unchanged. // To reduce the number of iterations, we use depth-first // search to reverse topologically sort the nodes. Node[] updateOrder = getReversePseudoTopologicalOrder(nodes); dirty = true; while (dirty) { dirty = false; for (int i = 0; i < updateOrder.length; i++) { updateOrder[i].updateLiveness(); } } // Replace dead non-nulling assignments with pops. // Note that this does not affect liveness information. insn = instructions.getFirst(); for (int i = 0; i < nodes.length; i++) { AbstractInsnNode next = insn.getNext(); if (nodes[i] != null) nodes[i].removeDeadAssignments(instructions, insn); insn = next; } assert insn == null; // Find useful locations for nulling. // // To do this, we perform backward dataflow analysis from allocations. This is // similar to liveness analysis, but not exactly the same, since a potential // allocation makes _all_ variables unsafe-in at that node, unlike the corresponding // 'use' in liveness analysis. // // Definitions: // - The alloc set of a node contains all local variables if executing that node // could cause an allocation, and otherwise it is empty. // - A variable is unsafe on an edge if there is a directed path from that edge // to an alloc that does not go through any def of that variable. A variable is // unsafe-in at a node if it is unsafe on any of the in-edges of that node; it // is unsafe-out at a node if it is unsafe-out on any of the out-edges of the // node. // (TODO: We would like a def that's different from liveness analysis's def. The // reason is that an local object reference might be overwritten with, say, an // integer local, once the object reference goes out of scope. In this case, the // overwriting does our job for us. However, this situation seems unlikely enough // that it is ignored for the moment.) // // Dataflow equations: // in[n] = alloc[n] U (out[n] - def[n]) // out[n] = U for s in succ[n] of in[s] dirty = true; while (dirty) { dirty = false; for (int i = 0; i < updateOrder.length; i++) { updateOrder[i].updateSafety(); } } // Null out locals where desirable. // // For any given local object reference x, the properties we want are: // 1. At any node where x is not live-in and an allocation could occur during // the execution of the node, x is null during the execution of the node. // 2. x must not be nulled while it is live. // 3. When possible, we should prefer to insert few instructions. // // Safety information is solely used to get better results for property 3. // // To achieve property 1 while not violating properties 2 & 3, we develop a method // by induction on the number of executed nodes in a given run of the program. The // inductive hypothesis is similar to property 1: // (I.H.) At any node where x is not live-in and x is unsafe-in, x is null during the // execution of the node. // Note that any node satisfying this inductive hypothesis also satisfies property 1, // since x is unsafe-in at any node that could perform an allocation. // // We prefer to insert nullings only before the execution of nodes, since inserting // them after the execution of nodes is difficult in some cases, such as jumps. // (TODO: Determine whether a code size advantage could be gained by sometimes inserting // nullings after the execution of nodes.) // // Note that we have already performed this pre-processing: for each node v that assigns // a possibly non-null value to x, if x is not live-out at v, we replaced v's instruction // with a pop instruction. This makes no difference to the meaning of the program, since // the stack effect is the same and the stored value is never read. (TODO: However, this // does affect debugging, since the variable will appear as null when it would not otherwise.) // // (Basis) // For the first executed node, nodes[0], arguments are the only locals which // might be non-null. Therefore, before the execution of this node, we null out // x if it is an argument that is not live-in and is unsafe-in, so the I.H. holds. // (Inductive Step) // Suppose we have executed at least one node's instruction. Let v be the next // node to be executed. If x is live-in or is not unsafe-in at v, then the I.H. holds // trivially. Otherwise, x is not live-in and is unsafe-in at v. Let u be the previously // executed node. // - If x is live-out at u, then it might be non-null, so we null x before // executing v. // - If x is live-in at u, then it might have been non-null before the execution of // u, and since we only null before the execution of nodes, it might still be // non-null after u is executed. Thus, we null x before executing v. // - If neither of the previous cases holds, then x is neither live-in nor live-out at u. // > If x is not unsafe-in at u, note that x being unsafe-in at v implies x is unsafe-out // at u. Thus, x must be a member of u's def set. Since x is not live-out at u, this // must be a nulling assignment to x, since we removed all other dead assignments. // > Otherwise, x is unsafe-in at u. By the I.H., x was null during the execution of u. // Since we have removed non-nulling assignments to x where x is not live-out, x remained // null after the execution of u. // In either subcase, x is null before the execution of v, so we do not need to set it to // null. // Thus, property 1 holds for v. // // To apply this method statically, we null x before a node v if any statically-possible // execution path requires it. i.e. If x is not live-in and is unsafe-in at v, we null x // before v if any predecessor of v has x live-in or live-out. // // There is a practical complication introduced by labels, since they are considered nodes // by the ASM library. Since labels are the target of jumps, it is not possible to null x // immediately before the execution of the label. However, since labels do nothing during // their execution, it is safe to null x immediately _after_ the execution of the label. // Calculate which object reference locals were live before each node. BitSet objectArguments = getObjectArguments(); for (int i = 0; i < nodes.length; i++) { if (nodes[i] != null) { nodes[i].calculatePreviouslyLive(objectArguments); } } // Insert nullings. insn = instructions.getFirst(); for (int i = 0; i < nodes.length; i++) { if (nodes[i] != null) insn = nodes[i].insertNullings(instructions, insn, isInstanceMethod()); else insn = insn.getNext(); } assert insn == null; accept(cv); } /** * Returns a set containing all the local variables that are both * object references and arguments. */ private BitSet getObjectArguments() { Type[] argumentTypes = Type.getArgumentTypes(desc); BitSet objectArguments = new BitSet(); int stackOffset = 0; // Implicit 'this' argument. if (isInstanceMethod()) { objectArguments.set(0); stackOffset++; } // Explicit arguments. for (int i = 0; i < argumentTypes.length; i++) { int sort = argumentTypes[i].getSort(); if (sort == Type.ARRAY || sort == Type.OBJECT) { objectArguments.set(stackOffset); } stackOffset += argumentTypes[i].getSize(); } return objectArguments; } /** * Returns true if this is an instance method. i.e. the 0'th local * is the this pointer. Otherwise returns false. */ private boolean isInstanceMethod() { return 0 == (access & Opcodes.ACC_STATIC); } }; } /** * Print debugging information about each node in the control graph. * @param nodes the control graph nodes. */ @SuppressWarnings("unused") private static void dumpNodes(Node[] nodes) { for (int i = 0; i < nodes.length; i++) { System.err.println(nodes[i]); } } /** True if some set has changed in this iteration of liveness. False otherwise. */ private boolean dirty; /** * Sort the nodes in reverse pseudo-topological order using depth-first * search, to determine a good update order for reverse data flow analyses. * Unreachable nodes are ignored. * * @param nodes the nodes as they are ordered in the method. * @return the reachable nodes in reverse pseudo-topological order. */ private static Node[] getReversePseudoTopologicalOrder(Node[] nodes) { ArrayList<Node> result = new ArrayList<Node>(); // Depth-first search, written iteratively so as not to overrun the Java // stack. // By adding the nodes in order of finishing time, we get a reverse // pseudo-topological order. It is only pseudo-topological since the // control graph may be cyclic. Set<Node> visited = new HashSet<Node>(); Stack<Node> nodeStack = new Stack<Node>(); Stack<NodeCons> succIteratorStack = new Stack<NodeCons>(); nodeStack.push(nodes[0]); succIteratorStack.push(nodes[0].succ); while (!nodeStack.isEmpty()) { if (succIteratorStack.peek() != null) { Node n = succIteratorStack.peek().node; succIteratorStack.push(succIteratorStack.pop().next); if (!visited.contains(n)) { visited.add(n); nodeStack.push(n); succIteratorStack.push(n.succ); } } else { result.add(nodeStack.pop()); succIteratorStack.pop(); } } // Avoid ArrayList.toArray since it uses slow reflection. Node[] resultArray = new Node[result.size()]; for (int i = 0; i < resultArray.length; i++) { resultArray[i] = result.get(i); } return resultArray; } /** * A node in the control flow graph. * * @author Malcolm Sharpe */ private class Node extends Frame { /** The successors of this node. */ private NodeCons succ = null; /** The predecessors of this node. */ private NodeCons pred = null; public Node(int nLocals, int nStack) { super(nLocals, nStack); } public Node(Frame src) { super(src); } /** Add a successor of this node. */ public final void addSucc(Node successor) { succ = new NodeCons(successor, succ); } /** Add a predecessor of this node. */ public final void addPred(Node predecessor) { pred = new NodeCons(predecessor, pred); } /** Those local variables that are live on at least one in-edge. */ private BitSet liveIn = new BitSet(); /** Those local variables that are live on at least one out-edge. */ private BitSet liveOut = new BitSet(); /** Those local variables that are assigned to by this node's instruction. */ private BitSet def = new BitSet(); /** Those local variables that are read by this node's instruction. */ private BitSet use = new BitSet(); /** * Those local variables that, if non-live, should be null here because an * an allocation could occur. */ private BitSet alloc = new BitSet(); /** Those variables that are unsafe on at least one in-edge. */ private BitSet unsafeIn = new BitSet(); /** Those variables that are unsafe on at least one out-edge. */ private BitSet unsafeOut = new BitSet(); /** Initialize def, use, and alloc sets based on the instruction corresponding to this node. */ public final void initInfoSets(AbstractInsnNode insn) { if (AbstractInsnNode.VAR_INSN == insn.getType()) { // Detect reads and writes to local object references. VarInsnNode var = (VarInsnNode)insn; // We care only about object references and return addresses. (The latter only // because it uses the astore instruction.) // Read opcodes: // aload, aload_n (for 0 <= n <= 3), ret (for return addresses) // Write opcodes: // astore, astore_n (for 0 <= n <= 3) // ASM handles the *_n forms automatically. switch (var.getOpcode()) { case Opcodes.ALOAD: case Opcodes.RET: use.set(var.var); break; case Opcodes.ASTORE: def.set(var.var); break; } } else { // Detect potential allocations, caused by explicit allocations or occurring in method invocations. switch (insn.getOpcode()) { case Opcodes.ANEWARRAY: case Opcodes.MULTIANEWARRAY: case Opcodes.NEW: case Opcodes.NEWARRAY: case Opcodes.INVOKEINTERFACE: case Opcodes.INVOKESPECIAL: case Opcodes.INVOKESTATIC: case Opcodes.INVOKEVIRTUAL: alloc.set(0, getLocals()); } } } /** Update the 'liveIn' and 'liveOut' sets of this node. */ public void updateLiveness() { int oldOutCardinality = liveOut.cardinality(); for (NodeCons cons = succ; cons != null; cons = cons.next) { Node n = cons.node; liveOut.or(n.liveIn); } if (liveOut.cardinality() != oldOutCardinality) dirty = true; liveIn.or(liveOut); liveIn.andNot(def); liveIn.or(use); } /** Update the 'unsafeIn' and 'unsafeOut' sets of this node. */ public void updateSafety() { int oldOutCardinality = unsafeOut.cardinality(); for (NodeCons cons = succ; cons != null; cons = cons.next) { Node n = cons.node; unsafeOut.or(n.unsafeIn); } if (unsafeOut.cardinality() != oldOutCardinality) dirty = true; unsafeIn.or(unsafeOut); unsafeIn.andNot(def); unsafeIn.or(alloc); } /** The index of this node in the array of nodes. */ private int index = -1; /** Set the index of this node in the array of nodes. */ public void setIndex(int index) { this.index = index; } /** * Print this node in a format that is helpful for debugging. */ public String toString() { // Get the ordered indices of the successors of this node. int[] succIndices = new int[nodeConsLength(succ)]; int i = 0; for (NodeCons cons = succ; cons != null; cons = cons.next) { Node n = cons.node; succIndices[i++] = n.index; } Arrays.sort(succIndices); // Format the node information. StringBuilder sb = new StringBuilder(); sb.append("Node "); sb.append(index); sb.append(" ->"); for (int index : succIndices) { sb.append(" "); sb.append(index); } sb.append(";"); for (int j = 0; j < getStackSize(); j++) { sb.append(" "); Value v = getStack(j); sb.append(v == IsNullInterpreter.NULL ? "null" : v); } return sb.toString(); } /** * Those object reference local variables that are live-in or live-out * in any predecessor node, or in the case of the first node, all object * reference arguments. */ private BitSet previouslyLive = new BitSet(); /** * Initialize the previouslyLive set. */ public final void calculatePreviouslyLive(BitSet objectArguments) { if (index == 0) { // This is the first node, so consider object reference arguments // previously live. previouslyLive.or(objectArguments); } for (NodeCons cons = pred; cons != null; cons = cons.next) { Node n = cons.node; previouslyLive.or(n.liveIn); previouslyLive.or(n.liveOut); } } /** * Replace non-nulling assignments to dead local references with pops, since these * otherwise cause trouble for our rewriter. Nulling assignments are allowed since * they spare us from inserting our own nullings. * * The 'def' set is updated for use by later analyses. */ public final void removeDeadAssignments(InsnList instructions, AbstractInsnNode insn) { if (insn.getOpcode() != Opcodes.ASTORE) return; VarInsnNode varInsn = (VarInsnNode)insn; Value topValue = getStack(getStackSize() - 1); if (!liveOut.get(varInsn.var) && topValue != IsNullInterpreter.NULL) { InsnNode pop = new InsnNode(Opcodes.POP); instructions.insert(insn, pop); instructions.remove(insn); def.clear(); } } /** * Insert nullings if required. */ public final AbstractInsnNode insertNullings(InsnList instructions, AbstractInsnNode insn, boolean isInstanceMethod) { assert insn != null; // Calculate who we need to null. // Object.clone() is slow, so avoid it. BitSet toNull = new BitSet(); toNull.or(previouslyLive); toNull.andNot(liveIn); toNull.and(unsafeIn); if (!nullThis && isInstanceMethod) { toNull.clear(0); } AbstractInsnNode next = insn.getNext(); if (toNull.cardinality() > 0) { // Construct the nulling instructions. InsnList nullingInsns = new InsnList(); for (int i = 0; i < toNull.length(); i++) { if (toNull.get(i)) { AbstractInsnNode pushNull = new InsnNode(Opcodes.ACONST_NULL); AbstractInsnNode store = new VarInsnNode(Opcodes.ASTORE, i); nullingInsns.add(pushNull); nullingInsns.add(store); } } if (AbstractInsnNode.LABEL == insn.getType()) { instructions.insert(insn, nullingInsns); } else { instructions.insertBefore(insn, nullingInsns); } } return next; } } /** * A pair of a Node and another NodeCons. Used for lightweight adjacency lists. * * @author Malcolm Sharpe */ private static final class NodeCons { public Node node; public NodeCons next; public NodeCons(Node node, NodeCons next) { this.node = node; this.next = next; } } /** * Finds the length of a linked list of nodes. * @param cons the linked list of nodes. * @return the length of the list. */ private static int nodeConsLength(NodeCons cons) { int result = 0; while (cons != null) { result++; cons = cons.next; } return result; } } /** * A BasicInterpreter that determines which values are known to be * null, based off the example in the ASM User Guide. * * @author Malcolm Sharpe */ class IsNullInterpreter extends BasicInterpreter { /** The value of object references guaranteed to be null. */ public final static BasicValue NULL = new BasicValue(Type.getObjectType("java/lang/Object")); @Override public Value newOperation(AbstractInsnNode insn) { // Only aconst_null is certain to create a null value. if (insn.getOpcode() == Opcodes.ACONST_NULL) { return NULL; } else { return super.newOperation(insn); } } @Override public Value unaryOperation(AbstractInsnNode insn, Value value) throws AnalyzerException { // Eclipse's Java compiler emits a CHECKCAST instruction when assigning null // to a variable. BasicInterpreter does not propagate our null information // through, so we must do this ourselves. if (insn.getOpcode() == Opcodes.CHECKCAST && NULL == value) { return NULL; } else { return super.unaryOperation(insn, value); } } @Override public Value merge(Value v, Value w) { // If both values are null, then their merged value is surely null, // but otherwise we do not know. if (NULL == v && NULL == w) { return NULL; } else { return super.merge(v, w); } } }