// Copyright 2014 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/common-operator.h" #include "src/compiler/control-reducer.h" #include "src/compiler/graph.h" #include "src/compiler/js-graph.h" #include "src/compiler/node-matchers.h" #include "src/compiler/node-properties-inl.h" #include "src/zone-containers.h" namespace v8 { namespace internal { namespace compiler { enum VisitState { kUnvisited = 0, kOnStack = 1, kRevisit = 2, kVisited = 3 }; enum Reachability { kFromStart = 8 }; #define TRACE(x) \ if (FLAG_trace_turbo_reduction) PrintF x class ControlReducerImpl { public: ControlReducerImpl(Zone* zone, JSGraph* jsgraph, CommonOperatorBuilder* common) : zone_(zone), jsgraph_(jsgraph), common_(common), state_(jsgraph->graph()->NodeCount(), kUnvisited, zone_), stack_(zone_), revisit_(zone_), dead_(NULL) {} Zone* zone_; JSGraph* jsgraph_; CommonOperatorBuilder* common_; ZoneVector state_; ZoneDeque stack_; ZoneDeque revisit_; Node* dead_; void Reduce() { Push(graph()->end()); do { // Process the node on the top of the stack, potentially pushing more // or popping the node off the stack. ReduceTop(); // If the stack becomes empty, revisit any nodes in the revisit queue. // If no nodes in the revisit queue, try removing dead loops. // If no dead loops, then finish. } while (!stack_.empty() || TryRevisit() || RepairAndRemoveLoops()); } bool TryRevisit() { while (!revisit_.empty()) { Node* n = revisit_.back(); revisit_.pop_back(); if (state_[n->id()] == kRevisit) { // state can change while in queue. Push(n); return true; } } return false; } // Repair the graph after the possible creation of non-terminating or dead // loops. Removing dead loops can produce more opportunities for reduction. bool RepairAndRemoveLoops() { // TODO(turbofan): we can skip this if the graph has no loops, but // we have to be careful about proper loop detection during reduction. // Gather all nodes backwards-reachable from end (through inputs). state_.assign(graph()->NodeCount(), kUnvisited); NodeVector nodes(zone_); AddNodesReachableFromEnd(nodes); // Walk forward through control nodes, looking for back edges to nodes // that are not connected to end. Those are non-terminating loops (NTLs). Node* start = graph()->start(); ZoneVector fw_reachability(graph()->NodeCount(), 0, zone_); fw_reachability[start->id()] = kFromStart | kOnStack; stack_.push_back(start); while (!stack_.empty()) { Node* node = stack_.back(); TRACE(("ControlFw: #%d:%s\n", node->id(), node->op()->mnemonic())); bool pop = true; for (Node* const succ : node->uses()) { byte reach = fw_reachability[succ->id()]; if ((reach & kOnStack) != 0 && state_[succ->id()] != kVisited) { // {succ} is on stack and not reachable from end. ConnectNTL(nodes, succ); fw_reachability.resize(graph()->NodeCount(), 0); pop = false; // continue traversing inputs to this node. break; } if ((reach & kFromStart) == 0 && IrOpcode::IsControlOpcode(succ->opcode())) { // {succ} is a control node and not yet reached from start. fw_reachability[succ->id()] |= kFromStart | kOnStack; stack_.push_back(succ); pop = false; // "recurse" into successor control node. break; } } if (pop) { fw_reachability[node->id()] &= ~kOnStack; stack_.pop_back(); } } // Trim references from dead nodes to live nodes first. jsgraph_->GetCachedNodes(&nodes); TrimNodes(nodes); // Any control nodes not reachable from start are dead, even loops. for (size_t i = 0; i < nodes.size(); i++) { Node* node = nodes[i]; byte reach = fw_reachability[node->id()]; if ((reach & kFromStart) == 0 && IrOpcode::IsControlOpcode(node->opcode())) { ReplaceNode(node, dead()); // uses will be added to revisit queue. } } return TryRevisit(); // try to push a node onto the stack. } // Connect {loop}, the header of a non-terminating loop, to the end node. void ConnectNTL(NodeVector& nodes, Node* loop) { TRACE(("ConnectNTL: #%d:%s\n", loop->id(), loop->op()->mnemonic())); if (loop->opcode() != IrOpcode::kTerminate) { // Insert a {Terminate} node if the loop has effects. ZoneDeque effects(zone_); for (Node* const use : loop->uses()) { if (use->opcode() == IrOpcode::kEffectPhi) effects.push_back(use); } int count = static_cast(effects.size()); if (count > 0) { Node** inputs = zone_->NewArray(1 + count); for (int i = 0; i < count; i++) inputs[i] = effects[i]; inputs[count] = loop; loop = graph()->NewNode(common_->Terminate(count), 1 + count, inputs); TRACE(("AddTerminate: #%d:%s[%d]\n", loop->id(), loop->op()->mnemonic(), count)); } } Node* to_add = loop; Node* end = graph()->end(); CHECK_EQ(IrOpcode::kEnd, end->opcode()); Node* merge = end->InputAt(0); if (merge == NULL || merge->opcode() == IrOpcode::kDead) { // The end node died; just connect end to {loop}. end->ReplaceInput(0, loop); } else if (merge->opcode() != IrOpcode::kMerge) { // Introduce a final merge node for {end->InputAt(0)} and {loop}. merge = graph()->NewNode(common_->Merge(2), merge, loop); end->ReplaceInput(0, merge); to_add = merge; } else { // Append a new input to the final merge at the end. merge->AppendInput(graph()->zone(), loop); merge->set_op(common_->Merge(merge->InputCount())); } nodes.push_back(to_add); state_.resize(graph()->NodeCount(), kUnvisited); state_[to_add->id()] = kVisited; AddBackwardsReachableNodes(nodes, nodes.size() - 1); } void AddNodesReachableFromEnd(NodeVector& nodes) { Node* end = graph()->end(); state_[end->id()] = kVisited; if (!end->IsDead()) { nodes.push_back(end); AddBackwardsReachableNodes(nodes, nodes.size() - 1); } } void AddBackwardsReachableNodes(NodeVector& nodes, size_t cursor) { while (cursor < nodes.size()) { Node* node = nodes[cursor++]; for (Node* const input : node->inputs()) { if (state_[input->id()] != kVisited) { state_[input->id()] = kVisited; nodes.push_back(input); } } } } void Trim() { // Gather all nodes backwards-reachable from end through inputs. state_.assign(graph()->NodeCount(), kUnvisited); NodeVector nodes(zone_); AddNodesReachableFromEnd(nodes); // Process cached nodes in the JSGraph too. jsgraph_->GetCachedNodes(&nodes); TrimNodes(nodes); } void TrimNodes(NodeVector& nodes) { // Remove dead->live edges. for (size_t j = 0; j < nodes.size(); j++) { Node* node = nodes[j]; for (UseIter i = node->uses().begin(); i != node->uses().end();) { size_t id = static_cast((*i)->id()); if (state_[id] != kVisited) { TRACE(("DeadLink: #%d:%s(%d) -> #%d:%s\n", (*i)->id(), (*i)->op()->mnemonic(), i.index(), node->id(), node->op()->mnemonic())); i.UpdateToAndIncrement(NULL); } else { ++i; } } } #if DEBUG // Verify that no inputs to live nodes are NULL. for (size_t j = 0; j < nodes.size(); j++) { Node* node = nodes[j]; for (Node* const input : node->inputs()) { CHECK_NE(NULL, input); } for (Node* const use : node->uses()) { size_t id = static_cast(use->id()); CHECK_EQ(kVisited, state_[id]); } } #endif } // Reduce the node on the top of the stack. // If an input {i} is not yet visited or needs to be revisited, push {i} onto // the stack and return. Otherwise, all inputs are visited, so apply // reductions for {node} and pop it off the stack. void ReduceTop() { size_t height = stack_.size(); Node* node = stack_.back(); if (node->IsDead()) return Pop(); // Node was killed while on stack. TRACE(("ControlReduce: #%d:%s\n", node->id(), node->op()->mnemonic())); // Recurse on an input if necessary. for (Node* const input : node->inputs()) { if (Recurse(input)) return; } // All inputs should be visited or on stack. Apply reductions to node. Node* replacement = ReduceNode(node); if (replacement != node) ReplaceNode(node, replacement); // After reducing the node, pop it off the stack. CHECK_EQ(static_cast(height), static_cast(stack_.size())); Pop(); // If there was a replacement, reduce it after popping {node}. if (replacement != node) Recurse(replacement); } // Push a node onto the stack if its state is {kUnvisited} or {kRevisit}. bool Recurse(Node* node) { size_t id = static_cast(node->id()); if (id < state_.size()) { if (state_[id] != kRevisit && state_[id] != kUnvisited) return false; } else { state_.resize((3 * id) / 2, kUnvisited); } Push(node); return true; } void Push(Node* node) { state_[node->id()] = kOnStack; stack_.push_back(node); } void Pop() { int pos = static_cast(stack_.size()) - 1; DCHECK_GE(pos, 0); DCHECK_EQ(kOnStack, state_[stack_[pos]->id()]); state_[stack_[pos]->id()] = kVisited; stack_.pop_back(); } // Queue a node to be revisited if it has been visited once already. void Revisit(Node* node) { size_t id = static_cast(node->id()); if (id < state_.size() && state_[id] == kVisited) { TRACE((" Revisit #%d:%s\n", node->id(), node->op()->mnemonic())); state_[id] = kRevisit; revisit_.push_back(node); } } Node* dead() { if (dead_ == NULL) dead_ = graph()->NewNode(common_->Dead()); return dead_; } //=========================================================================== // Reducer implementation: perform reductions on a node. //=========================================================================== Node* ReduceNode(Node* node) { if (node->op()->ControlInputCount() == 1) { // If a node has only one control input and it is dead, replace with dead. Node* control = NodeProperties::GetControlInput(node); if (control->opcode() == IrOpcode::kDead) { TRACE(("ControlDead: #%d:%s\n", node->id(), node->op()->mnemonic())); return control; } } // Reduce branches, phis, and merges. switch (node->opcode()) { case IrOpcode::kBranch: return ReduceBranch(node); case IrOpcode::kLoop: case IrOpcode::kMerge: return ReduceMerge(node); case IrOpcode::kSelect: return ReduceSelect(node); case IrOpcode::kPhi: case IrOpcode::kEffectPhi: return ReducePhi(node); default: return node; } } // Reduce redundant selects. Node* ReduceSelect(Node* const node) { Node* const cond = node->InputAt(0); Node* const tvalue = node->InputAt(1); Node* const fvalue = node->InputAt(2); if (tvalue == fvalue) return tvalue; switch (cond->opcode()) { case IrOpcode::kInt32Constant: return Int32Matcher(cond).Is(0) ? fvalue : tvalue; case IrOpcode::kInt64Constant: return Int64Matcher(cond).Is(0) ? fvalue : tvalue; case IrOpcode::kNumberConstant: return NumberMatcher(cond).Is(0) ? fvalue : tvalue; case IrOpcode::kHeapConstant: { Handle object = HeapObjectMatcher(cond).Value().handle(); if (object->IsTrue()) return tvalue; if (object->IsFalse()) return fvalue; break; } default: break; } return node; } // Reduce redundant phis. Node* ReducePhi(Node* node) { int n = node->InputCount(); if (n <= 1) return dead(); // No non-control inputs. if (n == 2) return node->InputAt(0); // Only one non-control input. Node* replacement = NULL; Node::Inputs inputs = node->inputs(); for (InputIter it = inputs.begin(); n > 1; --n, ++it) { Node* input = *it; if (input->opcode() == IrOpcode::kDead) continue; // ignore dead inputs. if (input != node && input != replacement) { // non-redundant input. if (replacement != NULL) return node; replacement = input; } } return replacement == NULL ? dead() : replacement; } // Reduce merges by trimming away dead inputs from the merge and phis. Node* ReduceMerge(Node* node) { // Count the number of live inputs. int live = 0; int index = 0; int live_index = 0; for (Node* const input : node->inputs()) { if (input->opcode() != IrOpcode::kDead) { live++; live_index = index; } index++; } if (live > 1 && live == node->InputCount()) return node; // nothing to do. TRACE(("ReduceMerge: #%d:%s (%d live)\n", node->id(), node->op()->mnemonic(), live)); if (live == 0) return dead(); // no remaining inputs. // Gather phis and effect phis to be edited. ZoneVector phis(zone_); for (Node* const use : node->uses()) { if (use->opcode() == IrOpcode::kPhi || use->opcode() == IrOpcode::kEffectPhi) { phis.push_back(use); } } if (live == 1) { // All phis are redundant. Replace them with their live input. for (Node* const phi : phis) ReplaceNode(phi, phi->InputAt(live_index)); // The merge itself is redundant. return node->InputAt(live_index); } // Edit phis in place, removing dead inputs and revisiting them. for (Node* const phi : phis) { TRACE((" PhiInMerge: #%d:%s (%d live)\n", phi->id(), phi->op()->mnemonic(), live)); RemoveDeadInputs(node, phi); Revisit(phi); } // Edit the merge in place, removing dead inputs. RemoveDeadInputs(node, node); return node; } // Reduce branches if they have constant inputs. Node* ReduceBranch(Node* node) { Node* cond = node->InputAt(0); bool is_true; switch (cond->opcode()) { case IrOpcode::kInt32Constant: is_true = !Int32Matcher(cond).Is(0); break; case IrOpcode::kInt64Constant: is_true = !Int64Matcher(cond).Is(0); break; case IrOpcode::kNumberConstant: is_true = !NumberMatcher(cond).Is(0); break; case IrOpcode::kHeapConstant: { Handle object = HeapObjectMatcher(cond).Value().handle(); if (object->IsTrue()) is_true = true; else if (object->IsFalse()) is_true = false; else return node; // TODO(turbofan): fold branches on strings, objects. break; } default: return node; } TRACE(("BranchReduce: #%d:%s = %s\n", node->id(), node->op()->mnemonic(), is_true ? "true" : "false")); // Replace IfTrue and IfFalse projections from this branch. Node* control = NodeProperties::GetControlInput(node); for (UseIter i = node->uses().begin(); i != node->uses().end();) { Node* to = *i; if (to->opcode() == IrOpcode::kIfTrue) { TRACE((" IfTrue: #%d:%s\n", to->id(), to->op()->mnemonic())); i.UpdateToAndIncrement(NULL); ReplaceNode(to, is_true ? control : dead()); } else if (to->opcode() == IrOpcode::kIfFalse) { TRACE((" IfFalse: #%d:%s\n", to->id(), to->op()->mnemonic())); i.UpdateToAndIncrement(NULL); ReplaceNode(to, is_true ? dead() : control); } else { ++i; } } return control; } // Remove inputs to {node} corresponding to the dead inputs to {merge} // and compact the remaining inputs, updating the operator. void RemoveDeadInputs(Node* merge, Node* node) { int pos = 0; for (int i = 0; i < node->InputCount(); i++) { // skip dead inputs. if (i < merge->InputCount() && merge->InputAt(i)->opcode() == IrOpcode::kDead) continue; // compact live inputs. if (pos != i) node->ReplaceInput(pos, node->InputAt(i)); pos++; } node->TrimInputCount(pos); if (node->opcode() == IrOpcode::kPhi) { node->set_op(common_->Phi(OpParameter(node->op()), pos - 1)); } else if (node->opcode() == IrOpcode::kEffectPhi) { node->set_op(common_->EffectPhi(pos - 1)); } else if (node->opcode() == IrOpcode::kMerge) { node->set_op(common_->Merge(pos)); } else if (node->opcode() == IrOpcode::kLoop) { node->set_op(common_->Loop(pos)); } else { UNREACHABLE(); } } // Replace uses of {node} with {replacement} and revisit the uses. void ReplaceNode(Node* node, Node* replacement) { if (node == replacement) return; TRACE((" Replace: #%d:%s with #%d:%s\n", node->id(), node->op()->mnemonic(), replacement->id(), replacement->op()->mnemonic())); for (Node* const use : node->uses()) { // Don't revisit this node if it refers to itself. if (use != node) Revisit(use); } node->ReplaceUses(replacement); node->Kill(); } Graph* graph() { return jsgraph_->graph(); } }; void ControlReducer::ReduceGraph(Zone* zone, JSGraph* jsgraph, CommonOperatorBuilder* common) { ControlReducerImpl impl(zone, jsgraph, common); impl.Reduce(); impl.Trim(); } void ControlReducer::TrimGraph(Zone* zone, JSGraph* jsgraph) { ControlReducerImpl impl(zone, jsgraph, NULL); impl.Trim(); } Node* ControlReducer::ReducePhiForTesting(JSGraph* jsgraph, CommonOperatorBuilder* common, Node* node) { Zone zone(jsgraph->graph()->zone()->isolate()); ControlReducerImpl impl(&zone, jsgraph, common); return impl.ReducePhi(node); } Node* ControlReducer::ReduceMergeForTesting(JSGraph* jsgraph, CommonOperatorBuilder* common, Node* node) { Zone zone(jsgraph->graph()->zone()->isolate()); ControlReducerImpl impl(&zone, jsgraph, common); return impl.ReduceMerge(node); } Node* ControlReducer::ReduceBranchForTesting(JSGraph* jsgraph, CommonOperatorBuilder* common, Node* node) { Zone zone(jsgraph->graph()->zone()->isolate()); ControlReducerImpl impl(&zone, jsgraph, common); return impl.ReduceBranch(node); } } } } // namespace v8::internal::compiler