v8
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++.
Loading...
Searching...
No Matches
late-load-elimination-reducer.cc
Go to the documentation of this file.
1// Copyright 2023 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
6
15
17
18#ifdef DEBUG
19#define TRACE(x) \
20 do { \
21 if (v8_flags.turboshaft_trace_load_elimination) \
22 StdoutStream() << x << std::endl; \
23 } while (false)
24#else
25#define TRACE(x)
26#endif
27
28std::ostream& operator<<(std::ostream& os, const MemoryAddress& mem) {
29 return os << "MemoryAddress{base=" << mem.base << ", index=" << mem.index
30 << ", offset=" << mem.offset << ", elem_size_log2="
31 << static_cast<uint32_t>(mem.element_size_log2)
32 << ", size=" << static_cast<uint32_t>(mem.size) << "}";
33}
34
36 TRACE("LateLoadElimination: Starting analysis");
37 LoopFinder loop_finder(phase_zone_, &graph_);
38 AnalyzerIterator iterator(phase_zone_, graph_, loop_finder);
39
40 bool compute_start_snapshot = true;
41 while (iterator.HasNext()) {
42 const Block* block = iterator.Next();
43
44 ProcessBlock(*block, compute_start_snapshot);
45 compute_start_snapshot = true;
46
47 // Consider re-processing for loops.
48 if (const GotoOp* last = block->LastOperation(graph_).TryCast<GotoOp>()) {
49 if (last->destination->IsLoop() &&
50 last->destination->LastPredecessor() == block) {
51 TRACE("> Considering reprocessing loop header " << last->destination);
52 const Block* loop_header = last->destination;
53 // {block} is the backedge of a loop. We recompute the loop header's
54 // initial snapshots, and if they differ from its original snapshot,
55 // then we revisit the loop.
56 if (BeginBlock<true>(loop_header)) {
57 TRACE(">> Will need to revisit loop");
58 // We set the snapshot of the loop's 1st predecessor to the newly
59 // computed snapshot. It's not quite correct, but this predecessor
60 // is guaranteed to end with a Goto, and we are now visiting the
61 // loop, which means that we don't really care about this
62 // predecessor anymore.
63 // The reason for saving this snapshot is to prevent infinite
64 // looping, since the next time we reach this point, the backedge
65 // snapshot could still invalidate things from the forward edge
66 // snapshot. By restricting the forward edge snapshot, we prevent
67 // this.
68 const Block* loop_1st_pred =
70 FinishBlock(loop_1st_pred);
71 // And we start a new fresh snapshot from this predecessor.
72 auto pred_snapshots =
73 block_to_snapshot_mapping_[loop_1st_pred->index()];
75 pred_snapshots->alias_snapshot);
76 object_maps_.StartNewSnapshot(pred_snapshots->maps_snapshot);
77 memory_.StartNewSnapshot(pred_snapshots->memory_snapshot);
78
79 iterator.MarkLoopForRevisit();
80 compute_start_snapshot = false;
81 } else {
82 TRACE(">> No need to revisit loop");
84 }
85 }
86 }
87 }
88
91 // Incorpoare load elimination decisions into int32-truncation data.
92 for (auto it = int32_truncated_loads_.begin();
93 it != int32_truncated_loads_.end();) {
94 OpIndex load_idx = it->first;
95 auto& truncations = it->second;
96 Replacement replacement = GetReplacement(load_idx);
97 // We distinguish a few different cases.
98 if (!replacement.IsLoadElimination()) {
99 // Case 1: This load is not going to be eliminated.
100 total_use_counts[load_idx] += graph_.Get(load_idx).saturated_use_count;
101 // Check if all uses we know so far, are all truncating uses.
102 if (total_use_counts[load_idx].IsSaturated() ||
103 total_use_counts[load_idx].Get() > truncations.size()) {
104 // We do know that we cannot int32-truncate this load, so eliminate
105 // it from the candidates.
106 int32_truncated_loads_.erase(it++);
107 continue;
108 }
109 // Otherwise, keep this candidate.
110 ++it;
111 continue;
112 } else {
113 OpIndex replaced_by_idx = replacement.replacement();
114 const Operation& replaced_by = graph_.Get(replaced_by_idx);
115 if (!replaced_by.Is<LoadOp>()) {
116 // Case 2: This load is replaced by a non-load (e.g. by the value
117 // stored that the load would read). This load cannot be truncated
118 // (because we are not going to have a load anymore), so eliminate it
119 // from the candidates.
120 int32_truncated_loads_.erase(it++);
121 continue;
122 } else {
123 // Case 3: This load is replaced by another load, so the truncating
124 // and the total uses have to be merged into the replacing use.
125 auto it2 = int32_truncated_loads_.find(replaced_by_idx);
126 if (it2 == int32_truncated_loads_.end()) {
127 // Case 3a: The replacing load is not tracked, so we assume it has
128 // non-truncating uses, so we can also ignore this load.
129 int32_truncated_loads_.erase(it++);
130 continue;
131 } else {
132 // Case 3b: The replacing load might have be a candidate for int32
133 // truncation, we merge the information into that load.
134 total_use_counts[replaced_by_idx] +=
135 graph_.Get(load_idx).saturated_use_count;
136 it2->second.insert(truncations.begin(), truncations.end());
137 int32_truncated_loads_.erase(it++);
138 continue;
139 }
140 }
141 }
142 }
143
144 // We have prepared everything and now extract the necessary replacement
145 // information.
146 for (const auto& [load_idx, int32_truncations] : int32_truncated_loads_) {
147 if (int32_truncations.empty()) continue;
148 if (!total_use_counts[load_idx].IsSaturated() &&
149 total_use_counts[load_idx].Get() == int32_truncations.size()) {
150 // All uses of this load are int32-truncating loads, so we replace them.
151 DCHECK(GetReplacement(load_idx).IsNone() ||
152 GetReplacement(load_idx).IsTaggedLoadToInt32Load());
153 for (const auto [change_idx, bitcast_idx] : int32_truncations) {
154 replacements_[change_idx] =
158 }
159 }
160 }
161}
162
164 bool compute_start_snapshot) {
165 TRACE("> ProcessBlock(" << block.index() << ")");
166 if (compute_start_snapshot) {
167 BeginBlock(&block);
168 }
169 if (block.IsLoop() && BackedgeHasSnapshot(block)) {
170 // Update the associated snapshot for the forward edge with the merged
171 // snapshot information from the forward- and backward edge.
172 // This will make sure that when evaluating whether a loop needs to be
173 // revisited, the inner loop compares the merged state with the backedge
174 // preventing us from exponential revisits for loops where the backedge
175 // invalidates loads which are eliminatable on the forward edge.
177 }
178
179 for (OpIndex op_idx : graph_.OperationIndices(block)) {
180 Operation& op = graph_.Get(op_idx);
181 if (ShouldSkipOptimizationStep()) continue;
182 if (ShouldSkipOperation(op)) continue;
183 switch (op.opcode) {
184 case Opcode::kLoad:
185 // Eliminate load or update state
186 ProcessLoad(op_idx, op.Cast<LoadOp>());
187 break;
188 case Opcode::kStore:
189 // Update state (+ maybe invalidate aliases)
190 ProcessStore(op_idx, op.Cast<StoreOp>());
191 break;
192 case Opcode::kAllocate:
193 // Create new non-alias
194 ProcessAllocate(op_idx, op.Cast<AllocateOp>());
195 break;
196 case Opcode::kCall:
197 // Invalidate state (+ maybe invalidate aliases)
198 ProcessCall(op_idx, op.Cast<CallOp>());
199 break;
200 case Opcode::kAssumeMap:
201 // Update known maps
202 ProcessAssumeMap(op_idx, op.Cast<AssumeMapOp>());
203 break;
204 case Opcode::kChange:
205 // Check for tagged -> word32 load replacement
206 ProcessChange(op_idx, op.Cast<ChangeOp>());
207 break;
208
209 case Opcode::kWordBinop:
210 // A WordBinop should never invalidate aliases (since the only time when
211 // it should take a non-aliasing object as input is for Smi checks).
212 DcheckWordBinop(op_idx, op.Cast<WordBinopOp>());
213 break;
214
215 case Opcode::kFrameState:
216 case Opcode::kDeoptimizeIf:
217 case Opcode::kComparison:
218#ifdef V8_ENABLE_WEBASSEMBLY
219 case Opcode::kTrapIf:
220#endif
221 // We explicitly break for these opcodes so that we don't call
222 // InvalidateAllNonAliasingInputs on their inputs, since they don't
223 // really create aliases. (and also, they don't write so it's
224 // fine to break)
225 DCHECK(!op.Effects().can_write());
226 break;
227
228 case Opcode::kDeoptimize:
229 case Opcode::kReturn:
230 // We explicitly break for these opcodes so that we don't call
231 // InvalidateAllNonAliasingInputs on their inputs, since they are block
232 // terminators without successors, meaning that it's not useful for the
233 // rest of the analysis to invalidate anything here.
234 DCHECK(op.IsBlockTerminator() && SuccessorBlocks(op).empty());
235 break;
236
237 case Opcode::kCatchBlockBegin:
238 case Opcode::kRetain:
239 case Opcode::kDidntThrow:
240 case Opcode::kCheckException:
241 case Opcode::kAtomicRMW:
242 case Opcode::kAtomicWord32Pair:
243 case Opcode::kMemoryBarrier:
244 case Opcode::kParameter:
245 case Opcode::kDebugBreak:
246 case Opcode::kJSStackCheck:
247#ifdef V8_ENABLE_WEBASSEMBLY
248 case Opcode::kWasmStackCheck:
249 case Opcode::kSimd128LaneMemory:
250 case Opcode::kGlobalSet:
251 case Opcode::kArraySet:
252 case Opcode::kStructSet:
253 case Opcode::kSetStackPointer:
254#endif // V8_ENABLE_WEBASSEMBLY
255 // We explicitly break for those operations that have can_write effects
256 // but don't actually write, or cannot interfere with load elimination.
257 break;
258 default:
259 // Operations that `can_write` should invalidate the state. All such
260 // operations should be already handled above, which means that we don't
261 // need a `if (can_write) { Invalidate(); }` here.
262 CHECK(!op.Effects().can_write());
263
264 // Even if the operation doesn't write, it could create an alias to its
265 // input by returning it. This happens for instance in Phis and in
266 // Change (although ChangeOp is already handled earlier by calling
267 // ProcessChange). We are conservative here by calling
268 // InvalidateAllNonAliasingInputs for all operations even though only
269 // few can actually create aliases to fresh allocations, the reason
270 // being that missing such a case would be a security issue, and it
271 // should be rare for fresh allocations to be used outside of
272 // Call/Store/Load/Change anyways.
273 TRACE("> Process other op (id=" << op_idx << ")");
275
276 break;
277 }
278 }
279
280 FinishBlock(&block);
281}
282
283namespace {
284
285// Returns true if replacing a Load with a RegisterRepresentation
286// {expected_reg_rep} and MemoryRepresentation of {expected_loaded_repr} with an
287// operation with RegisterRepresentation {actual} is valid. For instance,
288// replacing an operation that returns a Float64 by one that returns a Word64 is
289// not valid. Similarly, replacing a Tagged with an untagged value is probably
290// not valid because of the GC.
292 RegisterRepresentation expected_reg_repr,
293 MemoryRepresentation expected_loaded_repr) {
294 if (expected_loaded_repr.SizeInBytes() !=
296 .SizeInBytes()) {
297 // The replacement was truncated when being stored or should be truncated
298 // (or sign-extended) during the load. Since we don't have enough
299 // truncations operators in Turboshaft (eg, we don't have Int32 to Int8
300 // truncation), we just prevent load elimination in this case.
301
302 // TODO(dmercadier): add more truncations operators to Turboshaft, and
303 // insert the correct truncation when there is a mismatch between
304 // {expected_loaded_repr} and {actual}.
305
306 return false;
307 }
308
309 return expected_reg_repr == actual;
310}
311
312} // namespace
313
315 const LoadOp& load) {
316 TRACE("> ProcessLoad(" << op_idx << ")");
317
318 if (!load.kind.load_eliminable) {
319 // We don't optimize Loads/Stores to addresses that could be accessed
320 // non-canonically.
321 TRACE(">> Not load-eliminable; skipping");
322 return;
323 }
324 if (load.kind.is_atomic) {
325 // Atomic loads cannot be eliminated away, but potential concurrency
326 // invalidates known stored values.
327 TRACE(">> Atomic load, invalidating related memory");
328 memory_.Invalidate(load.base(), load.index(), load.offset);
329 return;
330 }
331
332 // We need to insert the load into the truncation mapping as a key, because
333 // all loads need to be revisited during processing.
335
336 if (OpIndex existing = memory_.Find(load); existing.valid()) {
337 const Operation& replacement = graph_.Get(existing);
338 // We need to make sure that {load} and {replacement} have the same output
339 // representation. In particular, in unreachable code, it's possible that
340 // the two of them have incompatible representations (like one could be
341 // Tagged and the other one Float64).
342 DCHECK_EQ(replacement.outputs_rep().size(), 1);
343 DCHECK_EQ(load.outputs_rep().size(), 1);
344 TRACE(">> Found potential replacement at offset " << existing);
345 if (RepIsCompatible(replacement.outputs_rep()[0], load.outputs_rep()[0],
346 load.loaded_rep)) {
347 TRACE(">>> Confirming replacement");
349 return;
350 } else {
351 TRACE(">>> Replacement has wrong representation: "
352 << replacement.outputs_rep()[0] << " instead of "
353 << load.outputs_rep()[0]);
354 }
355 }
356 // Reset the replacement of {op_idx} to Invalid, in case a previous visit of a
357 // loop has set it to something else.
359
360 // TODO(dmercadier): if we precisely track maps, then we could know from the
361 // map what we are loading in some cases. For instance, if the elements_kind
362 // of the map is *_DOUBLE_ELEMENTS, then a load at offset
363 // JSObject::kElementsOffset always load a FixedDoubleArray, with map
364 // fixed_double_array_map.
365
366 if (const ConstantOp* base = graph_.Get(load.base()).TryCast<ConstantOp>();
367 base != nullptr && base->kind == ConstantOp::Kind::kExternal) {
368 // External constants can be written by other threads, so we don't
369 // load-eliminate them, in order to always reload them.
370 TRACE(">> Ignoring load from External constant");
371 return;
372 }
373
374 memory_.Insert(load, op_idx);
375}
376
378 const StoreOp& store) {
379 TRACE("> ProcessStore(" << op_idx << ")");
380 // If we have a raw base and we allow those to be inner pointers, we can
381 // overwrite arbitrary values and need to invalidate anything that is
382 // potentially aliasing.
383 const bool invalidate_maybe_aliasing =
384 !store.kind.tagged_base &&
386
387 if (invalidate_maybe_aliasing) {
388 TRACE(
389 ">> Raw base or maybe inner pointer ==> Invalidating whole "
390 "maybe-aliasing memory");
392 }
393
394 if (!store.kind.load_eliminable) {
395 // We don't optimize Loads/Stores to addresses that could be accessed
396 // non-canonically.
397 TRACE(">> Not load-eliminable; skipping");
398 return;
399 }
400
401 // Updating the known stored values.
402 if (!invalidate_maybe_aliasing) memory_.Invalidate(store);
403 memory_.Insert(store);
404
405 // Updating aliases if the value stored was known as non-aliasing.
406 OpIndex value = store.value();
407 if (non_aliasing_objects_.HasKeyFor(value)) {
408 TRACE(">> Invalidate non-alias for value=" << value);
409 non_aliasing_objects_.Set(value, false);
410 }
411
412 // If we just stored a map, invalidate the maps for this base.
413 if (store.offset == HeapObject::kMapOffset && !store.index().valid()) {
414 if (object_maps_.HasKeyFor(store.base())) {
415 TRACE(">> Wiping map\n");
416 object_maps_.Set(store.base(), MapMaskAndOr{});
417 }
418 }
419}
420
421// Since we only loosely keep track of what can or can't alias, we assume that
422// anything that was guaranteed to not alias with anything (because it's in
423// {non_aliasing_objects_}) can alias with anything when coming back from the
424// call if it was an argument of the call.
426 const CallOp& op) {
427 TRACE("> ProcessCall(" << op_idx << ")");
428 const Operation& callee = graph_.Get(op.callee());
429#ifdef DEBUG
430 if (const ConstantOp* external_constant =
431 callee.template TryCast<Opmask::kExternalConstant>()) {
432 if (external_constant->external_reference() ==
433 ExternalReference::check_object_type()) {
434 return;
435 }
436 }
437#endif
438
439 // Some builtins do not create aliases and do not invalidate existing
440 // memory, and some even return fresh objects. For such cases, we don't
441 // invalidate the state, and record the non-alias if any.
442 if (!op.Effects().can_write()) {
443 TRACE(">> Call doesn't write, skipping");
444 return;
445 }
446 // Note: This does not detect wasm stack checks, but those are detected by the
447 // check just above.
449 // This is a stack check that cannot write heap memory.
450 TRACE(">> Call is loop stack check, skipping");
451 return;
452 }
453
454 auto builtin_id = TryGetBuiltinId(callee.TryCast<ConstantOp>(), broker_);
455
456 if (builtin_id) {
457 switch (*builtin_id) {
458 // TODO(dmercadier): extend this list.
459 case Builtin::kCopyFastSmiOrObjectElements:
460 // This function just replaces the Elements array of an object.
461 // It doesn't invalidate any alias or any other memory than this
462 // Elements array.
463 TRACE(
464 ">> Call is CopyFastSmiOrObjectElements, invalidating only "
465 "Elements for "
466 << op.arguments()[0]);
468 JSObject::kElementsOffset);
469 return;
470 default:
471 break;
472 }
473 }
474
475 // Not a builtin call, or not a builtin that we know doesn't invalidate
476 // memory.
478
479 if (builtin_id) {
480 switch (*builtin_id) {
481 case Builtin::kCreateShallowObjectLiteral:
482 // This builtin creates a fresh non-aliasing object.
483 non_aliasing_objects_.Set(op_idx, true);
484 break;
485 default:
486 break;
487 }
488 }
489
490 // The call could modify arbitrary memory, so we invalidate every
491 // potentially-aliasing object.
493}
494
495// The only time an Allocate should flow into a WordBinop is for Smi checks
496// (which, by the way, should be removed by MachineOptimizationReducer (since
497// Allocate never returns a Smi), but there is no guarantee that this happens
498// before load elimination). So, there is no need to invalidate non-aliases, and
499// we just DCHECK in this function that indeed, nothing else than a Smi check
500// happens on non-aliasing objects.
502 const WordBinopOp& binop) {
503#ifdef DEBUG
504 auto check = [&](V<Word> left, V<Word> right) {
505 if (auto key = non_aliasing_objects_.TryGetKeyFor(left);
506 key.has_value() && non_aliasing_objects_.Get(*key)) {
507 int64_t cst;
509 DCHECK(OperationMatcher(graph_).MatchSignedIntegralConstant(right, &cst));
511 }
512 };
513 check(binop.left(), binop.right());
514 check(binop.right(), binop.left());
515#endif
516}
517
519 const Operation& op) {
520 TRACE(">> InvalidateAllNonAliasingInputs for " << op);
521 for (OpIndex input : op.inputs()) {
522 InvalidateIfAlias(input);
523 }
524}
525
527 if (auto key = non_aliasing_objects_.TryGetKeyFor(op_idx);
528 key.has_value() && non_aliasing_objects_.Get(*key)) {
529 // An known non-aliasing object was passed as input to the Call; the Call
530 // could create aliases, so we have to consider going forward that this
531 // object could actually have aliases.
532 TRACE(">> InvalidateIfAlias: invalidating for id=" << op_idx);
534 }
535 if (const FrameStateOp* frame_state =
536 graph_.Get(op_idx).TryCast<FrameStateOp>()) {
537 TRACE(
538 ">> InvalidateIfAlias: recursively invalidating for FrameState inputs "
539 "(FrameState id="
540 << op_idx << ")");
541 // We also mark the arguments of FrameState passed on to calls as
542 // potentially-aliasing, because they could be accessed by the caller with a
543 // function_name.arguments[index].
544 // TODO(dmercadier): this is more conservative that we'd like, since only a
545 // few functions use .arguments. Using a native-context-specific protector
546 // for .arguments might allow to avoid invalidating frame states' content.
547 for (OpIndex input : frame_state->inputs()) {
548 InvalidateIfAlias(input);
549 }
550 }
551}
552
554 const AllocateOp&) {
555 TRACE("> ProcessAllocate(" << op_idx << ") ==> Fresh non-aliasing object");
556 non_aliasing_objects_.Set(op_idx, true);
557}
558
560 OpIndex op_idx, const AssumeMapOp& assume_map) {
561 TRACE("> ProcessAssumeMap(" << op_idx << ")");
562 OpIndex object = assume_map.heap_object();
564 ComputeMinMaxHash(assume_map.maps)));
565}
566
567bool IsInt32TruncatedLoadPattern(const Graph& graph, OpIndex change_idx,
568 const ChangeOp& change, OpIndex* bitcast_idx,
569 OpIndex* load_idx) {
570 DCHECK_EQ(change_idx, graph.Index(change));
571
572 if (!change.Is<Opmask::kTruncateWord64ToWord32>()) return false;
573 const TaggedBitcastOp* bitcast =
574 graph.Get(change.input())
576 if (bitcast == nullptr) return false;
577 // We require that the bitcast has no other uses. This could be slightly
578 // generalized by allowing multiple int32-truncating uses, but that is more
579 // expensive to detect and it is very unlikely that we ever see such a case
580 // (e.g. because of GVN).
581 if (!bitcast->saturated_use_count.IsOne()) return false;
582 const LoadOp* load = graph.Get(bitcast->input()).TryCast<LoadOp>();
583 if (load == nullptr) return false;
584 if (load->loaded_rep.SizeInBytesLog2() !=
586 return false;
587 }
588 if (bitcast_idx) *bitcast_idx = change.input();
589 if (load_idx) *load_idx = bitcast->input();
590 return true;
591}
592
594 const ChangeOp& change) {
595 TRACE("> ProcessChange(" << op_idx << ")");
596 // We look for this special case:
597 // TruncateWord64ToWord32(BitcastTaggedToWordPtrForTagAndSmiBits(Load(x))) =>
598 // Load(x)
599 // where the new Load uses Int32 rather than the tagged representation.
600 OpIndex bitcast_idx, load_idx;
601 if (IsInt32TruncatedLoadPattern(graph_, op_idx, change, &bitcast_idx,
602 &load_idx)) {
603 int32_truncated_loads_[load_idx][op_idx] = bitcast_idx;
604 }
605
606 InvalidateIfAlias(change.input());
607}
608
613
619
621 const Block& loop_header) {
622 auto non_aliasing_snapshot = non_aliasing_objects_.Seal();
623 auto object_maps_snapshot = object_maps_.Seal();
624 auto memory_snapshot = memory_.Seal();
625
627 [loop_header.LastPredecessor()->NeighboringPredecessor()->index()] =
628 Snapshot{non_aliasing_snapshot, object_maps_snapshot,
629 memory_snapshot};
630
631 non_aliasing_objects_.StartNewSnapshot(non_aliasing_snapshot);
632 object_maps_.StartNewSnapshot(object_maps_snapshot);
633 memory_.StartNewSnapshot(memory_snapshot);
634}
635
637 const Block& loop_header) const {
638 DCHECK(loop_header.IsLoop());
639 return block_to_snapshot_mapping_[loop_header.LastPredecessor()->index()]
640 .has_value();
641}
642
643template <bool for_loop_revisit>
646 for_loop_revisit,
647 block->IsLoop() &&
648 block_to_snapshot_mapping_[block->LastPredecessor()->index()]
649 .has_value());
650
651 // Collect the snapshots of all predecessors.
652 {
656 for (const Block* p : block->PredecessorsIterable()) {
657 auto pred_snapshots = block_to_snapshot_mapping_[p->index()];
658 // When we visit the loop for the first time, the loop header hasn't
659 // been visited yet, so we ignore it.
660 DCHECK_IMPLIES(!pred_snapshots.has_value(),
661 block->IsLoop() && block->LastPredecessor() == p);
662 if (!pred_snapshots.has_value()) {
663 DCHECK(!for_loop_revisit);
664 continue;
665 }
666 // Note that the backedge snapshot of an inner loop in kFirstVisit will
667 // also be taken into account if we are in the kSecondVisit of an outer
668 // loop. The data in the backedge snapshot could be out-dated, but if it
669 // is, then it's fine: if the backedge of the outer-loop was more
670 // restrictive than its forward incoming edge, then the forward incoming
671 // edge of the inner loop should reflect this restriction.
672 predecessor_alias_snapshots_.push_back(pred_snapshots->alias_snapshot);
673 predecessor_memory_snapshots_.push_back(pred_snapshots->memory_snapshot);
674 if (p->NeighboringPredecessor() != nullptr || !block->IsLoop() ||
675 block->LastPredecessor() != p) {
676 // We only add a MapSnapshot predecessor for non-backedge predecessor.
677 // This is because maps coming from inside of the loop may be wrong
678 // until a specific check has been executed.
679 predecessor_maps_snapshots_.push_back(pred_snapshots->maps_snapshot);
680 }
681 }
682 }
683
684 // Note that predecessors are in reverse order, which means that the backedge
685 // is at offset 0.
686 constexpr int kBackedgeOffset = 0;
687 constexpr int kForwardEdgeOffset = 1;
688
689 bool loop_needs_revisit = false;
690 // Start a new snapshot for this block by merging information from
691 // predecessors.
692 auto merge_aliases = [&](AliasKey key,
693 base::Vector<const bool> predecessors) -> bool {
694 if (for_loop_revisit && predecessors[kForwardEdgeOffset] &&
695 !predecessors[kBackedgeOffset]) {
696 // The backedge doesn't think that {key} is no-alias, but the loop
697 // header previously thought it was --> need to revisit.
698 loop_needs_revisit = true;
699 }
700 return base::all_of(predecessors);
701 };
704
705 auto merge_maps =
706 [&](MapKey key,
708 MapMaskAndOr minmax;
709 for (const MapMaskAndOr pred : predecessors) {
710 if (is_empty(pred)) {
711 // One of the predecessors doesn't have maps for this object, so we have
712 // to assume that this object could have any map.
713 return MapMaskAndOr{};
714 }
715 minmax = CombineMinMax(minmax, pred);
716 }
717 return minmax;
718 };
720 merge_maps);
721
722 // Merging for {memory_} means setting values to Invalid unless all
723 // predecessors have the same value.
724 // TODO(dmercadier): we could insert of Phis during the pass to merge existing
725 // information. This is a bit hard, because we are currently in an analyzer
726 // rather than a reducer. Still, we could "prepare" the insertion now and then
727 // really insert them during the Reduce phase of the CopyingPhase.
728 auto merge_memory = [&](MemoryKey key,
729 base::Vector<const OpIndex> predecessors) -> OpIndex {
730 if (for_loop_revisit && predecessors[kForwardEdgeOffset].valid() &&
731 predecessors[kBackedgeOffset] != predecessors[kForwardEdgeOffset]) {
732 // {key} had a value in the loop header, but the backedge and the forward
733 // edge don't agree on its value, which means that the loop invalidated
734 // some memory data, and thus needs to be revisited.
735 loop_needs_revisit = true;
736 }
737 return base::all_equal(predecessors) ? predecessors[0] : OpIndex::Invalid();
738 };
740 merge_memory);
741
742 if (block->IsLoop()) return loop_needs_revisit;
743 return false;
744}
745
746template bool LateLoadEliminationAnalyzer::BeginBlock<true>(const Block* block);
748 const Block* block);
749
750} // namespace v8::internal::compiler::turboshaft
static constexpr int kMapOffset
void push_back(const T &value)
NeighboringPredecessorIterable PredecessorsIterable() const
Definition graph.h:340
void StartNewSnapshot(base::Vector< const Snapshot > predecessors)
base::iterator_range< OpIndexIterator > OperationIndices(const Block &block) const
Definition graph.h:957
V8_INLINE const Operation & Get(OpIndex i) const
Definition graph.h:618
std::map< OpIndex, base::SmallMap< std::map< OpIndex, OpIndex >, 4 > > int32_truncated_loads_
FixedBlockSidetable< std::optional< Snapshot > > block_to_snapshot_mapping_
void ProcessBlock(const Block &block, bool compute_start_snapshot)
static LoadEliminationReplacement LoadElimination(OpIndex replacement)
static LoadEliminationReplacement Int32TruncationElimination(OpIndex replacement)
static MemoryRepresentation FromRegisterRepresentation(RegisterRepresentation repr, bool is_signed)
static constexpr MemoryRepresentation Int32()
static constexpr OpIndex Invalid()
Definition index.h:88
void StartNewSnapshot(base::Vector< const Snapshot > predecessors, const ChangeCallback &change_callback={})
bool is_empty
Definition sweeper.cc:229
#define TRACE(...)
bool all_equal(const C &container)
constexpr Vector< T > VectorOf(T *start, size_t size)
Definition vector.h:360
bool all_of(const C &container, const P &predicate)
MapMaskAndOr ComputeMinMaxHash(ZoneRefSet< Map > maps)
std::ostream & operator<<(std::ostream &os, PaddingSpace padding)
bool RepIsCompatible(RegisterRepresentation actual, RegisterRepresentation expected_reg_repr, uint8_t in_memory_size)
V8_INLINE const Operation & Get(const Graph &graph, OpIndex index)
Definition graph.h:1231
MapMaskAndOr CombineMinMax(MapMaskAndOr a, MapMaskAndOr b)
V8_EXPORT_PRIVATE V8_INLINE bool ShouldSkipOperation(const Operation &op)
std::optional< Builtin > TryGetBuiltinId(const ConstantOp *target, JSHeapBroker *broker)
Definition operations.cc:45
bool IsInt32TruncatedLoadPattern(const Graph &graph, OpIndex change_idx, const ChangeOp &change, OpIndex *bitcast_idx, OpIndex *load_idx)
V8_EXPORT_PRIVATE bool ShouldSkipOptimizationStep()
Definition utils.h:84
base::SmallVector< Block *, 4 > SuccessorBlocks(const Block &block, const Graph &graph)
bool TryCast(Tagged< From > value, Tagged< To > *out)
Definition casting.h:77
bool IsNone(Tagged< FieldType > obj)
Definition field-type.h:50
const intptr_t kSmiTagMask
Definition v8-internal.h:88
#define CHECK(condition)
Definition logging.h:124
#define DCHECK_IMPLIES(v1, v2)
Definition logging.h:493
#define DCHECK(condition)
Definition logging.h:482
#define DCHECK_EQ(v1, v2)
Definition logging.h:485
base::Vector< const OpIndex > arguments() const
V8_EXPORT_PRIVATE bool IsStackCheck(const Graph &graph, JSHeapBroker *broker, StackCheckKind kind) const
Definition operations.cc:64
base::Vector< const OpIndex > inputs() const
const underlying_operation_t< Op > * TryCast() const
Definition operations.h:990
underlying_operation_t< Op > & Cast()
Definition operations.h:980
base::Vector< const RegisterRepresentation > outputs_rep() const