blob: 46a76dfbc1d2870e5401f4cb0c284b01ac1e2771 [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "jit/ScalarReplacement.h"
#include "mozilla/Vector.h"
#include "jit/IonAnalysis.h"
#include "jit/JitSpewer.h"
#include "jit/MIR.h"
#include "jit/MIRGenerator.h"
#include "jit/MIRGraph.h"
#include "vm/UnboxedObject.h"
#include "jsobjinlines.h"
namespace js {
namespace jit {
template <typename MemoryView>
class EmulateStateOf
{
private:
typedef typename MemoryView::BlockState BlockState;
MIRGenerator* mir_;
MIRGraph& graph_;
// Block state at the entrance of all basic blocks.
Vector<BlockState*, 8, SystemAllocPolicy> states_;
public:
EmulateStateOf(MIRGenerator* mir, MIRGraph& graph)
: mir_(mir),
graph_(graph)
{
}
bool run(MemoryView& view);
};
template <typename MemoryView>
bool
EmulateStateOf<MemoryView>::run(MemoryView& view)
{
// Initialize the current block state of each block to an unknown state.
if (!states_.appendN(nullptr, graph_.numBlocks()))
return false;
// Initialize the first block which needs to be traversed in RPO.
MBasicBlock* startBlock = view.startingBlock();
if (!view.initStartingState(&states_[startBlock->id()]))
return false;
// Iterate over each basic block which has a valid entry state, and merge
// the state in the successor blocks.
for (ReversePostorderIterator block = graph_.rpoBegin(startBlock); block != graph_.rpoEnd(); block++) {
if (mir_->shouldCancel(MemoryView::phaseName))
return false;
// Get the block state as the result of the merge of all predecessors
// which have already been visited in RPO. This means that backedges
// are not yet merged into the loop.
BlockState* state = states_[block->id()];
if (!state)
continue;
view.setEntryBlockState(state);
// Iterates over resume points, phi and instructions.
for (MNodeIterator iter(*block); iter; ) {
// Increment the iterator before visiting the instruction, as the
// visit function might discard itself from the basic block.
MNode* ins = *iter++;
if (ins->isDefinition())
ins->toDefinition()->accept(&view);
else
view.visitResumePoint(ins->toResumePoint());
if (view.oom())
return false;
}
// For each successor, merge the current state into the state of the
// successors.
for (size_t s = 0; s < block->numSuccessors(); s++) {
MBasicBlock* succ = block->getSuccessor(s);
if (!view.mergeIntoSuccessorState(*block, succ, &states_[succ->id()]))
return false;
}
}
states_.clear();
return true;
}
static bool
IsObjectEscaped(MInstruction* ins, JSObject* objDefault = nullptr);
// Returns False if the lambda is not escaped and if it is optimizable by
// ScalarReplacementOfObject.
static bool
IsLambdaEscaped(MLambda* lambda, JSObject* obj)
{
JitSpewDef(JitSpew_Escape, "Check lambda\n", lambda);
JitSpewIndent spewIndent(JitSpew_Escape);
// The scope chain is not escaped if none of the Lambdas which are
// capturing it are escaped.
for (MUseIterator i(lambda->usesBegin()); i != lambda->usesEnd(); i++) {
MNode* consumer = (*i)->consumer();
if (!consumer->isDefinition()) {
// Cannot optimize if it is observable from fun.arguments or others.
if (!consumer->toResumePoint()->isRecoverableOperand(*i)) {
JitSpew(JitSpew_Escape, "Observable lambda cannot be recovered");
return true;
}
continue;
}
MDefinition* def = consumer->toDefinition();
if (!def->isFunctionEnvironment()) {
JitSpewDef(JitSpew_Escape, "is escaped by\n", def);
return true;
}
if (IsObjectEscaped(def->toInstruction(), obj)) {
JitSpewDef(JitSpew_Escape, "is indirectly escaped by\n", def);
return true;
}
}
JitSpew(JitSpew_Escape, "Lambda is not escaped");
return false;
}
// Returns False if the object is not escaped and if it is optimizable by
// ScalarReplacementOfObject.
//
// For the moment, this code is dumb as it only supports objects which are not
// changing shape, and which are known by TI at the object creation.
static bool
IsObjectEscaped(MInstruction* ins, JSObject* objDefault)
{
MOZ_ASSERT(ins->type() == MIRType_Object);
MOZ_ASSERT(ins->isNewObject() || ins->isGuardShape() || ins->isCreateThisWithTemplate() ||
ins->isNewCallObject() || ins->isFunctionEnvironment());
JitSpewDef(JitSpew_Escape, "Check object\n", ins);
JitSpewIndent spewIndent(JitSpew_Escape);
JSObject* obj = objDefault;
if (!obj)
obj = MObjectState::templateObjectOf(ins);
if (!obj) {
JitSpew(JitSpew_Escape, "No template object defined.");
return true;
}
// Check if the object is escaped. If the object is not the first argument
// of either a known Store / Load, then we consider it as escaped. This is a
// cheap and conservative escape analysis.
for (MUseIterator i(ins->usesBegin()); i != ins->usesEnd(); i++) {
MNode* consumer = (*i)->consumer();
if (!consumer->isDefinition()) {
// Cannot optimize if it is observable from fun.arguments or others.
if (!consumer->toResumePoint()->isRecoverableOperand(*i)) {
JitSpew(JitSpew_Escape, "Observable object cannot be recovered");
return true;
}
continue;
}
MDefinition* def = consumer->toDefinition();
switch (def->op()) {
case MDefinition::Op_StoreFixedSlot:
case MDefinition::Op_LoadFixedSlot:
// Not escaped if it is the first argument.
if (def->indexOf(*i) == 0)
break;
JitSpewDef(JitSpew_Escape, "is escaped by\n", def);
return true;
case MDefinition::Op_LoadUnboxedScalar:
case MDefinition::Op_StoreUnboxedScalar:
case MDefinition::Op_LoadUnboxedObjectOrNull:
case MDefinition::Op_StoreUnboxedObjectOrNull:
case MDefinition::Op_LoadUnboxedString:
case MDefinition::Op_StoreUnboxedString:
// Not escaped if it is the first argument.
if (def->indexOf(*i) != 0) {
JitSpewDef(JitSpew_Escape, "is escaped by\n", def);
return true;
}
if (!def->getOperand(1)->isConstant()) {
JitSpewDef(JitSpew_Escape, "is addressed with unknown index\n", def);
return true;
}
break;
case MDefinition::Op_PostWriteBarrier:
break;
case MDefinition::Op_Slots: {
#ifdef DEBUG
// Assert that MSlots are only used by MStoreSlot and MLoadSlot.
MSlots* ins = def->toSlots();
MOZ_ASSERT(ins->object() != 0);
for (MUseIterator i(ins->usesBegin()); i != ins->usesEnd(); i++) {
// toDefinition should normally never fail, since they don't get
// captured by resume points.
MDefinition* def = (*i)->consumer()->toDefinition();
MOZ_ASSERT(def->op() == MDefinition::Op_StoreSlot ||
def->op() == MDefinition::Op_LoadSlot);
}
#endif
break;
}
case MDefinition::Op_GuardShape: {
MGuardShape* guard = def->toGuardShape();
MOZ_ASSERT(!ins->isGuardShape());
if (obj->maybeShape() != guard->shape()) {
JitSpewDef(JitSpew_Escape, "has a non-matching guard shape\n", guard);
return true;
}
if (IsObjectEscaped(def->toInstruction(), obj)) {
JitSpewDef(JitSpew_Escape, "is indirectly escaped by\n", def);
return true;
}
break;
}
case MDefinition::Op_Lambda: {
MLambda* lambda = def->toLambda();
if (IsLambdaEscaped(lambda, obj)) {
JitSpewDef(JitSpew_Escape, "is indirectly escaped by\n", lambda);
return true;
}
break;
}
// This instruction is a no-op used to verify that scalar replacement
// is working as expected in jit-test.
case MDefinition::Op_AssertRecoveredOnBailout:
break;
default:
JitSpewDef(JitSpew_Escape, "is escaped by\n", def);
return true;
}
}
JitSpew(JitSpew_Escape, "Object is not escaped");
return false;
}
class ObjectMemoryView : public MDefinitionVisitorDefaultNoop
{
public:
typedef MObjectState BlockState;
static const char* phaseName;
private:
TempAllocator& alloc_;
MConstant* undefinedVal_;
MInstruction* obj_;
MBasicBlock* startBlock_;
BlockState* state_;
// Used to improve the memory usage by sharing common modification.
const MResumePoint* lastResumePoint_;
bool oom_;
public:
ObjectMemoryView(TempAllocator& alloc, MInstruction* obj);
MBasicBlock* startingBlock();
bool initStartingState(BlockState** pState);
void setEntryBlockState(BlockState* state);
bool mergeIntoSuccessorState(MBasicBlock* curr, MBasicBlock* succ, BlockState** pSuccState);
#ifdef DEBUG
void assertSuccess();
#else
void assertSuccess() {}
#endif
bool oom() const { return oom_; }
public:
void visitResumePoint(MResumePoint* rp);
void visitObjectState(MObjectState* ins);
void visitStoreFixedSlot(MStoreFixedSlot* ins);
void visitLoadFixedSlot(MLoadFixedSlot* ins);
void visitPostWriteBarrier(MPostWriteBarrier* ins);
void visitStoreSlot(MStoreSlot* ins);
void visitLoadSlot(MLoadSlot* ins);
void visitGuardShape(MGuardShape* ins);
void visitFunctionEnvironment(MFunctionEnvironment* ins);
void visitLambda(MLambda* ins);
void visitStoreUnboxedScalar(MStoreUnboxedScalar* ins);
void visitLoadUnboxedScalar(MLoadUnboxedScalar* ins);
void visitStoreUnboxedObjectOrNull(MStoreUnboxedObjectOrNull* ins);
void visitLoadUnboxedObjectOrNull(MLoadUnboxedObjectOrNull* ins);
void visitStoreUnboxedString(MStoreUnboxedString* ins);
void visitLoadUnboxedString(MLoadUnboxedString* ins);
private:
void storeOffset(MInstruction* ins, size_t offset, MDefinition* value);
void loadOffset(MInstruction* ins, size_t offset);
};
const char* ObjectMemoryView::phaseName = "Scalar Replacement of Object";
ObjectMemoryView::ObjectMemoryView(TempAllocator& alloc, MInstruction* obj)
: alloc_(alloc),
obj_(obj),
startBlock_(obj->block()),
state_(nullptr),
lastResumePoint_(nullptr),
oom_(false)
{
// Annotate snapshots RValue such that we recover the store first.
obj_->setIncompleteObject();
// Annotate the instruction such that we do not replace it by a
// Magic(JS_OPTIMIZED_OUT) in case of removed uses.
obj_->setImplicitlyUsedUnchecked();
}
MBasicBlock*
ObjectMemoryView::startingBlock()
{
return startBlock_;
}
bool
ObjectMemoryView::initStartingState(BlockState** pState)
{
// Uninitialized slots have an "undefined" value.
undefinedVal_ = MConstant::New(alloc_, UndefinedValue());
startBlock_->insertBefore(obj_, undefinedVal_);
// Create a new block state and insert at it at the location of the new object.
BlockState* state = BlockState::New(alloc_, obj_, undefinedVal_);
if (!state)
return false;
startBlock_->insertAfter(obj_, state);
// Hold out of resume point until it is visited.
state->setInWorklist();
*pState = state;
return true;
}
void
ObjectMemoryView::setEntryBlockState(BlockState* state)
{
state_ = state;
}
bool
ObjectMemoryView::mergeIntoSuccessorState(MBasicBlock* curr, MBasicBlock* succ,
BlockState** pSuccState)
{
BlockState* succState = *pSuccState;
// When a block has no state yet, create an empty one for the
// successor.
if (!succState) {
// If the successor is not dominated then the object cannot flow
// in this basic block without a Phi. We know that no Phi exist
// in non-dominated successors as the conservative escaped
// analysis fails otherwise. Such condition can succeed if the
// successor is a join at the end of a if-block and the object
// only exists within the branch.
if (!startBlock_->dominates(succ))
return true;
// If there is only one predecessor, carry over the last state of the
// block to the successor. As the block state is immutable, if the
// current block has multiple successors, they will share the same entry
// state.
if (succ->numPredecessors() <= 1 || !state_->numSlots()) {
*pSuccState = state_;
return true;
}
// If we have multiple predecessors, then we allocate one Phi node for
// each predecessor, and create a new block state which only has phi
// nodes. These would later be removed by the removal of redundant phi
// nodes.
succState = BlockState::Copy(alloc_, state_);
if (!succState)
return false;
size_t numPreds = succ->numPredecessors();
for (size_t slot = 0; slot < state_->numSlots(); slot++) {
MPhi* phi = MPhi::New(alloc_);
if (!phi->reserveLength(numPreds))
return false;
// Fill the input of the successors Phi with undefined
// values, and each block later fills the Phi inputs.
for (size_t p = 0; p < numPreds; p++)
phi->addInput(undefinedVal_);
// Add Phi in the list of Phis of the basic block.
succ->addPhi(phi);
succState->setSlot(slot, phi);
}
// Insert the newly created block state instruction at the beginning
// of the successor block, after all the phi nodes. Note that it
// would be captured by the entry resume point of the successor
// block.
succ->insertBefore(succ->safeInsertTop(), succState);
*pSuccState = succState;
}
MOZ_ASSERT_IF(succ == startBlock_, startBlock_->isLoopHeader());
if (succ->numPredecessors() > 1 && succState->numSlots() && succ != startBlock_) {
// We need to re-compute successorWithPhis as the previous EliminatePhis
// phase might have removed all the Phis from the successor block.
size_t currIndex;
MOZ_ASSERT(!succ->phisEmpty());
if (curr->successorWithPhis()) {
MOZ_ASSERT(curr->successorWithPhis() == succ);
currIndex = curr->positionInPhiSuccessor();
} else {
currIndex = succ->indexForPredecessor(curr);
curr->setSuccessorWithPhis(succ, currIndex);
}
MOZ_ASSERT(succ->getPredecessor(currIndex) == curr);
// Copy the current slot states to the index of current block in all the
// Phi created during the first visit of the successor.
for (size_t slot = 0; slot < state_->numSlots(); slot++) {
MPhi* phi = succState->getSlot(slot)->toPhi();
phi->replaceOperand(currIndex, state_->getSlot(slot));
}
}
return true;
}
#ifdef DEBUG
void
ObjectMemoryView::assertSuccess()
{
for (MUseIterator i(obj_->usesBegin()); i != obj_->usesEnd(); i++) {
MNode* ins = (*i)->consumer();
MDefinition* def = nullptr;
// Resume points have been replaced by the object state.
if (ins->isResumePoint() || (def = ins->toDefinition())->isRecoveredOnBailout()) {
MOZ_ASSERT(obj_->isIncompleteObject());
continue;
}
// The only remaining uses would be removed by DCE, which will also
// recover the object on bailouts.
MOZ_ASSERT(def->isSlots() || def->isLambda());
MOZ_ASSERT(!def->hasDefUses());
}
}
#endif
void
ObjectMemoryView::visitResumePoint(MResumePoint* rp)
{
// As long as the MObjectState is not yet seen next to the allocation, we do
// not patch the resume point to recover the side effects.
if (!state_->isInWorklist()) {
rp->addStore(alloc_, state_, lastResumePoint_);
lastResumePoint_ = rp;
}
}
void
ObjectMemoryView::visitObjectState(MObjectState* ins)
{
if (ins->isInWorklist())
ins->setNotInWorklist();
}
void
ObjectMemoryView::visitStoreFixedSlot(MStoreFixedSlot* ins)
{
// Skip stores made on other objects.
if (ins->object() != obj_)
return;
// Clone the state and update the slot value.
if (state_->hasFixedSlot(ins->slot())) {
state_ = BlockState::Copy(alloc_, state_);
if (!state_) {
oom_ = true;
return;
}
state_->setFixedSlot(ins->slot(), ins->value());
ins->block()->insertBefore(ins->toInstruction(), state_);
} else {
// UnsafeSetReserveSlot can access baked-in slots which are guarded by
// conditions, which are not seen by the escape analysis.
MBail* bailout = MBail::New(alloc_, Bailout_Inevitable);
ins->block()->insertBefore(ins, bailout);
}
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitLoadFixedSlot(MLoadFixedSlot* ins)
{
// Skip loads made on other objects.
if (ins->object() != obj_)
return;
// Replace load by the slot value.
if (state_->hasFixedSlot(ins->slot())) {
ins->replaceAllUsesWith(state_->getFixedSlot(ins->slot()));
} else {
// UnsafeGetReserveSlot can access baked-in slots which are guarded by
// conditions, which are not seen by the escape analysis.
MBail* bailout = MBail::New(alloc_, Bailout_Inevitable);
ins->block()->insertBefore(ins, bailout);
ins->replaceAllUsesWith(undefinedVal_);
}
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitPostWriteBarrier(MPostWriteBarrier* ins)
{
// Skip loads made on other objects.
if (ins->object() != obj_)
return;
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitStoreSlot(MStoreSlot* ins)
{
// Skip stores made on other objects.
MSlots* slots = ins->slots()->toSlots();
if (slots->object() != obj_) {
// Guard objects are replaced when they are visited.
MOZ_ASSERT(!slots->object()->isGuardShape() || slots->object()->toGuardShape()->obj() != obj_);
return;
}
// Clone the state and update the slot value.
if (state_->hasDynamicSlot(ins->slot())) {
state_ = BlockState::Copy(alloc_, state_);
if (!state_) {
oom_ = true;
return;
}
state_->setDynamicSlot(ins->slot(), ins->value());
ins->block()->insertBefore(ins->toInstruction(), state_);
} else {
// UnsafeSetReserveSlot can access baked-in slots which are guarded by
// conditions, which are not seen by the escape analysis.
MBail* bailout = MBail::New(alloc_, Bailout_Inevitable);
ins->block()->insertBefore(ins, bailout);
}
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitLoadSlot(MLoadSlot* ins)
{
// Skip loads made on other objects.
MSlots* slots = ins->slots()->toSlots();
if (slots->object() != obj_) {
// Guard objects are replaced when they are visited.
MOZ_ASSERT(!slots->object()->isGuardShape() || slots->object()->toGuardShape()->obj() != obj_);
return;
}
// Replace load by the slot value.
if (state_->hasDynamicSlot(ins->slot())) {
ins->replaceAllUsesWith(state_->getDynamicSlot(ins->slot()));
} else {
// UnsafeGetReserveSlot can access baked-in slots which are guarded by
// conditions, which are not seen by the escape analysis.
MBail* bailout = MBail::New(alloc_, Bailout_Inevitable);
ins->block()->insertBefore(ins, bailout);
ins->replaceAllUsesWith(undefinedVal_);
}
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitGuardShape(MGuardShape* ins)
{
// Skip loads made on other objects.
if (ins->obj() != obj_)
return;
// Replace the shape guard by its object.
ins->replaceAllUsesWith(obj_);
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitFunctionEnvironment(MFunctionEnvironment* ins)
{
// Skip function environment which are not aliases of the NewCallObject.
MDefinition* input = ins->input();
if (!input->isLambda() || input->toLambda()->scopeChain() != obj_)
return;
// Replace the function environment by the scope chain of the lambda.
ins->replaceAllUsesWith(obj_);
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitLambda(MLambda* ins)
{
if (ins->scopeChain() != obj_)
return;
// In order to recover the lambda we need to recover the scope chain, as the
// lambda is holding it.
ins->setIncompleteObject();
}
static size_t
GetOffsetOf(MDefinition* index, size_t width, int32_t baseOffset)
{
int32_t idx = index->toConstant()->value().toInt32();
MOZ_ASSERT(idx >= 0);
MOZ_ASSERT(baseOffset >= 0 && size_t(baseOffset) >= UnboxedPlainObject::offsetOfData());
return idx * width + baseOffset - UnboxedPlainObject::offsetOfData();
}
static size_t
GetOffsetOf(MDefinition* index, Scalar::Type type, int32_t baseOffset)
{
return GetOffsetOf(index, Scalar::byteSize(type), baseOffset);
}
void
ObjectMemoryView::storeOffset(MInstruction* ins, size_t offset, MDefinition* value)
{
// Clone the state and update the slot value.
MOZ_ASSERT(state_->hasOffset(offset));
state_ = BlockState::Copy(alloc_, state_);
if (!state_) {
oom_ = true;
return;
}
state_->setOffset(offset, value);
ins->block()->insertBefore(ins, state_);
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::loadOffset(MInstruction* ins, size_t offset)
{
// Replace load by the slot value.
MOZ_ASSERT(state_->hasOffset(offset));
ins->replaceAllUsesWith(state_->getOffset(offset));
// Remove original instruction.
ins->block()->discard(ins);
}
void
ObjectMemoryView::visitStoreUnboxedScalar(MStoreUnboxedScalar* ins)
{
// Skip stores made on other objects.
if (ins->elements() != obj_)
return;
size_t offset = GetOffsetOf(ins->index(), ins->storageType(), ins->offsetAdjustment());
storeOffset(ins, offset, ins->value());
}
void
ObjectMemoryView::visitLoadUnboxedScalar(MLoadUnboxedScalar* ins)
{
// Skip loads made on other objects.
if (ins->elements() != obj_)
return;
// Replace load by the slot value.
size_t offset = GetOffsetOf(ins->index(), ins->storageType(), ins->offsetAdjustment());
loadOffset(ins, offset);
}
void
ObjectMemoryView::visitStoreUnboxedObjectOrNull(MStoreUnboxedObjectOrNull* ins)
{
// Skip stores made on other objects.
if (ins->elements() != obj_)
return;
// Clone the state and update the slot value.
size_t offset = GetOffsetOf(ins->index(), sizeof(uintptr_t), ins->offsetAdjustment());
storeOffset(ins, offset, ins->value());
}
void
ObjectMemoryView::visitLoadUnboxedObjectOrNull(MLoadUnboxedObjectOrNull* ins)
{
// Skip loads made on other objects.
if (ins->elements() != obj_)
return;
// Replace load by the slot value.
size_t offset = GetOffsetOf(ins->index(), sizeof(uintptr_t), ins->offsetAdjustment());
loadOffset(ins, offset);
}
void
ObjectMemoryView::visitStoreUnboxedString(MStoreUnboxedString* ins)
{
// Skip stores made on other objects.
if (ins->elements() != obj_)
return;
// Clone the state and update the slot value.
size_t offset = GetOffsetOf(ins->index(), sizeof(uintptr_t), ins->offsetAdjustment());
storeOffset(ins, offset, ins->value());
}
void
ObjectMemoryView::visitLoadUnboxedString(MLoadUnboxedString* ins)
{
// Skip loads made on other objects.
if (ins->elements() != obj_)
return;
// Replace load by the slot value.
size_t offset = GetOffsetOf(ins->index(), sizeof(uintptr_t), ins->offsetAdjustment());
loadOffset(ins, offset);
}
static bool
IndexOf(MDefinition* ins, int32_t* res)
{
MOZ_ASSERT(ins->isLoadElement() || ins->isStoreElement());
MDefinition* indexDef = ins->getOperand(1); // ins->index();
if (indexDef->isBoundsCheck())
indexDef = indexDef->toBoundsCheck()->index();
if (indexDef->isToInt32())
indexDef = indexDef->toToInt32()->getOperand(0);
if (!indexDef->isConstantValue())
return false;
Value index = indexDef->constantValue();
if (!index.isInt32())
return false;
*res = index.toInt32();
return true;
}
// Returns False if the elements is not escaped and if it is optimizable by
// ScalarReplacementOfArray.
static bool
IsElementEscaped(MElements* def, uint32_t arraySize)
{
JitSpewDef(JitSpew_Escape, "Check elements\n", def);
JitSpewIndent spewIndent(JitSpew_Escape);
for (MUseIterator i(def->usesBegin()); i != def->usesEnd(); i++) {
// The MIRType_Elements cannot be captured in a resume point as
// it does not represent a value allocation.
MDefinition* access = (*i)->consumer()->toDefinition();
switch (access->op()) {
case MDefinition::Op_LoadElement: {
MOZ_ASSERT(access->toLoadElement()->elements() == def);
// If we need hole checks, then the array cannot be escaped
// as the array might refer to the prototype chain to look
// for properties, thus it might do additional side-effects
// which are not reflected by the alias set, is we are
// bailing on holes.
if (access->toLoadElement()->needsHoleCheck()) {
JitSpewDef(JitSpew_Escape,
"has a load element with a hole check\n", access);
return true;
}
// If the index is not a constant then this index can alias
// all others. We do not handle this case.
int32_t index;
if (!IndexOf(access, &index)) {
JitSpewDef(JitSpew_Escape,
"has a load element with a non-trivial index\n", access);
return true;
}
if (index < 0 || arraySize <= uint32_t(index)) {
JitSpewDef(JitSpew_Escape,
"has a load element with an out-of-bound index\n", access);
return true;
}
break;
}
case MDefinition::Op_StoreElement: {
MOZ_ASSERT(access->toStoreElement()->elements() == def);
// If we need hole checks, then the array cannot be escaped
// as the array might refer to the prototype chain to look
// for properties, thus it might do additional side-effects
// which are not reflected by the alias set, is we are
// bailing on holes.
if (access->toStoreElement()->needsHoleCheck()) {
JitSpewDef(JitSpew_Escape,
"has a store element with a hole check\n", access);
return true;
}
// If the index is not a constant then this index can alias
// all others. We do not handle this case.
int32_t index;
if (!IndexOf(access, &index)) {
JitSpewDef(JitSpew_Escape, "has a store element with a non-trivial index\n", access);
return true;
}
if (index < 0 || arraySize <= uint32_t(index)) {
JitSpewDef(JitSpew_Escape, "has a store element with an out-of-bound index\n", access);
return true;
}
// We are not yet encoding magic hole constants in resume points.
if (access->toStoreElement()->value()->type() == MIRType_MagicHole) {
JitSpewDef(JitSpew_Escape, "has a store element with an magic-hole constant\n", access);
return true;
}
break;
}
case MDefinition::Op_SetInitializedLength:
MOZ_ASSERT(access->toSetInitializedLength()->elements() == def);
break;
case MDefinition::Op_InitializedLength:
MOZ_ASSERT(access->toInitializedLength()->elements() == def);
break;
case MDefinition::Op_ArrayLength:
MOZ_ASSERT(access->toArrayLength()->elements() == def);
break;
default:
JitSpewDef(JitSpew_Escape, "is escaped by\n", access);
return true;
}
}
JitSpew(JitSpew_Escape, "Elements is not escaped");
return false;
}
// Returns False if the array is not escaped and if it is optimizable by
// ScalarReplacementOfArray.
//
// For the moment, this code is dumb as it only supports arrays which are not
// changing length, with only access with known constants.
static bool
IsArrayEscaped(MInstruction* ins)
{
MOZ_ASSERT(ins->type() == MIRType_Object);
MOZ_ASSERT(ins->isNewArray());
uint32_t length = ins->toNewArray()->length();
JitSpewDef(JitSpew_Escape, "Check array\n", ins);
JitSpewIndent spewIndent(JitSpew_Escape);
JSObject* obj = ins->toNewArray()->templateObject();
if (!obj) {
JitSpew(JitSpew_Escape, "No template object defined.");
return true;
}
if (obj->is<UnboxedArrayObject>()) {
JitSpew(JitSpew_Escape, "Template object is an unboxed plain object.");
return true;
}
if (length >= 16) {
JitSpew(JitSpew_Escape, "Array has too many elements");
return true;
}
// Check if the object is escaped. If the object is not the first argument
// of either a known Store / Load, then we consider it as escaped. This is a
// cheap and conservative escape analysis.
for (MUseIterator i(ins->usesBegin()); i != ins->usesEnd(); i++) {
MNode* consumer = (*i)->consumer();
if (!consumer->isDefinition()) {
// Cannot optimize if it is observable from fun.arguments or others.
if (!consumer->toResumePoint()->isRecoverableOperand(*i)) {
JitSpew(JitSpew_Escape, "Observable array cannot be recovered");
return true;
}
continue;
}
MDefinition* def = consumer->toDefinition();
switch (def->op()) {
case MDefinition::Op_Elements: {
MElements *elem = def->toElements();
MOZ_ASSERT(elem->object() == ins);
if (IsElementEscaped(elem, length)) {
JitSpewDef(JitSpew_Escape, "is indirectly escaped by\n", elem);
return true;
}
break;
}
// This instruction is a no-op used to verify that scalar replacement
// is working as expected in jit-test.
case MDefinition::Op_AssertRecoveredOnBailout:
break;
default:
JitSpewDef(JitSpew_Escape, "is escaped by\n", def);
return true;
}
}
JitSpew(JitSpew_Escape, "Array is not escaped");
return false;
}
// This class replaces every MStoreElement and MSetInitializedLength by an
// MArrayState which emulates the content of the array. All MLoadElement,
// MInitializedLength and MArrayLength are replaced by the corresponding value.
//
// In order to restore the value of the array correctly in case of bailouts, we
// replace all reference of the allocation by the MArrayState definition.
class ArrayMemoryView : public MDefinitionVisitorDefaultNoop
{
public:
typedef MArrayState BlockState;
static const char* phaseName;
private:
TempAllocator& alloc_;
MConstant* undefinedVal_;
MConstant* length_;
MInstruction* arr_;
MBasicBlock* startBlock_;
BlockState* state_;
// Used to improve the memory usage by sharing common modification.
const MResumePoint* lastResumePoint_;
bool oom_;
public:
ArrayMemoryView(TempAllocator& alloc, MInstruction* arr);
MBasicBlock* startingBlock();
bool initStartingState(BlockState** pState);
void setEntryBlockState(BlockState* state);
bool mergeIntoSuccessorState(MBasicBlock* curr, MBasicBlock* succ, BlockState** pSuccState);
#ifdef DEBUG
void assertSuccess();
#else
void assertSuccess() {}
#endif
bool oom() const { return oom_; }
private:
bool isArrayStateElements(MDefinition* elements);
void discardInstruction(MInstruction* ins, MDefinition* elements);
public:
void visitResumePoint(MResumePoint* rp);
void visitArrayState(MArrayState* ins);
void visitStoreElement(MStoreElement* ins);
void visitLoadElement(MLoadElement* ins);
void visitSetInitializedLength(MSetInitializedLength* ins);
void visitInitializedLength(MInitializedLength* ins);
void visitArrayLength(MArrayLength* ins);
};
const char* ArrayMemoryView::phaseName = "Scalar Replacement of Array";
ArrayMemoryView::ArrayMemoryView(TempAllocator& alloc, MInstruction* arr)
: alloc_(alloc),
undefinedVal_(nullptr),
length_(nullptr),
arr_(arr),
startBlock_(arr->block()),
state_(nullptr),
lastResumePoint_(nullptr),
oom_(false)
{
// Annotate snapshots RValue such that we recover the store first.
arr_->setIncompleteObject();
// Annotate the instruction such that we do not replace it by a
// Magic(JS_OPTIMIZED_OUT) in case of removed uses.
arr_->setImplicitlyUsedUnchecked();
}
MBasicBlock*
ArrayMemoryView::startingBlock()
{
return startBlock_;
}
bool
ArrayMemoryView::initStartingState(BlockState** pState)
{
// Uninitialized elements have an "undefined" value.
undefinedVal_ = MConstant::New(alloc_, UndefinedValue());
MConstant* initLength = MConstant::New(alloc_, Int32Value(0));
arr_->block()->insertBefore(arr_, undefinedVal_);
arr_->block()->insertBefore(arr_, initLength);
// Create a new block state and insert at it at the location of the new array.
BlockState* state = BlockState::New(alloc_, arr_, undefinedVal_, initLength);
if (!state)
return false;
startBlock_->insertAfter(arr_, state);
// Hold out of resume point until it is visited.
state->setInWorklist();
*pState = state;
return true;
}
void
ArrayMemoryView::setEntryBlockState(BlockState* state)
{
state_ = state;
}
bool
ArrayMemoryView::mergeIntoSuccessorState(MBasicBlock* curr, MBasicBlock* succ,
BlockState** pSuccState)
{
BlockState* succState = *pSuccState;
// When a block has no state yet, create an empty one for the
// successor.
if (!succState) {
// If the successor is not dominated then the array cannot flow
// in this basic block without a Phi. We know that no Phi exist
// in non-dominated successors as the conservative escaped
// analysis fails otherwise. Such condition can succeed if the
// successor is a join at the end of a if-block and the array
// only exists within the branch.
if (!startBlock_->dominates(succ))
return true;
// If there is only one predecessor, carry over the last state of the
// block to the successor. As the block state is immutable, if the
// current block has multiple successors, they will share the same entry
// state.
if (succ->numPredecessors() <= 1 || !state_->numElements()) {
*pSuccState = state_;
return true;
}
// If we have multiple predecessors, then we allocate one Phi node for
// each predecessor, and create a new block state which only has phi
// nodes. These would later be removed by the removal of redundant phi
// nodes.
succState = BlockState::Copy(alloc_, state_);
if (!succState)
return false;
size_t numPreds = succ->numPredecessors();
for (size_t index = 0; index < state_->numElements(); index++) {
MPhi* phi = MPhi::New(alloc_);
if (!phi->reserveLength(numPreds))
return false;
// Fill the input of the successors Phi with undefined
// values, and each block later fills the Phi inputs.
for (size_t p = 0; p < numPreds; p++)
phi->addInput(undefinedVal_);
// Add Phi in the list of Phis of the basic block.
succ->addPhi(phi);
succState->setElement(index, phi);
}
// Insert the newly created block state instruction at the beginning
// of the successor block, after all the phi nodes. Note that it
// would be captured by the entry resume point of the successor
// block.
succ->insertBefore(succ->safeInsertTop(), succState);
*pSuccState = succState;
}
MOZ_ASSERT_IF(succ == startBlock_, startBlock_->isLoopHeader());
if (succ->numPredecessors() > 1 && succState->numElements() && succ != startBlock_) {
// We need to re-compute successorWithPhis as the previous EliminatePhis
// phase might have removed all the Phis from the successor block.
size_t currIndex;
MOZ_ASSERT(!succ->phisEmpty());
if (curr->successorWithPhis()) {
MOZ_ASSERT(curr->successorWithPhis() == succ);
currIndex = curr->positionInPhiSuccessor();
} else {
currIndex = succ->indexForPredecessor(curr);
curr->setSuccessorWithPhis(succ, currIndex);
}
MOZ_ASSERT(succ->getPredecessor(currIndex) == curr);
// Copy the current element states to the index of current block in all
// the Phi created during the first visit of the successor.
for (size_t index = 0; index < state_->numElements(); index++) {
MPhi* phi = succState->getElement(index)->toPhi();
phi->replaceOperand(currIndex, state_->getElement(index));
}
}
return true;
}
#ifdef DEBUG
void
ArrayMemoryView::assertSuccess()
{
MOZ_ASSERT(!arr_->hasLiveDefUses());
}
#endif
void
ArrayMemoryView::visitResumePoint(MResumePoint* rp)
{
// As long as the MArrayState is not yet seen next to the allocation, we do
// not patch the resume point to recover the side effects.
if (!state_->isInWorklist()) {
rp->addStore(alloc_, state_, lastResumePoint_);
lastResumePoint_ = rp;
}
}
void
ArrayMemoryView::visitArrayState(MArrayState* ins)
{
if (ins->isInWorklist())
ins->setNotInWorklist();
}
bool
ArrayMemoryView::isArrayStateElements(MDefinition* elements)
{
return elements->isElements() && elements->toElements()->object() == arr_;
}
void
ArrayMemoryView::discardInstruction(MInstruction* ins, MDefinition* elements)
{
MOZ_ASSERT(elements->isElements());
ins->block()->discard(ins);
if (!elements->hasLiveDefUses())
elements->block()->discard(elements->toInstruction());
}
void
ArrayMemoryView::visitStoreElement(MStoreElement* ins)
{
// Skip other array objects.
MDefinition* elements = ins->elements();
if (!isArrayStateElements(elements))
return;
// Register value of the setter in the state.
int32_t index;
MOZ_ALWAYS_TRUE(IndexOf(ins, &index));
state_ = BlockState::Copy(alloc_, state_);
if (!state_) {
oom_ = true;
return;
}
state_->setElement(index, ins->value());
ins->block()->insertBefore(ins, state_);
// Remove original instruction.
discardInstruction(ins, elements);
}
void
ArrayMemoryView::visitLoadElement(MLoadElement* ins)
{
// Skip other array objects.
MDefinition* elements = ins->elements();
if (!isArrayStateElements(elements))
return;
// Replace by the value contained at the index.
int32_t index;
MOZ_ALWAYS_TRUE(IndexOf(ins, &index));
ins->replaceAllUsesWith(state_->getElement(index));
// Remove original instruction.
discardInstruction(ins, elements);
}
void
ArrayMemoryView::visitSetInitializedLength(MSetInitializedLength* ins)
{
// Skip other array objects.
MDefinition* elements = ins->elements();
if (!isArrayStateElements(elements))
return;
// Replace by the new initialized length. Note that the argument of
// MSetInitalizedLength is the last index and not the initialized length.
// To obtain the length, we need to add 1 to it, and thus we need to create
// a new constant that we register in the ArrayState.
state_ = BlockState::Copy(alloc_, state_);
if (!state_) {
oom_ = true;
return;
}
int32_t initLengthValue = ins->index()->constantValue().toInt32() + 1;
MConstant* initLength = MConstant::New(alloc_, Int32Value(initLengthValue));
ins->block()->insertBefore(ins, initLength);
ins->block()->insertBefore(ins, state_);
state_->setInitializedLength(initLength);
// Remove original instruction.
discardInstruction(ins, elements);
}
void
ArrayMemoryView::visitInitializedLength(MInitializedLength* ins)
{
// Skip other array objects.
MDefinition* elements = ins->elements();
if (!isArrayStateElements(elements))
return;
// Replace by the value of the length.
ins->replaceAllUsesWith(state_->initializedLength());
// Remove original instruction.
discardInstruction(ins, elements);
}
void
ArrayMemoryView::visitArrayLength(MArrayLength* ins)
{
// Skip other array objects.
MDefinition* elements = ins->elements();
if (!isArrayStateElements(elements))
return;
// Replace by the value of the length.
if (!length_) {
length_ = MConstant::New(alloc_, Int32Value(state_->numElements()));
arr_->block()->insertBefore(arr_, length_);
}
ins->replaceAllUsesWith(length_);
// Remove original instruction.
discardInstruction(ins, elements);
}
bool
ScalarReplacement(MIRGenerator* mir, MIRGraph& graph)
{
EmulateStateOf<ObjectMemoryView> replaceObject(mir, graph);
EmulateStateOf<ArrayMemoryView> replaceArray(mir, graph);
bool addedPhi = false;
for (ReversePostorderIterator block = graph.rpoBegin(); block != graph.rpoEnd(); block++) {
if (mir->shouldCancel("Scalar Replacement (main loop)"))
return false;
for (MInstructionIterator ins = block->begin(); ins != block->end(); ins++) {
if ((ins->isNewObject() || ins->isCreateThisWithTemplate() || ins->isNewCallObject()) &&
!IsObjectEscaped(*ins))
{
ObjectMemoryView view(graph.alloc(), *ins);
if (!replaceObject.run(view))
return false;
view.assertSuccess();
addedPhi = true;
continue;
}
if (ins->isNewArray() && !IsArrayEscaped(*ins)) {
ArrayMemoryView view(graph.alloc(), *ins);
if (!replaceArray.run(view))
return false;
view.assertSuccess();
addedPhi = true;
continue;
}
}
}
if (addedPhi) {
// Phis added by Scalar Replacement are only redundant Phis which are
// not directly captured by any resume point but only by the MDefinition
// state. The conservative observability only focuses on Phis which are
// not used as resume points operands.
AssertExtendedGraphCoherency(graph);
if (!EliminatePhis(mir, graph, ConservativeObservability))
return false;
}
return true;
}
} /* namespace jit */
} /* namespace js */