Package org.openquark.cal.internal.javamodel

Source Code of org.openquark.cal.internal.javamodel.NullingClassAdapter$Node

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);
        }
    }
   
}
TOP

Related Classes of org.openquark.cal.internal.javamodel.NullingClassAdapter$Node

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.