blob: af5c69626ce469feadbb17b5574413fe28124202 [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. ``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
* 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 "DFGArgumentsEliminationPhase.h"
#if ENABLE(DFG_JIT)
#include "BytecodeLivenessAnalysisInlines.h"
#include "ClonedArguments.h"
#include "DFGArgumentsUtilities.h"
#include "DFGBasicBlockInlines.h"
#include "DFGBlockMapInlines.h"
#include "DFGClobberize.h"
#include "DFGCombinedLiveness.h"
#include "DFGForAllKills.h"
#include "DFGGraph.h"
#include "DFGInsertionSet.h"
#include "DFGLivenessAnalysisPhase.h"
#include "DFGOSRAvailabilityAnalysisPhase.h"
#include "DFGPhase.h"
#include "JSCInlines.h"
#include <wtf/HashSet.h>
#include <wtf/ListDump.h>
namespace JSC { namespace DFG {
namespace {
bool verbose = false;
class ArgumentsEliminationPhase : public Phase {
public:
ArgumentsEliminationPhase(Graph& graph)
: Phase(graph, "arguments elimination")
{
}
bool run()
{
// For now this phase only works on SSA. This could be changed; we could have a block-local
// version over LoadStore.
DFG_ASSERT(m_graph, nullptr, m_graph.m_form == SSA);
if (verbose) {
dataLog("Graph before arguments elimination:\n");
m_graph.dump();
}
identifyCandidates();
if (m_candidates.isEmpty())
return false;
eliminateCandidatesThatEscape();
if (m_candidates.isEmpty())
return false;
eliminateCandidatesThatInterfere();
if (m_candidates.isEmpty())
return false;
transform();
return true;
}
private:
// Just finds nodes that we know how to work with.
void identifyCandidates()
{
for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
for (Node* node : *block) {
switch (node->op()) {
case CreateDirectArguments:
case CreateClonedArguments:
m_candidates.add(node);
break;
case CreateScopedArguments:
// FIXME: We could handle this if it wasn't for the fact that scoped arguments are
// always stored into the activation.
// https://bugs.webkit.org/show_bug.cgi?id=143072 and
// https://bugs.webkit.org/show_bug.cgi?id=143073
break;
default:
break;
}
}
}
if (verbose)
dataLog("Candidates: ", listDump(m_candidates), "\n");
}
// Look for escaping sites, and remove from the candidates set if we see an escape.
void eliminateCandidatesThatEscape()
{
auto escape = [&] (Edge edge, Node* source) {
if (!edge)
return;
bool removed = m_candidates.remove(edge.node());
if (removed && verbose)
dataLog("eliminating candidate: ", edge.node(), " because it escapes from: ", source, "\n");
};
auto escapeBasedOnArrayMode = [&] (ArrayMode mode, Edge edge, Node* source) {
switch (mode.type()) {
case Array::DirectArguments:
if (edge->op() != CreateDirectArguments)
escape(edge, source);
break;
case Array::Contiguous: {
if (edge->op() != CreateClonedArguments) {
escape(edge, source);
return;
}
// Everything is fine if we're doing an in-bounds access.
if (mode.isInBounds())
break;
// If we're out-of-bounds then we proceed only if the object prototype is
// sane (i.e. doesn't have indexed properties).
JSGlobalObject* globalObject = m_graph.globalObjectFor(edge->origin.semantic);
if (globalObject->objectPrototypeIsSane()) {
m_graph.watchpoints().addLazily(globalObject->objectPrototype()->structure()->transitionWatchpointSet());
if (globalObject->objectPrototypeIsSane())
break;
}
escape(edge, source);
break;
}
case Array::ForceExit:
break;
default:
escape(edge, source);
break;
}
};
for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
for (Node* node : *block) {
switch (node->op()) {
case GetFromArguments:
DFG_ASSERT(m_graph, node, node->child1()->op() == CreateDirectArguments);
break;
case GetByVal:
escapeBasedOnArrayMode(node->arrayMode(), node->child1(), node);
escape(node->child2(), node);
escape(node->child3(), node);
break;
case GetArrayLength:
escapeBasedOnArrayMode(node->arrayMode(), node->child1(), node);
escape(node->child2(), node);
break;
case LoadVarargs:
break;
case CallVarargs:
case ConstructVarargs:
case TailCallVarargs:
case TailCallVarargsInlinedCaller:
escape(node->child1(), node);
escape(node->child2(), node);
break;
case Check:
m_graph.doToChildren(
node,
[&] (Edge edge) {
if (edge.willNotHaveCheck())
return;
if (alreadyChecked(edge.useKind(), SpecObject))
return;
escape(edge, node);
});
break;
case MovHint:
case PutHint:
break;
case GetButterfly:
// This barely works. The danger is that the GetButterfly is used by something that
// does something escaping to a candidate. Fortunately, the only butterfly-using ops
// that we exempt here also use the candidate directly. If there ever was a
// butterfly-using op that we wanted to exempt, then we'd have to look at the
// butterfly's child and check if it's a candidate.
break;
case CheckArray:
escapeBasedOnArrayMode(node->arrayMode(), node->child1(), node);
break;
// FIXME: We should be able to handle GetById/GetByOffset on callee.
// https://bugs.webkit.org/show_bug.cgi?id=143075
case GetByOffset:
if (node->child2()->op() == CreateClonedArguments && node->storageAccessData().offset == clonedArgumentsLengthPropertyOffset)
break;
FALLTHROUGH;
default:
m_graph.doToChildren(node, [&] (Edge edge) { return escape(edge, node); });
break;
}
}
}
if (verbose)
dataLog("After escape analysis: ", listDump(m_candidates), "\n");
}
// Anywhere that a candidate is live (in bytecode or in DFG), check if there is a chance of
// interference between the stack area that the arguments object copies from and the arguments
// object's payload. Conservatively this means that the stack region doesn't get stored to.
void eliminateCandidatesThatInterfere()
{
performLivenessAnalysis(m_graph);
performOSRAvailabilityAnalysis(m_graph);
m_graph.initializeNodeOwners();
CombinedLiveness combinedLiveness(m_graph);
BlockMap<Operands<bool>> clobberedByBlock(m_graph);
for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
Operands<bool>& clobberedByThisBlock = clobberedByBlock[block];
clobberedByThisBlock = Operands<bool>(OperandsLike, m_graph.block(0)->variablesAtHead);
for (Node* node : *block) {
clobberize(
m_graph, node, NoOpClobberize(),
[&] (AbstractHeap heap) {
if (heap.kind() != Stack) {
ASSERT(!heap.overlaps(Stack));
return;
}
ASSERT(!heap.payload().isTop());
VirtualRegister reg(heap.payload().value32());
clobberedByThisBlock.operand(reg) = true;
},
NoOpClobberize());
}
}
for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
// Stop if we've already removed all candidates.
if (m_candidates.isEmpty())
return;
// Ignore blocks that don't write to the stack.
bool writesToStack = false;
for (unsigned i = clobberedByBlock[block].size(); i--;) {
if (clobberedByBlock[block][i]) {
writesToStack = true;
break;
}
}
if (!writesToStack)
continue;
forAllKillsInBlock(
m_graph, combinedLiveness, block,
[&] (unsigned nodeIndex, Node* candidate) {
if (!m_candidates.contains(candidate))
return;
// Check if this block has any clobbers that affect this candidate. This is a fairly
// fast check.
bool isClobberedByBlock = false;
Operands<bool>& clobberedByThisBlock = clobberedByBlock[block];
if (InlineCallFrame* inlineCallFrame = candidate->origin.semantic.inlineCallFrame) {
if (inlineCallFrame->isVarargs()) {
isClobberedByBlock |= clobberedByThisBlock.operand(
inlineCallFrame->stackOffset + CallFrameSlot::argumentCount);
}
if (!isClobberedByBlock || inlineCallFrame->isClosureCall) {
isClobberedByBlock |= clobberedByThisBlock.operand(
inlineCallFrame->stackOffset + CallFrameSlot::callee);
}
if (!isClobberedByBlock) {
for (unsigned i = 0; i < inlineCallFrame->arguments.size() - 1; ++i) {
VirtualRegister reg =
VirtualRegister(inlineCallFrame->stackOffset) +
CallFrame::argumentOffset(i);
if (clobberedByThisBlock.operand(reg)) {
isClobberedByBlock = true;
break;
}
}
}
} else {
// We don't include the ArgumentCount or Callee in this case because we can be
// damn sure that this won't be clobbered.
for (unsigned i = 1; i < static_cast<unsigned>(codeBlock()->numParameters()); ++i) {
if (clobberedByThisBlock.argument(i)) {
isClobberedByBlock = true;
break;
}
}
}
if (!isClobberedByBlock)
return;
// Check if we can immediately eliminate this candidate. If the block has a clobber
// for this arguments allocation, and we'd have to examine every node in the block,
// then we can just eliminate the candidate.
if (nodeIndex == block->size() && candidate->owner != block) {
if (verbose)
dataLog("eliminating candidate: ", candidate, " because it is clobbered by: ", block->at(nodeIndex), "\n");
m_candidates.remove(candidate);
return;
}
// This loop considers all nodes up to the nodeIndex, excluding the nodeIndex.
while (nodeIndex--) {
Node* node = block->at(nodeIndex);
if (node == candidate)
break;
bool found = false;
clobberize(
m_graph, node, NoOpClobberize(),
[&] (AbstractHeap heap) {
if (heap.kind() == Stack && !heap.payload().isTop()) {
if (argumentsInvolveStackSlot(candidate, VirtualRegister(heap.payload().value32())))
found = true;
return;
}
if (heap.overlaps(Stack))
found = true;
},
NoOpClobberize());
if (found) {
if (verbose)
dataLog("eliminating candidate: ", candidate, " because it is clobbered by ", block->at(nodeIndex), "\n");
m_candidates.remove(candidate);
return;
}
}
});
}
// Q: How do we handle OSR exit with a live PhantomArguments at a point where the inline call
// frame is dead? A: Naively we could say that PhantomArguments must escape the stack slots. But
// that would break PutStack sinking, which in turn would break object allocation sinking, in
// cases where we have a varargs call to an otherwise pure method. So, we need something smarter.
// For the outermost arguments, we just have a PhantomArguments that magically knows that it
// should load the arguments from the call frame. For the inline arguments, we have the heap map
// in the availabiltiy map track each possible inline argument as a promoted heap location. If the
// PutStacks for those arguments aren't sunk, those heap locations will map to very trivial
// availabilities (they will be flush availabilities). But if sinking happens then those
// availabilities may become whatever. OSR exit should be able to handle this quite naturally,
// since those availabilities speak of the stack before the optimizing compiler stack frame is
// torn down.
if (verbose)
dataLog("After interference analysis: ", listDump(m_candidates), "\n");
}
void transform()
{
InsertionSet insertionSet(m_graph);
for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
for (unsigned nodeIndex = 0; nodeIndex < block->size(); ++nodeIndex) {
Node* node = block->at(nodeIndex);
auto getArrayLength = [&] (Node* candidate) -> Node* {
return emitCodeToGetArgumentsArrayLength(
insertionSet, candidate, nodeIndex, node->origin);
};
switch (node->op()) {
case CreateDirectArguments:
if (!m_candidates.contains(node))
break;
node->setOpAndDefaultFlags(PhantomDirectArguments);
break;
case CreateClonedArguments:
if (!m_candidates.contains(node))
break;
node->setOpAndDefaultFlags(PhantomClonedArguments);
break;
case GetFromArguments: {
Node* candidate = node->child1().node();
if (!m_candidates.contains(candidate))
break;
DFG_ASSERT(
m_graph, node,
node->child1()->op() == CreateDirectArguments
|| node->child1()->op() == PhantomDirectArguments);
VirtualRegister reg =
virtualRegisterForArgument(node->capturedArgumentsOffset().offset() + 1) +
node->origin.semantic.stackOffset();
StackAccessData* data = m_graph.m_stackAccessData.add(reg, FlushedJSValue);
node->convertToGetStack(data);
break;
}
case GetByOffset: {
Node* candidate = node->child2().node();
if (!m_candidates.contains(candidate))
break;
if (node->child2()->op() != PhantomClonedArguments)
break;
ASSERT(node->storageAccessData().offset == clonedArgumentsLengthPropertyOffset);
// Meh, this is kind of hackish - we use an Identity so that we can reuse the
// getArrayLength() helper.
node->convertToIdentityOn(getArrayLength(candidate));
break;
}
case GetArrayLength: {
Node* candidate = node->child1().node();
if (!m_candidates.contains(candidate))
break;
// Meh, this is kind of hackish - we use an Identity so that we can reuse the
// getArrayLength() helper.
node->convertToIdentityOn(getArrayLength(candidate));
break;
}
case GetByVal: {
// FIXME: For ClonedArguments, we would have already done a separate bounds check.
// This code will cause us to have two bounds checks - the original one that we
// already factored out in SSALoweringPhase, and the new one we insert here, which is
// often implicitly part of GetMyArgumentByVal. B3 will probably eliminate the
// second bounds check, but still - that's just silly.
// https://bugs.webkit.org/show_bug.cgi?id=143076
Node* candidate = node->child1().node();
if (!m_candidates.contains(candidate))
break;
Node* result = nullptr;
if (node->child2()->isInt32Constant()) {
unsigned index = node->child2()->asUInt32();
InlineCallFrame* inlineCallFrame = candidate->origin.semantic.inlineCallFrame;
bool safeToGetStack;
if (inlineCallFrame)
safeToGetStack = index < inlineCallFrame->arguments.size() - 1;
else {
safeToGetStack =
index < static_cast<unsigned>(codeBlock()->numParameters()) - 1;
}
if (safeToGetStack) {
StackAccessData* data;
VirtualRegister arg = virtualRegisterForArgument(index + 1);
if (inlineCallFrame)
arg += inlineCallFrame->stackOffset;
data = m_graph.m_stackAccessData.add(arg, FlushedJSValue);
if (!inlineCallFrame || inlineCallFrame->isVarargs()) {
insertionSet.insertNode(
nodeIndex, SpecNone, CheckInBounds, node->origin,
node->child2(), Edge(getArrayLength(candidate), Int32Use));
}
result = insertionSet.insertNode(
nodeIndex, node->prediction(), GetStack, node->origin, OpInfo(data));
}
}
if (!result) {
NodeType op;
if (node->arrayMode().isInBounds())
op = GetMyArgumentByVal;
else
op = GetMyArgumentByValOutOfBounds;
result = insertionSet.insertNode(
nodeIndex, node->prediction(), op, node->origin,
node->child1(), node->child2());
}
// Need to do this because we may have a data format conversion here.
node->convertToIdentityOn(result);
break;
}
case LoadVarargs: {
Node* candidate = node->child1().node();
if (!m_candidates.contains(candidate))
break;
LoadVarargsData* varargsData = node->loadVarargsData();
InlineCallFrame* inlineCallFrame = candidate->origin.semantic.inlineCallFrame;
if (inlineCallFrame
&& !inlineCallFrame->isVarargs()
&& inlineCallFrame->arguments.size() - varargsData->offset <= varargsData->limit) {
// LoadVarargs can exit, so it better be exitOK.
DFG_ASSERT(m_graph, node, node->origin.exitOK);
bool canExit = true;
Node* argumentCount = insertionSet.insertConstant(
nodeIndex, node->origin.withExitOK(canExit),
jsNumber(inlineCallFrame->arguments.size() - varargsData->offset));
insertionSet.insertNode(
nodeIndex, SpecNone, MovHint, node->origin.takeValidExit(canExit),
OpInfo(varargsData->count.offset()), Edge(argumentCount));
insertionSet.insertNode(
nodeIndex, SpecNone, PutStack, node->origin.withExitOK(canExit),
OpInfo(m_graph.m_stackAccessData.add(varargsData->count, FlushedInt32)),
Edge(argumentCount, KnownInt32Use));
DFG_ASSERT(m_graph, node, varargsData->limit - 1 >= varargsData->mandatoryMinimum);
// Define our limit to not include "this", since that's a bit easier to reason about.
unsigned limit = varargsData->limit - 1;
Node* undefined = nullptr;
for (unsigned storeIndex = 0; storeIndex < limit; ++storeIndex) {
// First determine if we have an element we can load, and load it if
// possible.
unsigned loadIndex = storeIndex + varargsData->offset;
Node* value;
if (loadIndex + 1 < inlineCallFrame->arguments.size()) {
VirtualRegister reg =
virtualRegisterForArgument(loadIndex + 1) +
inlineCallFrame->stackOffset;
StackAccessData* data = m_graph.m_stackAccessData.add(
reg, FlushedJSValue);
value = insertionSet.insertNode(
nodeIndex, SpecNone, GetStack, node->origin.withExitOK(canExit),
OpInfo(data));
} else {
// FIXME: We shouldn't have to store anything if
// storeIndex >= varargsData->mandatoryMinimum, but we will still
// have GetStacks in that range. So if we don't do the stores, we'll
// have degenerate IR: we'll have GetStacks of something that didn't
// have PutStacks.
// https://bugs.webkit.org/show_bug.cgi?id=147434
if (!undefined) {
undefined = insertionSet.insertConstant(
nodeIndex, node->origin.withExitOK(canExit), jsUndefined());
}
value = undefined;
}
// Now that we have a value, store it.
VirtualRegister reg = varargsData->start + storeIndex;
StackAccessData* data =
m_graph.m_stackAccessData.add(reg, FlushedJSValue);
insertionSet.insertNode(
nodeIndex, SpecNone, MovHint, node->origin.takeValidExit(canExit),
OpInfo(reg.offset()), Edge(value));
insertionSet.insertNode(
nodeIndex, SpecNone, PutStack, node->origin.withExitOK(canExit),
OpInfo(data), Edge(value));
}
node->remove();
node->origin.exitOK = canExit;
break;
}
node->setOpAndDefaultFlags(ForwardVarargs);
break;
}
case CallVarargs:
case ConstructVarargs:
case TailCallVarargs:
case TailCallVarargsInlinedCaller: {
Node* candidate = node->child3().node();
if (!m_candidates.contains(candidate))
break;
CallVarargsData* varargsData = node->callVarargsData();
InlineCallFrame* inlineCallFrame = candidate->origin.semantic.inlineCallFrame;
if (inlineCallFrame && !inlineCallFrame->isVarargs()) {
Vector<Node*> arguments;
for (unsigned i = 1 + varargsData->firstVarArgOffset; i < inlineCallFrame->arguments.size(); ++i) {
StackAccessData* data = m_graph.m_stackAccessData.add(
virtualRegisterForArgument(i) + inlineCallFrame->stackOffset,
FlushedJSValue);
Node* value = insertionSet.insertNode(
nodeIndex, SpecNone, GetStack, node->origin, OpInfo(data));
arguments.append(value);
}
unsigned firstChild = m_graph.m_varArgChildren.size();
m_graph.m_varArgChildren.append(node->child1());
m_graph.m_varArgChildren.append(node->child2());
for (Node* argument : arguments)
m_graph.m_varArgChildren.append(Edge(argument));
switch (node->op()) {
case CallVarargs:
node->setOpAndDefaultFlags(Call);
break;
case ConstructVarargs:
node->setOpAndDefaultFlags(Construct);
break;
case TailCallVarargs:
node->setOpAndDefaultFlags(TailCall);
break;
case TailCallVarargsInlinedCaller:
node->setOpAndDefaultFlags(TailCallInlinedCaller);
break;
default:
RELEASE_ASSERT_NOT_REACHED();
}
node->children = AdjacencyList(
AdjacencyList::Variable,
firstChild, m_graph.m_varArgChildren.size() - firstChild);
break;
}
switch (node->op()) {
case CallVarargs:
node->setOpAndDefaultFlags(CallForwardVarargs);
break;
case ConstructVarargs:
node->setOpAndDefaultFlags(ConstructForwardVarargs);
break;
case TailCallVarargs:
node->setOpAndDefaultFlags(TailCallForwardVarargs);
break;
case TailCallVarargsInlinedCaller:
node->setOpAndDefaultFlags(TailCallForwardVarargsInlinedCaller);
break;
default:
RELEASE_ASSERT_NOT_REACHED();
}
break;
}
case CheckArray:
case GetButterfly: {
if (!m_candidates.contains(node->child1().node()))
break;
node->remove();
break;
}
default:
break;
}
}
insertionSet.execute(block);
}
}
HashSet<Node*> m_candidates;
};
} // anonymous namespace
bool performArgumentsElimination(Graph& graph)
{
return runPhase<ArgumentsEliminationPhase>(graph);
}
} } // namespace JSC::DFG
#endif // ENABLE(DFG_JIT)