v8
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++.
Loading...
Searching...
No Matches
dead-code-elimination-reducer.h
Go to the documentation of this file.
1// Copyright 2022 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#ifndef V8_COMPILER_TURBOSHAFT_DEAD_CODE_ELIMINATION_REDUCER_H_
6#define V8_COMPILER_TURBOSHAFT_DEAD_CODE_ELIMINATION_REDUCER_H_
7
8#include <iomanip>
9#include <optional>
10
11#include "src/common/globals.h"
18
20
22
23// General overview
24//
25// DeadCodeAnalysis iterates the graph backwards to propagate liveness
26// information. This information consists of the ControlState and the
27// OperationState.
28//
29// OperationState reflects the liveness of operations. An operation is live if
30//
31// 1) The operation has the `IsRequiredWhenUnused()` property.
32// 2) Any of its outputs is live (is used in a live operation).
33//
34// If the operation is not live, it is dead and can be eliminated.
35//
36// ControlState describes to which block we could jump immediately without
37// changing the program semantics. That is missing any side effects, required
38// control flow or any live operations. This information is then used
39// at BranchOps to rewrite them to a GotoOp towards the corresponding block.
40// From the output control state(s) c after an operation, the control state c'
41// before the operation is computed as follows:
42//
43// | Bi if ct, cf are Bi or Unreachable
44// c' = [Branch](ct, cf) = {
45// | NotEliminatable otherwise
46//
47// And if c' = Bi, then the BranchOp can be rewritten into GotoOp(Bi).
48//
49// | NotEliminatable if Op is live
50// c' = [Op](c) = {
51// | c otherwise
52//
53// | Bk if c = Bk
54// c' = [Merge i](c) = { Bi if Merge i has no live phis
55// | NotEliminatable otherwise
56//
57// Where Merge is an imaginary operation at the start of every merge block. This
58// is the important part for the analysis. If block `Merge i` does not have any
59// live phi operations, then we don't necessarily need to distinguish the
60// control flow paths going into that block and if we further don't encounter
61// any live operations along any of the paths leading to `Merge i`
62// starting at some BranchOp, we can skip both branches and eliminate the
63// control flow entirely by rewriting the BranchOp into a GotoOp(Bi). Notice
64// that if the control state already describes a potential Goto-target Bk, then
65// we do not replace that in order to track the farthest block we can jump to.
66
68 // Lattice:
69 //
70 // NotEliminatable
71 // / | \
72 // B1 ... Bn
73 // \ | /
74 // Unreachable
75 //
76 // We use ControlState to propagate information during the analysis about how
77 // branches can be rewritten. Read the values like this:
78 // - NotEliminatable: We cannot rewrite a branch, because we need the control
79 // flow (e.g. because we have seen live operations on either branch or need
80 // the phi at the merge).
81 // - Bj: Control can be rewritten to go directly to Block Bj, because all
82 // paths to that block are free of live operations.
83 // - Unreachable: This is the bottom element and it represents that we haven't
84 // seen anything live yet and are free to rewrite branches to any block
85 // reachable from the current block.
91
96 return ControlState{kBlock, block};
97 }
99
101 : kind(kind), block(block) {}
102
104 const ControlState& rhs) {
105 switch (lhs.kind) {
107 return rhs;
108 case Kind::kBlock: {
109 if (rhs.kind == Kind::kUnreachable) return lhs;
110 if (rhs.kind == Kind::kNotEliminatable) return rhs;
111 if (lhs.block == rhs.block) return lhs;
112 return NotEliminatable();
113 }
115 return lhs;
116 }
117 }
118
121};
122
123inline std::ostream& operator<<(std::ostream& stream,
124 const ControlState& state) {
125 switch (state.kind) {
127 return stream << "NotEliminatable";
129 return stream << "Block(" << state.block << ")";
131 return stream << "Unreachable";
132 }
133}
134
135inline bool operator==(const ControlState& lhs, const ControlState& rhs) {
136 if (lhs.kind != rhs.kind) return false;
137 if (lhs.kind == ControlState::kBlock) {
139 return lhs.block == rhs.block;
140 }
141 return true;
142}
143
144inline bool operator!=(const ControlState& lhs, const ControlState& rhs) {
145 return !(lhs == rhs);
146}
147
149 // Lattice:
150 //
151 // Live
152 // |
153 // Dead
154 //
155 // Describes the liveness state of an operation.
156 enum Liveness : uint8_t {
159 };
160
162 static_assert(kDead == 0 && kLive == 1);
163 return static_cast<Liveness>(lhs | rhs);
164 }
165};
166
167inline std::ostream& operator<<(std::ostream& stream,
168 OperationState::Liveness liveness) {
169 switch (liveness) {
171 return stream << "Dead";
173 return stream << "Live";
174 }
175 UNREACHABLE();
176}
177
179 public:
181 : graph_(graph),
182 liveness_(graph.op_id_count(), OperationState::kDead, phase_zone,
183 &graph),
184 entry_control_state_(graph.block_count(), ControlState::Unreachable(),
185 phase_zone),
187
188 template <bool trace_analysis>
189 std::pair<FixedOpIndexSidetable<OperationState::Liveness>,
191 Run() {
192 if constexpr (trace_analysis) {
193 std::cout << "===== Running Dead Code Analysis =====\n";
194 }
195 for (uint32_t unprocessed_count = graph_.block_count();
196 unprocessed_count > 0;) {
197 BlockIndex block_index = static_cast<BlockIndex>(unprocessed_count - 1);
198 --unprocessed_count;
199
200 const Block& block = graph_.Get(block_index);
201 ProcessBlock<trace_analysis>(block, &unprocessed_count);
202 }
203
204 if constexpr (trace_analysis) {
205 std::cout << "===== Results =====\n== Operation State ==\n";
206 for (Block b : graph_.blocks()) {
207 std::cout << PrintAsBlockHeader{b} << ":\n";
208 for (OpIndex index : graph_.OperationIndices(b)) {
209 std::cout << " " << std::setw(8) << liveness_[index] << " "
210 << std::setw(3) << index.id() << ": " << graph_.Get(index)
211 << "\n";
212 }
213 }
214
215 std::cout << "== Rewritable Branches ==\n";
216 for (auto [branch_id, target] : rewritable_branch_targets_) {
217 DCHECK(target.valid());
218 std::cout << " " << std::setw(3) << branch_id << ": Branch ==> Goto "
219 << target.id() << "\n";
220 }
221 std::cout << "==========\n";
222 }
223
224 return {std::move(liveness_), std::move(rewritable_branch_targets_)};
225 }
226
227 template <bool trace_analysis>
228 void ProcessBlock(const Block& block, uint32_t* unprocessed_count) {
229 if constexpr (trace_analysis) {
230 std::cout << "\n==========\n=== Processing " << PrintAsBlockHeader{block}
231 << ":\n==========\nEXIT CONTROL STATE\n";
232 }
233 auto successors = SuccessorBlocks(block.LastOperation(graph_));
234 ControlState control_state = ControlState::Unreachable();
235 for (size_t i = 0; i < successors.size(); ++i) {
236 const auto& r = entry_control_state_[successors[i]->index()];
237 if constexpr (trace_analysis) {
238 std::cout << " Successor " << successors[i]->index() << ": " << r
239 << "\n";
240 }
241 control_state = ControlState::LeastUpperBound(control_state, r);
242 }
243 if constexpr (trace_analysis)
244 std::cout << "Combined: " << control_state << "\n";
245
246 // If control_state == ControlState::Block(b), then the merge block b is
247 // reachable through every path starting at the current block without any
248 // live operations.
249
250 if constexpr (trace_analysis) std::cout << "OPERATION STATE\n";
251 auto op_range = graph_.OperationIndices(block);
252 bool has_live_phis = false;
253 for (auto it = op_range.end(); it != op_range.begin();) {
254 --it;
255 OpIndex index = *it;
256 const Operation& op = graph_.Get(index);
257 if constexpr (trace_analysis) std::cout << index << ":" << op << "\n";
259
260 if (op.Is<DeadOp>()) {
261 // Operation is already recognized as dead by a previous analysis.
263 } else if (op.Is<CallOp>()) {
264 // The function contains a call, so it's not a leaf function.
265 is_leaf_function_ = false;
266 } else if (op.Is<BranchOp>() || op.Is<GotoOp>()) {
267 if (control_state != ControlState::NotEliminatable()) {
268 // Branch is still dead.
270 // If we know a target block we can rewrite into a goto.
271 if (control_state.kind == ControlState::kBlock) {
272 BlockIndex target = control_state.block;
273 DCHECK(target.valid());
275 }
276 } else {
277 // Branch is live. We cannot rewrite it.
278 op_state = OperationState::kLive;
279 rewritable_branch_targets_.remove(index);
280 }
281 } else if (op.IsRequiredWhenUnused()) {
282 op_state = OperationState::kLive;
283 } else if (op.Is<PhiOp>()) {
284 has_live_phis = has_live_phis || (op_state == OperationState::kLive);
285
286 if (block.IsLoop()) {
287 const PhiOp& phi = op.Cast<PhiOp>();
288 // Check if the operation state of the input coming from the backedge
289 // changes the liveness of the phi. In that case, trigger a revisit of
290 // the loop.
291 if (liveness_[phi.inputs()[PhiOp::kLoopPhiBackEdgeIndex]] <
292 op_state) {
293 if constexpr (trace_analysis) {
294 std::cout
295 << "Operation state has changed. Need to revisit loop.\n";
296 }
297 Block* backedge = block.LastPredecessor();
298 // Revisit the loop by increasing the {unprocessed_count} to include
299 // all blocks of the loop.
300 *unprocessed_count =
301 std::max(*unprocessed_count, backedge->index().id() + 1);
302 }
303 }
304 }
305
306 // TODO(nicohartmann@): Handle Stack Guards to allow elimination of
307 // otherwise empty loops.
308 //
309 // if(const CallOp* call = op.TryCast<CallOp>()) {
310 // if(std::string(call->descriptor->descriptor->debug_name())
311 // == "StackGuard") {
312 // DCHECK_EQ(op_state, OperationState::kLive);
313 // op_state = OperationState::kWeakLive;
314 // }
315 // }
316
317 DCHECK_LE(liveness_[index], op_state);
318 // If everything is still dead. We don't need to update anything.
319 if (op_state == OperationState::kDead) continue;
320
321 // We have a live operation.
322 if constexpr (trace_analysis) {
323 std::cout << " " << op_state << " <== " << liveness_[index] << "\n";
324 }
325 liveness_[index] = op_state;
326
327 if constexpr (trace_analysis) {
328 if (op.input_count > 0) std::cout << " Updating inputs:\n";
329 }
330 for (OpIndex input : op.inputs()) {
331 auto old_input_state = liveness_[input];
332 auto new_input_state =
333 OperationState::LeastUpperBound(old_input_state, op_state);
334 if constexpr (trace_analysis) {
335 std::cout << " " << input << ": " << new_input_state
336 << " <== " << old_input_state << " || " << op_state << "\n";
337 }
338 liveness_[input] = new_input_state;
339 }
340
341 if (op_state == OperationState::kLive &&
342 control_state != ControlState::NotEliminatable()) {
343 // This block has live operations, which means that we can't skip it.
344 // Reset the ControlState to NotEliminatable.
345 if constexpr (trace_analysis) {
346 std::cout << "Block has live operations. New control state: "
348 }
349 control_state = ControlState::NotEliminatable();
350 }
351 }
352
353 if constexpr (trace_analysis) {
354 std::cout << "ENTRY CONTROL STATE\nAfter operations: " << control_state
355 << "\n";
356 }
357
358 // If this block is a merge and we don't have any live phis, it is a
359 // potential target for branch redirection.
360 if (block.IsMerge()) {
361 if (!has_live_phis) {
362 if (control_state.kind != ControlState::kBlock) {
363 control_state = ControlState::Block(block.index());
364 if constexpr (trace_analysis) {
365 std::cout
366 << "Block is loop or merge and has no live phi operations.\n";
367 }
368 } else if constexpr (trace_analysis) {
369 std::cout << "Block is loop or merge and has no live phi "
370 "operations.\nControl state already has a goto block: "
371 << control_state << "\n";
372 }
373 }
374 } else if (block.IsLoop()) {
375 // If this is a loop, we reset the control state to avoid jumps into the
376 // middle of the loop. In particular, this is required to prevent
377 // introducing new backedges when blocks towards the end of the loop body
378 // want to jump to a block at the beginning (past the header).
379 control_state = ControlState::NotEliminatable();
380 if constexpr (trace_analysis) {
381 std::cout << "Block is loop header. Resetting control state: "
382 << control_state << "\n";
383 }
384
385 if (entry_control_state_[block.index()] != control_state) {
386 if constexpr (trace_analysis) {
387 std::cout << "Control state has changed. Need to revisit loop.\n";
388 }
389 Block* backedge = block.LastPredecessor();
390 DCHECK_NOT_NULL(backedge);
391 // Revisit the loop by increasing the {unprocessed_count} to include
392 // all blocks of the loop.
393 *unprocessed_count =
394 std::max(*unprocessed_count, backedge->index().id() + 1);
395 }
396 }
397
398 if constexpr (trace_analysis) {
399 std::cout << "Final: " << control_state << "\n";
400 }
401 entry_control_state_[block.index()] = control_state;
402 }
403
404 bool is_leaf_function() const { return is_leaf_function_; }
405
406 private:
411 // The stack check at function entry of leaf functions can be eliminated, as
412 // it is guaranteed that another stack check will be hit eventually. This flag
413 // records if the current function is a leaf function.
414 bool is_leaf_function_ = true;
415};
416
417template <class Next>
419 : public UniformReducerAdapter<DeadCodeEliminationReducer, Next> {
420 public:
422
424
425 // DeadCodeElimination can change the control flow in somewhat unexpected ways
426 // (ie, a block with a single predecessor in the input graph can end up with
427 // multiple predecessors in the output graph), so we prevent the CopyingPhase
428 // from automatically inlining blocks with a single predecessor when we run
429 // the DeadCodeEliminationReducer.
430 bool CanAutoInlineBlocksWithSinglePredecessor() const { return false; }
431
432 void Analyze() {
433 // TODO(nicohartmann@): We might want to make this a flag.
434 constexpr bool trace_analysis = false;
436 analyzer_.Run<trace_analysis>();
437 Next::Analyze();
438 }
439
441 if (TryRewriteBranch(ig_index)) return V<None>::Invalid();
442 return Next::ReduceInputGraphBranch(ig_index, branch);
443 }
444
446 if (TryRewriteBranch(ig_index)) return {};
447 return Next::ReduceInputGraphGoto(ig_index, gto);
448 }
449
450 template <typename Op, typename Continuation>
451 OpIndex ReduceInputGraphOperation(OpIndex ig_index, const Op& op) {
452 if ((*liveness_)[ig_index] == OperationState::kDead) {
453 return OpIndex::Invalid();
454 }
455 return Continuation{this}.ReduceInputGraph(ig_index, op);
456 }
457
458 bool IsLeafFunction() const { return analyzer_.is_leaf_function(); }
459
460 private:
462 const BlockIndex* goto_target;
463 if (branch_rewrite_targets_.contains(index, &goto_target)) {
464 Asm().Goto(Asm().MapToNewGraph(&Asm().input_graph().Get(*goto_target)));
465 return true;
466 }
467 return false;
468 }
469 std::optional<FixedOpIndexSidetable<OperationState::Liveness>> liveness_;
471 Asm().phase_zone(), &Asm().input_graph()};
472 DeadCodeAnalysis analyzer_{Asm().modifiable_input_graph(),
473 Asm().phase_zone()};
474};
475
477
478} // namespace v8::internal::compiler::turboshaft
479
480#endif // V8_COMPILER_TURBOSHAFT_DEAD_CODE_ELIMINATION_REDUCER_H_
#define REDUCE_INPUT_GRAPH(operation)
static constexpr BlockIndex Invalid()
Definition index.h:872
std::pair< FixedOpIndexSidetable< OperationState::Liveness >, SparseOpIndexSideTable< BlockIndex > > Run()
FixedOpIndexSidetable< OperationState::Liveness > liveness_
void ProcessBlock(const Block &block, uint32_t *unprocessed_count)
V< None > REDUCE_INPUT_GRAPH Goto(V< None > ig_index, const GotoOp &gto)
V< None > REDUCE_INPUT_GRAPH Branch(V< None > ig_index, const BranchOp &branch)
std::optional< FixedOpIndexSidetable< OperationState::Liveness > > liveness_
base::iterator_range< OpIndexIterator > OperationIndices(const Block &block) const
Definition graph.h:957
base::iterator_range< base::DerefPtrIterator< Block > > blocks()
Definition graph.h:984
V8_INLINE const Operation & Get(OpIndex i) const
Definition graph.h:618
static constexpr OpIndex Invalid()
Definition index.h:88
#define TURBOSHAFT_REDUCER_BOILERPLATE(Name)
Definition assembler.h:823
TNode< Object > target
int r
Definition mul-fft.cc:298
std::ostream & operator<<(std::ostream &os, PaddingSpace padding)
bool operator==(const ControlState &lhs, const ControlState &rhs)
V8_INLINE const Operation & Get(const Graph &graph, OpIndex index)
Definition graph.h:1231
base::SmallVector< Block *, 4 > SuccessorBlocks(const Block &block, const Graph &graph)
bool operator!=(const ControlState &lhs, const ControlState &rhs)
#define DCHECK_LE(v1, v2)
Definition logging.h:490
#define DCHECK_NOT_NULL(val)
Definition logging.h:492
#define DCHECK(condition)
Definition logging.h:482
#define DCHECK_EQ(v1, v2)
Definition logging.h:485
ControlState(Kind kind, BlockIndex block=BlockIndex::Invalid())
static ControlState LeastUpperBound(const ControlState &lhs, const ControlState &rhs)
static Liveness LeastUpperBound(Liveness lhs, Liveness rhs)
base::Vector< const OpIndex > inputs() const
underlying_operation_t< Op > & Cast()
Definition operations.h:980
static constexpr size_t kLoopPhiBackEdgeIndex