v8
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++.
Loading...
Searching...
No Matches
value-numbering-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_VALUE_NUMBERING_REDUCER_H_
6#define V8_COMPILER_TURBOSHAFT_VALUE_NUMBERING_REDUCER_H_
7
8#include "src/base/logging.h"
9#include "src/base/vector.h"
15#include "src/utils/utils.h"
17
18namespace v8 {
19namespace internal {
20namespace compiler {
21namespace turboshaft {
22
23// Value numbering removes redundant nodes from the graph. A simple example
24// could be:
25//
26// x = a + b
27// y = a + b
28// z = x * y
29//
30// Is simplified to
31//
32// x = a + b
33// z = x * x
34//
35// It works by storing previously seen nodes in a hashmap, and when visiting a
36// new node, we check to see if it's already in the hashmap. If yes, then we
37// return the old node. If not, then we keep the new one (and add it into the
38// hashmap). A high-level pseudo-code would be:
39//
40// def VisitOp(op):
41// if op in hashmap:
42// return hashmap.get(op)
43// else:
44// hashmap.add(op)
45// return op
46//
47// We implemented our own hashmap (to have more control, it should become
48// clearer why by the end of this explanation). When there is a collision, we
49// look at the next index (and the next one if there is yet another collision,
50// etc). While not the fastest approach, it has the advantage of not requiring
51// any dynamic memory allocation (besides the initial table, and the resizing).
52//
53// For the approach describe above (the pseudocode and the paragraph before it)
54// to be correct, a node should only be replaced by a node defined in blocks
55// that dominate the current block. Thus, this reducer relies on the fact that
56// OptimizationPhases that iterate the graph dominator order. Then, when going
57// down the dominator tree, we add nodes to the hashmap, and when going back up
58// the dominator tree, we remove nodes from the hashmap.
59//
60// In order to efficiently remove all the nodes of a given block from the
61// hashmap, we maintain a linked-list of hashmap entries per block (this way, we
62// don't have to iterate the wole hashmap). Note that, in practice, we think in
63// terms of "depth" rather than "block", and we thus have one linked-list per
64// depth of the dominator tree. The heads of those linked lists are stored in
65// the vector {depths_heads_}. The linked lists are then implemented in-place in
66// the hashtable entries, thanks to the `depth_neighboring_entry` field of the
67// `Entry` structure.
68// To remove all of the entries from a given linked list, we iterate the entries
69// in the linked list, setting all of their `hash` field to 0 (we prevent hashes
70// from being equal to 0, in order to detect empty entries: their hash is 0).
71
72template <class Next>
74
76 public:
77 void enter() { scopes_++; }
78 void leave() { scopes_--; }
79 bool is_active() { return scopes_ > 0; }
80
81 private:
82 int scopes_{0};
83};
84
85// In rare cases of intentional duplication of instructions, we need to disable
86// value numbering. This scope manages that.
88 public:
89 template <class Reducer>
90 explicit DisableValueNumbering(Reducer* reducer) {
91 if constexpr (reducer_list_contains<typename Reducer::ReducerList,
93 scopes_ = reducer->gvn_disabled_scope();
94 scopes_->enter();
95 }
96 }
97
99 if (scopes_ != nullptr) scopes_->leave();
100 }
101
102 private:
104};
105
106template <class Next>
107class ValueNumberingReducer : public Next {
108#if defined(__clang__)
111#endif
112
113 public:
114 TURBOSHAFT_REDUCER_BOILERPLATE(ValueNumbering)
115
116 template <typename Op>
117 static constexpr bool CanBeGVNed() {
118 constexpr Opcode opcode = operation_to_opcode_v<Op>;
119 /* Throwing operations have a non-trivial lowering, so they don't work
120 * with value numbering. */
121 if constexpr (MayThrow(opcode)) return false;
122 if constexpr (opcode == Opcode::kCatchBlockBegin) {
123 /* CatchBlockBegin are never interesting to GVN, but additionally
124 * split-edge can transform CatchBlockBeginOp into PhiOp, which means
125 * that there is no guarantee here than {result} is indeed a
126 * CatchBlockBegin. */
127 return false;
128 }
129 if constexpr (opcode == Opcode::kComment) {
130 /* We don't want to GVN comments. */
131 return false;
132 }
133 return true;
134 }
135
136#define EMIT_OP(Name) \
137 template <class... Args> \
138 OpIndex Reduce##Name(Args... args) { \
139 OpIndex next_index = Asm().output_graph().next_operation_index(); \
140 USE(next_index); \
141 OpIndex result = Next::Reduce##Name(args...); \
142 if (ShouldSkipOptimizationStep()) return result; \
143 if constexpr (!CanBeGVNed<Name##Op>()) return result; \
144 DCHECK_EQ(next_index, result); \
145 return AddOrFind<Name##Op>(result); \
146 }
148#undef EMIT_OP
149
150 void Bind(Block* block) {
151 Next::Bind(block);
152 ResetToBlock(block);
153 dominator_path_.push_back(block);
154 depths_heads_.push_back(nullptr);
155 }
156
157 // Resets {table_} up to the first dominator of {block} that it contains.
158 void ResetToBlock(Block* block) {
159 Block* target = block->GetDominator();
160 while (!dominator_path_.empty() && target != nullptr &&
161 dominator_path_.back() != target) {
162 if (dominator_path_.back()->Depth() > target->Depth()) {
164 } else if (dominator_path_.back()->Depth() < target->Depth()) {
165 target = target->GetDominator();
166 } else {
167 // {target} and {dominator_path.back} have the same depth but are not
168 // equal, so we go one level up for both.
170 target = target->GetDominator();
171 }
172 }
173 }
174
175 template <class Op>
176 bool WillGVNOp(const Op& op) {
177 Entry* entry = Find(op);
178 return !entry->IsEmpty();
179 }
180
182
183 private:
184 // TODO(dmercadier): Once the mapping from Operations to Blocks has been added
185 // to turboshaft, remove the `block` field from the `Entry` structure.
186 struct Entry {
189 size_t hash = 0;
191
192 bool IsEmpty() const { return hash == 0; }
193 };
194
195 template <class Op>
197 if (is_disabled()) return op_idx;
198
199 const Op& op = Asm().output_graph().Get(op_idx).template Cast<Op>();
200 if (std::is_same_v<Op, PendingLoopPhiOp> || op.IsBlockTerminator() ||
201 (!op.Effects().repetition_is_eliminatable() &&
202 !std::is_same_v<Op, DeoptimizeIfOp>)) {
203 // GVNing DeoptimizeIf is safe, despite its lack of
204 // repetition_is_eliminatable.
205 return op_idx;
206 }
208
209 size_t hash;
210 Entry* entry = Find(op, &hash);
211 if (entry->IsEmpty()) {
212 // {op} is not present in the state, inserting it.
213 *entry = Entry{op_idx, Asm().current_block()->index(), hash,
214 depths_heads_.back()};
215 depths_heads_.back() = entry;
216 ++entry_count_;
217 return op_idx;
218 } else {
219 // {op} is already present, removing it from the graph and returning the
220 // previous one.
221 Next::RemoveLast(op_idx);
222 return entry->value;
223 }
224 }
225
226 template <class Op>
227 Entry* Find(const Op& op, size_t* hash_ret = nullptr) {
228 constexpr bool same_block_only = std::is_same<Op, PhiOp>::value;
229 size_t hash = ComputeHash<same_block_only>(op);
230 size_t start_index = hash & mask_;
231 for (size_t i = start_index;; i = NextEntryIndex(i)) {
232 Entry& entry = table_[i];
233 if (entry.IsEmpty()) {
234 // We didn't find {op} in {table_}. Returning where it could be
235 // inserted.
236 if (hash_ret) *hash_ret = hash;
237 return &entry;
238 }
239 if (entry.hash == hash) {
240 const Operation& entry_op = Asm().output_graph().Get(entry.value);
241 if (entry_op.Is<Op>() &&
242 (!same_block_only ||
243 entry.block == Asm().current_block()->index()) &&
244 entry_op.Cast<Op>().EqualsForGVN(op)) {
245 return &entry;
246 }
247 }
248 // Making sure that we don't have an infinite loop.
249 DCHECK_NE(start_index, NextEntryIndex(i));
250 }
251 }
252
253 // Remove all of the Entries of the current depth.
255 for (Entry* entry = depths_heads_.back(); entry != nullptr;) {
256 entry->hash = 0;
257 Entry* next_entry = entry->depth_neighboring_entry;
258 entry->depth_neighboring_entry = nullptr;
259 entry = next_entry;
260 --entry_count_;
261 }
262 depths_heads_.pop_back();
263 dominator_path_.pop_back();
264 }
265
266 // If the table is too full, double its size and re-insert the old entries.
268 if (V8_LIKELY(table_.size() - (table_.size() / 4) > entry_count_)) return;
269 base::Vector<Entry> new_table = table_ =
270 Asm().phase_zone()->template NewVector<Entry>(table_.size() * 2);
271 size_t mask = mask_ = table_.size() - 1;
272
273 for (size_t depth_idx = 0; depth_idx < depths_heads_.size(); depth_idx++) {
274 // It's important to fill the new hash by inserting data in increasing
275 // depth order, in order to avoid holes when later calling
276 // ClearCurrentDepthEntries. Consider for instance:
277 //
278 // ---+------+------+------+----
279 // | a1 | a2 | a3 |
280 // ---+------+------+------+----
281 //
282 // Where a1, a2 and a3 have the same hash. By construction, we know that
283 // depth(a1) <= depth(a2) <= depth(a3). If, when re-hashing, we were to
284 // insert them in another order, say:
285 //
286 // ---+------+------+------+----
287 // | a3 | a1 | a2 |
288 // ---+------+------+------+----
289 //
290 // Then, when we'll call ClearCurrentDepthEntries to remove entries from
291 // a3's depth, we'll get this:
292 //
293 // ---+------+------+------+----
294 // | null | a1 | a2 |
295 // ---+------+------+------+----
296 //
297 // And, when looking if a1 is in the hash, we'd find a "null" where we
298 // expect it, and assume that it's not present. If, instead, we always
299 // conserve the increasing depth order, then when removing a3, we'd get:
300 //
301 // ---+------+------+------+----
302 // | a1 | a2 | null |
303 // ---+------+------+------+----
304 //
305 // Where we can still find a1 and a2.
306 Entry* entry = depths_heads_[depth_idx];
307 depths_heads_[depth_idx] = nullptr;
308
309 while (entry != nullptr) {
310 for (size_t i = entry->hash & mask;; i = NextEntryIndex(i)) {
311 if (new_table[i].hash == 0) {
312 new_table[i] = *entry;
313 Entry* next_entry = entry->depth_neighboring_entry;
314 new_table[i].depth_neighboring_entry = depths_heads_[depth_idx];
315 depths_heads_[depth_idx] = &new_table[i];
316 entry = next_entry;
317 break;
318 }
319 }
320 }
321 }
322 }
323
324 template <bool same_block_only, class Op>
325 size_t ComputeHash(const Op& op) {
326 size_t hash = op.hash_value();
327 if (same_block_only) {
328 hash = fast_hash_combine(Asm().current_block()->index(), hash);
329 }
330 if (V8_UNLIKELY(hash == 0)) return 1;
331 return hash;
332 }
333
334 size_t NextEntryIndex(size_t index) { return (index + 1) & mask_; }
336 return V8_LIKELY(entry + 1 < table_.end()) ? entry + 1 : &table_[0];
337 }
339 return V8_LIKELY(entry > table_.begin()) ? entry - 1 : table_.end() - 1;
340 }
341
343
345 base::Vector<Entry> table_ = Asm().phase_zone()->template NewVector<Entry>(
347 std::max<size_t>(128, Asm().input_graph().op_id_capacity() / 2)));
348 size_t mask_ = table_.size() - 1;
349 size_t entry_count_ = 0;
352};
353
354} // namespace turboshaft
355} // namespace compiler
356} // namespace internal
357} // namespace v8
358
359#endif // V8_COMPILER_TURBOSHAFT_VALUE_NUMBERING_REDUCER_H_
Entry * Find(const Op &op, size_t *hash_ret=nullptr)
#define TURBOSHAFT_REDUCER_BOILERPLATE(Name)
Definition assembler.h:823
#define EMIT_OP(Name)
Definition assembler.h:1068
uint32_t const mask
constexpr size_t RoundUpToPowerOfTwo(size_t value)
Definition bits.h:252
constexpr Opcode operation_to_opcode_v
Definition operations.h:397
constexpr bool MayThrow(Opcode opcode)
Definition operations.h:432
V8_INLINE size_t fast_hash_combine()
Definition fast-hash.h:19
return value
Definition map-inl.h:893
Tagged< To > Cast(Tagged< From > value, const v8::SourceLocation &loc=INIT_SOURCE_LOCATION_IN_DEBUG)
Definition casting.h:150
#define TURBOSHAFT_OPERATION_LIST(V)
Definition operations.h:362
#define DCHECK_NE(v1, v2)
Definition logging.h:486
underlying_operation_t< Op > & Cast()
Definition operations.h:980
#define V8_LIKELY(condition)
Definition v8config.h:661
#define V8_UNLIKELY(condition)
Definition v8config.h:660