blob: b631209e8d17268d09a3e07782d78c93462d0bfc [file] [log] [blame]
/*
* 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 <wtf/CommaPrinter.h>
#include <wtf/HashSet.h>
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<PhaseMode mode>
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<InPlaceAbstractState>(m_graph);
m_interpreter = std::make_unique<AbstractInterpreter<InPlaceAbstractState>>(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<BlockMap<HashSet<Node*>>>(m_graph);
m_stateAtTail = std::make_unique<BlockMap<HashSet<Node*>>>(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<InPlaceAbstractState> m_state;
std::unique_ptr<AbstractInterpreter<InPlaceAbstractState>> m_interpreter;
std::unique_ptr<BlockMap<HashSet<Node*>>> m_stateAtHead;
std::unique_ptr<BlockMap<HashSet<Node*>>> m_stateAtTail;
bool m_isConverged;
};
} // anonymous namespace
bool performFastStoreBarrierInsertion(Graph& graph)
{
return runPhase<StoreBarrierInsertionPhase<PhaseMode::Fast>>(graph);
}
bool performGlobalStoreBarrierInsertion(Graph& graph)
{
return runPhase<StoreBarrierInsertionPhase<PhaseMode::Global>>(graph);
}
} } // namespace JSC::DFG
#endif // ENABLE(DFG_JIT)