// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/compiler/js-inlining-heuristic.h"
#include "src/compilation-info.h"
#include "src/compiler/common-operator.h"
#include "src/compiler/node-matchers.h"
#include "src/compiler/simplified-operator.h"
#include "src/objects-inl.h"
namespace v8 {
namespace internal {
namespace compiler {
#define TRACE(...) \
do { \
if (FLAG_trace_turbo_inlining) PrintF(__VA_ARGS__); \
} while (false)
namespace {
int CollectFunctions(Node* node, Handle<JSFunction>* functions,
int functions_size) {
DCHECK_NE(0, functions_size);
HeapObjectMatcher m(node);
if (m.HasValue() && m.Value()->IsJSFunction()) {
functions[0] = Handle<JSFunction>::cast(m.Value());
return 1;
}
if (m.IsPhi()) {
int const value_input_count = m.node()->op()->ValueInputCount();
if (value_input_count > functions_size) return 0;
for (int n = 0; n < value_input_count; ++n) {
HeapObjectMatcher m(node->InputAt(n));
if (!m.HasValue() || !m.Value()->IsJSFunction()) return 0;
functions[n] = Handle<JSFunction>::cast(m.Value());
}
return value_input_count;
}
return 0;
}
bool CanInlineFunction(Handle<JSFunction> function) {
// Built-in functions are handled by the JSBuiltinReducer.
if (function->shared()->HasBuiltinFunctionId()) return false;
// Don't inline builtins.
if (function->shared()->IsBuiltin()) return false;
// Quick check on the size of the AST to avoid parsing large candidate.
if (function->shared()->ast_node_count() > FLAG_max_inlined_nodes) {
return false;
}
// Avoid inlining across the boundary of asm.js code.
if (function->shared()->asm_function()) return false;
return true;
}
} // namespace
Reduction JSInliningHeuristic::Reduce(Node* node) {
if (!IrOpcode::IsInlineeOpcode(node->opcode())) return NoChange();
// Check if we already saw that {node} before, and if so, just skip it.
if (seen_.find(node->id()) != seen_.end()) return NoChange();
seen_.insert(node->id());
// Check if the {node} is an appropriate candidate for inlining.
Node* callee = node->InputAt(0);
Candidate candidate;
candidate.node = node;
candidate.num_functions =
CollectFunctions(callee, candidate.functions, kMaxCallPolymorphism);
if (candidate.num_functions == 0) {
return NoChange();
} else if (candidate.num_functions > 1 && !FLAG_polymorphic_inlining) {
TRACE(
"Not considering call site #%d:%s, because polymorphic inlining "
"is disabled\n",
node->id(), node->op()->mnemonic());
return NoChange();
}
// Functions marked with %SetForceInlineFlag are immediately inlined.
bool can_inline = false, force_inline = true;
for (int i = 0; i < candidate.num_functions; ++i) {
Handle<JSFunction> function = candidate.functions[i];
if (!function->shared()->force_inline()) {
force_inline = false;
}
if (CanInlineFunction(function)) {
can_inline = true;
}
}
if (force_inline) return InlineCandidate(candidate);
if (!can_inline) return NoChange();
// Stop inlining once the maximum allowed level is reached.
int level = 0;
for (Node* frame_state = NodeProperties::GetFrameStateInput(node);
frame_state->opcode() == IrOpcode::kFrameState;
frame_state = NodeProperties::GetFrameStateInput(frame_state)) {
FrameStateInfo const& frame_info = OpParameter<FrameStateInfo>(frame_state);
if (FrameStateFunctionInfo::IsJSFunctionType(frame_info.type())) {
if (++level > FLAG_max_inlining_levels) {
TRACE(
"Not considering call site #%d:%s, because inlining depth "
"%d exceeds maximum allowed level %d\n",
node->id(), node->op()->mnemonic(), level,
FLAG_max_inlining_levels);
return NoChange();
}
}
}
// Gather feedback on how often this call site has been hit before.
if (node->opcode() == IrOpcode::kJSCallFunction) {
CallFunctionParameters const p = CallFunctionParametersOf(node->op());
candidate.frequency = p.frequency();
} else {
CallConstructParameters const p = CallConstructParametersOf(node->op());
candidate.frequency = p.frequency();
}
// Handling of special inlining modes right away:
// - For restricted inlining: stop all handling at this point.
// - For stressing inlining: immediately handle all functions.
switch (mode_) {
case kRestrictedInlining:
return NoChange();
case kStressInlining:
return InlineCandidate(candidate);
case kGeneralInlining:
break;
}
// In the general case we remember the candidate for later.
candidates_.insert(candidate);
return NoChange();
}
void JSInliningHeuristic::Finalize() {
if (candidates_.empty()) return; // Nothing to do without candidates.
if (FLAG_trace_turbo_inlining) PrintCandidates();
// We inline at most one candidate in every iteration of the fixpoint.
// This is to ensure that we don't consume the full inlining budget
// on things that aren't called very often.
// TODO(bmeurer): Use std::priority_queue instead of std::set here.
while (!candidates_.empty()) {
if (cumulative_count_ > FLAG_max_inlined_nodes_cumulative) return;
auto i = candidates_.begin();
Candidate candidate = *i;
candidates_.erase(i);
// Make sure we don't try to inline dead candidate nodes.
if (!candidate.node->IsDead()) {
Reduction const reduction = InlineCandidate(candidate);
if (reduction.Changed()) return;
}
}
}
Reduction JSInliningHeuristic::InlineCandidate(Candidate const& candidate) {
int const num_calls = candidate.num_functions;
Node* const node = candidate.node;
if (num_calls == 1) {
Handle<JSFunction> function = candidate.functions[0];
Reduction const reduction = inliner_.ReduceJSCall(node, function);
if (reduction.Changed()) {
cumulative_count_ += function->shared()->ast_node_count();
}
return reduction;
}
// Expand the JSCallFunction/JSCallConstruct node to a subgraph first if
// we have multiple known target functions.
DCHECK_LT(1, num_calls);
Node* calls[kMaxCallPolymorphism + 1];
Node* if_successes[kMaxCallPolymorphism];
Node* callee = NodeProperties::GetValueInput(node, 0);
Node* fallthrough_control = NodeProperties::GetControlInput(node);
// Setup the inputs for the cloned call nodes.
int const input_count = node->InputCount();
Node** inputs = graph()->zone()->NewArray<Node*>(input_count);
for (int i = 0; i < input_count; ++i) {
inputs[i] = node->InputAt(i);
}
// Create the appropriate control flow to dispatch to the cloned calls.
for (int i = 0; i < num_calls; ++i) {
Node* target = jsgraph()->HeapConstant(candidate.functions[i]);
if (i != (num_calls - 1)) {
Node* check =
graph()->NewNode(simplified()->ReferenceEqual(), callee, target);
Node* branch =
graph()->NewNode(common()->Branch(), check, fallthrough_control);
fallthrough_control = graph()->NewNode(common()->IfFalse(), branch);
if_successes[i] = graph()->NewNode(common()->IfTrue(), branch);
} else {
if_successes[i] = fallthrough_control;
}
// The first input to the call is the actual target (which we specialize
// to the known {target}); the last input is the control dependency.
inputs[0] = target;
inputs[input_count - 1] = if_successes[i];
calls[i] = graph()->NewNode(node->op(), input_count, inputs);
if_successes[i] = graph()->NewNode(common()->IfSuccess(), calls[i]);
}
// Check if we have an exception projection for the call {node}.
Node* if_exception = nullptr;
for (Edge const edge : node->use_edges()) {
if (NodeProperties::IsControlEdge(edge) &&
edge.from()->opcode() == IrOpcode::kIfException) {
if_exception = edge.from();
break;
}
}
if (if_exception != nullptr) {
// Morph the {if_exception} projection into a join.
Node* if_exceptions[kMaxCallPolymorphism + 1];
for (int i = 0; i < num_calls; ++i) {
if_exceptions[i] =
graph()->NewNode(common()->IfException(), calls[i], calls[i]);
}
Node* exception_control =
graph()->NewNode(common()->Merge(num_calls), num_calls, if_exceptions);
if_exceptions[num_calls] = exception_control;
Node* exception_effect = graph()->NewNode(common()->EffectPhi(num_calls),
num_calls + 1, if_exceptions);
Node* exception_value = graph()->NewNode(
common()->Phi(MachineRepresentation::kTagged, num_calls), num_calls + 1,
if_exceptions);
ReplaceWithValue(if_exception, exception_value, exception_effect,
exception_control);
}
// Morph the call site into the dispatched call sites.
Node* control =
graph()->NewNode(common()->Merge(num_calls), num_calls, if_successes);
calls[num_calls] = control;
Node* effect =
graph()->NewNode(common()->EffectPhi(num_calls), num_calls + 1, calls);
Node* value =
graph()->NewNode(common()->Phi(MachineRepresentation::kTagged, num_calls),
num_calls + 1, calls);
ReplaceWithValue(node, value, effect, control);
// Inline the individual, cloned call sites.
for (int i = 0; i < num_calls; ++i) {
Handle<JSFunction> function = candidate.functions[i];
Node* node = calls[i];
Reduction const reduction = inliner_.ReduceJSCall(node, function);
if (reduction.Changed()) {
cumulative_count_ += function->shared()->ast_node_count();
}
}
return Replace(value);
}
bool JSInliningHeuristic::CandidateCompare::operator()(
const Candidate& left, const Candidate& right) const {
if (left.frequency > right.frequency) {
return true;
} else if (left.frequency < right.frequency) {
return false;
} else {
return left.node->id() > right.node->id();
}
}
void JSInliningHeuristic::PrintCandidates() {
PrintF("Candidates for inlining (size=%zu):\n", candidates_.size());
for (const Candidate& candidate : candidates_) {
PrintF(" #%d:%s, frequency:%g\n", candidate.node->id(),
candidate.node->op()->mnemonic(), candidate.frequency);
for (int i = 0; i < candidate.num_functions; ++i) {
Handle<JSFunction> function = candidate.functions[i];
PrintF(" - size:%d, name: %s\n", function->shared()->ast_node_count(),
function->shared()->DebugName()->ToCString().get());
}
}
}
Graph* JSInliningHeuristic::graph() const { return jsgraph()->graph(); }
CommonOperatorBuilder* JSInliningHeuristic::common() const {
return jsgraph()->common();
}
SimplifiedOperatorBuilder* JSInliningHeuristic::simplified() const {
return jsgraph()->simplified();
}
} // namespace compiler
} // namespace internal
} // namespace v8