/* * Copyright (C) 2015-2016 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "DFGStoreBarrierInsertionPhase.h" #if ENABLE(DFG_JIT) #include "DFGAbstractInterpreterInlines.h" #include "DFGBlockMapInlines.h" #include "DFGDoesGC.h" #include "DFGGraph.h" #include "DFGInPlaceAbstractState.h" #include "DFGInsertionSet.h" #include "DFGPhase.h" #include "JSCInlines.h" #include #include namespace JSC { namespace DFG { namespace { bool verbose = false; enum class PhaseMode { // Does only a local analysis for store barrier insertion and assumes that pointers live // from predecessor blocks may need barriers. Assumes CPS conventions. Does not use AI for // eliminating store barriers, but does a best effort to eliminate barriers when you're // storing a non-cell value by using Node::result() and by looking at constants. The local // analysis is based on GC epochs, so it will eliminate a lot of locally redundant barriers. Fast, // Does a global analysis for store barrier insertion. Reuses the GC-epoch-based analysis // used by Fast, but adds a conservative merge rule for propagating information from one // block to the next. This will ensure for example that if a value V coming from multiple // predecessors in B didn't need any more barriers at the end of each predecessor (either // because it was the last allocated object in that predecessor or because it just had a // barrier executed), then until we hit another GC point in B, we won't need another barrier // on V. Uses AI for eliminating barriers when we know that the value being stored is not a // cell. Assumes SSA conventions. Global }; template class StoreBarrierInsertionPhase : public Phase { public: StoreBarrierInsertionPhase(Graph& graph) : Phase(graph, mode == PhaseMode::Fast ? "fast store barrier insertion" : "global store barrier insertion") , m_insertionSet(graph) { } bool run() { if (verbose) { dataLog("Starting store barrier insertion:\n"); m_graph.dump(); } switch (mode) { case PhaseMode::Fast: { DFG_ASSERT(m_graph, nullptr, m_graph.m_form != SSA); m_graph.clearEpochs(); for (BasicBlock* block : m_graph.blocksInNaturalOrder()) handleBlock(block); return true; } case PhaseMode::Global: { DFG_ASSERT(m_graph, nullptr, m_graph.m_form == SSA); m_state = std::make_unique(m_graph); m_interpreter = std::make_unique>(m_graph, *m_state); m_isConverged = false; // First run the analysis. Inside basic blocks we use an epoch-based analysis that // is very precise. At block boundaries, we just propagate which nodes may need a // barrier. This gives us a very nice bottom->top fixpoint: we start out assuming // that no node needs any barriers at block boundaries, and then we converge // towards believing that all nodes need barriers. "Needing a barrier" is like // saying that the node is in a past epoch. "Not needing a barrier" is like saying // that the node is in the current epoch. m_stateAtHead = std::make_unique>>(m_graph); m_stateAtTail = std::make_unique>>(m_graph); BlockList postOrder = m_graph.blocksInPostOrder(); bool changed = true; while (changed) { changed = false; // Intentional backwards loop because we are using RPO. for (unsigned blockIndex = postOrder.size(); blockIndex--;) { BasicBlock* block = postOrder[blockIndex]; if (!handleBlock(block)) { // If the block didn't finish, then it cannot affect the fixpoint. continue; } // Construct the state-at-tail based on the epochs of live nodes and the // current epoch. We grow state-at-tail monotonically to ensure convergence. bool thisBlockChanged = false; for (Node* node : block->ssa->liveAtTail) { if (node->epoch() != m_currentEpoch) { // If the node is older than the current epoch, then we may need to // run a barrier on it in the future. So, add it to the state. thisBlockChanged |= m_stateAtTail->at(block).add(node).isNewEntry; } } if (!thisBlockChanged) { // This iteration didn't learn anything new about this block. continue; } // Changed things. Make sure that we loop one more time. changed = true; for (BasicBlock* successor : block->successors()) { for (Node* node : m_stateAtTail->at(block)) m_stateAtHead->at(successor).add(node); } } } // Tell handleBlock() that it's time to actually insert barriers for real. m_isConverged = true; for (BasicBlock* block : m_graph.blocksInNaturalOrder()) handleBlock(block); return true; } } RELEASE_ASSERT_NOT_REACHED(); return false; } private: bool handleBlock(BasicBlock* block) { if (verbose) { dataLog("Dealing with block ", pointerDump(block), "\n"); if (reallyInsertBarriers()) dataLog(" Really inserting barriers.\n"); } m_currentEpoch = Epoch::first(); if (mode == PhaseMode::Global) { if (!block->cfaHasVisited) return false; m_state->beginBasicBlock(block); for (Node* node : block->ssa->liveAtHead) { if (m_stateAtHead->at(block).contains(node)) { // If previous blocks tell us that this node may need a barrier in the // future, then put it in the ancient primordial epoch. This forces us to // emit a barrier on any possibly-cell store, regardless of the epoch of the // stored value. node->setEpoch(Epoch()); } else { // If previous blocks aren't requiring us to run a barrier on this node, // then put it in the current epoch. This means that we will skip barriers // on this node so long as we don't allocate. It also means that we won't // run barriers on stores to on one such node into another such node. That's // fine, because nodes would be excluded from the state set if at the tails // of all predecessors they always had the current epoch. node->setEpoch(m_currentEpoch); } } } bool result = true; for (m_nodeIndex = 0; m_nodeIndex < block->size(); ++m_nodeIndex) { m_node = block->at(m_nodeIndex); if (verbose) { dataLog( " ", m_currentEpoch, ": Looking at node ", m_node, " with children: "); CommaPrinter comma; m_graph.doToChildren( m_node, [&] (Edge edge) { dataLog(comma, edge, " (", edge->epoch(), ")"); }); dataLog("\n"); } if (mode == PhaseMode::Global) { // Execute edges separately because we don't want to insert barriers if the // operation doing the store does a check that ensures that the child is not // a cell. m_interpreter->startExecuting(); m_interpreter->executeEdges(m_node); } switch (m_node->op()) { case PutByValDirect: case PutByVal: case PutByValAlias: { switch (m_node->arrayMode().modeForPut().type()) { case Array::Contiguous: case Array::ArrayStorage: case Array::SlowPutArrayStorage: { Edge child1 = m_graph.varArgChild(m_node, 0); Edge child3 = m_graph.varArgChild(m_node, 2); considerBarrier(child1, child3); break; } default: break; } break; } case ArrayPush: { switch (m_node->arrayMode().type()) { case Array::Contiguous: case Array::ArrayStorage: considerBarrier(m_node->child1(), m_node->child2()); break; default: break; } break; } case PutById: case PutByIdFlush: case PutByIdDirect: case PutStructure: { considerBarrier(m_node->child1()); break; } case RecordRegExpCachedResult: { considerBarrier(m_graph.varArgChild(m_node, 0)); break; } case PutClosureVar: case PutToArguments: case SetRegExpObjectLastIndex: case MultiPutByOffset: { considerBarrier(m_node->child1(), m_node->child2()); break; } case PutByOffset: { considerBarrier(m_node->child2(), m_node->child3()); break; } case PutGlobalVariable: { considerBarrier(m_node->child1(), m_node->child2()); break; } case SetFunctionName: { considerBarrier(m_node->child1(), m_node->child2()); break; } default: break; } if (doesGC(m_graph, m_node)) m_currentEpoch.bump(); switch (m_node->op()) { case NewObject: case NewArray: case NewArrayWithSize: case NewArrayBuffer: case NewTypedArray: case NewRegexp: case MaterializeNewObject: case MaterializeCreateActivation: case NewStringObject: case MakeRope: case CreateActivation: case CreateDirectArguments: case CreateScopedArguments: case CreateClonedArguments: case NewFunction: case NewGeneratorFunction: // Nodes that allocate get to set their epoch because for those nodes we know // that they will be the newest object in the heap. m_node->setEpoch(m_currentEpoch); break; case AllocatePropertyStorage: case ReallocatePropertyStorage: // These allocate but then run their own barrier. insertBarrier(m_nodeIndex + 1, Edge(m_node->child1().node(), KnownCellUse)); m_node->setEpoch(Epoch()); break; case Upsilon: m_node->phi()->setEpoch(m_node->epoch()); m_node->setEpoch(Epoch()); break; default: // For nodes that aren't guaranteed to allocate, we say that their return value // (if there is one) could be arbitrarily old. m_node->setEpoch(Epoch()); break; } if (verbose) { dataLog( " ", m_currentEpoch, ": Done with node ", m_node, " (", m_node->epoch(), ") with children: "); CommaPrinter comma; m_graph.doToChildren( m_node, [&] (Edge edge) { dataLog(comma, edge, " (", edge->epoch(), ")"); }); dataLog("\n"); } if (mode == PhaseMode::Global) { if (!m_interpreter->executeEffects(m_nodeIndex, m_node)) { result = false; break; } } } if (mode == PhaseMode::Global) m_state->reset(); if (reallyInsertBarriers()) m_insertionSet.execute(block); return result; } void considerBarrier(Edge base, Edge child) { if (verbose) dataLog(" Considering adding barrier ", base, " => ", child, "\n"); // We don't need a store barrier if the child is guaranteed to not be a cell. switch (mode) { case PhaseMode::Fast: { // Don't try too hard because it's too expensive to run AI. if (child->hasConstant()) { if (!child->asJSValue().isCell()) { if (verbose) dataLog(" Rejecting because of constant type.\n"); return; } } else { switch (child->result()) { case NodeResultNumber: case NodeResultDouble: case NodeResultInt32: case NodeResultInt52: case NodeResultBoolean: if (verbose) dataLog(" Rejecting because of result type.\n"); return; default: break; } } break; } case PhaseMode::Global: { // Go into rage mode to eliminate any chance of a barrier with a non-cell child. We // can afford to keep around AI in Global mode. if (!m_interpreter->needsTypeCheck(child, ~SpecCell)) { if (verbose) dataLog(" Rejecting because of AI type.\n"); return; } break; } } // We don't need a store barrier if the base is at least as new as the child. For // example this won't need a barrier: // // var o = {} // var p = {} // p.f = o // // This is stronger than the currentEpoch rule in considerBarrier(Edge), because it will // also eliminate barriers in cases like this: // // var o = {} // o.epoch = 1, currentEpoch = 1 // var p = {} // o.epoch = 1, p.epoch = 2, currentEpoch = 2 // var q = {} // o.epoch = 1, p.epoch = 2, q.epoch = 3, currentEpoch = 3 // p.f = o // p.epoch >= o.epoch // // This relationship works because if it holds then we are in one of the following // scenarios. Note that we don't know *which* of these scenarios we are in, but it's // one of them (though without loss of generality, you can replace "a GC happened" with // "many GCs happened"). // // 1) There is no GC between the allocation/last-barrier of base, child and now. Then // we definitely don't need a barrier. // // 2) There was a GC after child was allocated but before base was allocated. Then we // don't need a barrier, because base is still a new object. // // 3) There was a GC after both child and base were allocated. Then they are both old. // We don't need barriers on stores of old into old. Note that in this case it // doesn't matter if there was also a GC between the allocation of child and base. // // Note that barriers will lift an object into the current epoch. This is sort of weird. // It means that later if you store that object into some other object, and that other // object was previously newer object, you'll think that you need a barrier. We could // avoid this by tracking allocation epoch and barrier epoch separately. For now I think // that this would be overkill. But this does mean that there are the following // possibilities when this relationship holds: // // 4) Base is allocated first. A GC happens and base becomes old. Then we allocate // child. (Note that alternatively the GC could happen during the allocation of // child.) Then we run a barrier on base. Base will appear to be as new as child // (same epoch). At this point, we don't need another barrier on base. // // 5) Base is allocated first. Then we allocate child. Then we run a GC. Then we run a // barrier on base. Base will appear newer than child. We don't need a barrier // because both objects are old. // // Something we watch out for here is that the null epoch is a catch-all for objects // allocated before we did any epoch tracking. Two objects being in the null epoch // means that we don't know their epoch relationship. if (!!base->epoch() && base->epoch() >= child->epoch()) { if (verbose) dataLog(" Rejecting because of epoch ordering.\n"); return; } considerBarrier(base); } void considerBarrier(Edge base) { if (verbose) dataLog(" Considering adding barrier on ", base, "\n"); // We don't need a store barrier if the epoch of the base is identical to the current // epoch. That means that we either just allocated the object and so it's guaranteed to // be in newgen, or we just ran a barrier on it so it's guaranteed to be remembered // already. if (base->epoch() == m_currentEpoch) { if (verbose) dataLog(" Rejecting because it's in the current epoch.\n"); return; } if (verbose) dataLog(" Inserting barrier.\n"); insertBarrier(m_nodeIndex, base); } void insertBarrier(unsigned nodeIndex, Edge base, bool exitOK = true) { // If we're in global mode, we should only insert the barriers once we have converged. if (!reallyInsertBarriers()) return; // FIXME: We could support StoreBarrier(UntypedUse:). That would be sort of cool. // But right now we don't need it. // If the original edge was unchecked, we should also not have a check. We may be in a context // where checks are not allowed. If we ever did have to insert a barrier at an ExitInvalid // context and that barrier needed a check, then we could make that work by hoisting the check. // That doesn't happen right now. if (base.useKind() != KnownCellUse) { DFG_ASSERT(m_graph, m_node, m_node->origin.exitOK); base.setUseKind(CellUse); } m_insertionSet.insertNode( nodeIndex, SpecNone, StoreBarrier, m_node->origin.takeValidExit(exitOK), base); base->setEpoch(m_currentEpoch); } bool reallyInsertBarriers() { return mode == PhaseMode::Fast || m_isConverged; } InsertionSet m_insertionSet; Epoch m_currentEpoch; unsigned m_nodeIndex; Node* m_node; // Things we only use in Global mode. std::unique_ptr m_state; std::unique_ptr> m_interpreter; std::unique_ptr>> m_stateAtHead; std::unique_ptr>> m_stateAtTail; bool m_isConverged; }; } // anonymous namespace bool performFastStoreBarrierInsertion(Graph& graph) { return runPhase>(graph); } bool performGlobalStoreBarrierInsertion(Graph& graph) { return runPhase>(graph); } } } // namespace JSC::DFG #endif // ENABLE(DFG_JIT)