• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 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 #include <iterator>
6 
7 #include "src/compiler/store-store-elimination.h"
8 
9 #include "src/compiler/all-nodes.h"
10 #include "src/compiler/js-graph.h"
11 #include "src/compiler/node-properties.h"
12 #include "src/compiler/simplified-operator.h"
13 
14 namespace v8 {
15 namespace internal {
16 namespace compiler {
17 
18 #define TRACE(fmt, ...)                                         \
19   do {                                                          \
20     if (FLAG_trace_store_elimination) {                         \
21       PrintF("RedundantStoreFinder: " fmt "\n", ##__VA_ARGS__); \
22     }                                                           \
23   } while (false)
24 
25 // CHECK_EXTRA is like CHECK, but has two or more arguments: a boolean
26 // expression, a format string, and any number of extra arguments. The boolean
27 // expression will be evaluated at runtime. If it evaluates to false, then an
28 // error message will be shown containing the condition, as well as the extra
29 // info formatted like with printf.
30 #define CHECK_EXTRA(condition, fmt, ...)                                 \
31   do {                                                                   \
32     if (V8_UNLIKELY(!(condition))) {                                     \
33       V8_Fatal(__FILE__, __LINE__, "Check failed: %s. Extra info: " fmt, \
34                #condition, ##__VA_ARGS__);                               \
35     }                                                                    \
36   } while (0)
37 
38 #ifdef DEBUG
39 #define DCHECK_EXTRA(condition, fmt, ...) \
40   CHECK_EXTRA(condition, fmt, ##__VA_ARGS__)
41 #else
42 #define DCHECK_EXTRA(condition, fmt, ...) ((void)0)
43 #endif
44 
45 // Store-store elimination.
46 //
47 // The aim of this optimization is to detect the following pattern in the
48 // effect graph:
49 //
50 // - StoreField[+24, kRepTagged](263, ...)
51 //
52 //   ... lots of nodes from which the field at offset 24 of the object
53 //       returned by node #263 cannot be observed ...
54 //
55 // - StoreField[+24, kRepTagged](263, ...)
56 //
57 // In such situations, the earlier StoreField cannot be observed, and can be
58 // eliminated. This optimization should work for any offset and input node, of
59 // course.
60 //
61 // The optimization also works across splits. It currently does not work for
62 // loops, because we tend to put a stack check in loops, and like deopts,
63 // stack checks can observe anything.
64 
65 // Assumption: every byte of a JS object is only ever accessed through one
66 // offset. For instance, byte 15 of a given object may be accessed using a
67 // two-byte read at offset 14, or a four-byte read at offset 12, but never
68 // both in the same program.
69 //
70 // This implementation needs all dead nodes removed from the graph, and the
71 // graph should be trimmed.
72 
73 namespace {
74 
75 typedef uint32_t StoreOffset;
76 
77 struct UnobservableStore {
78   NodeId id_;
79   StoreOffset offset_;
80 
81   bool operator==(const UnobservableStore) const;
82   bool operator!=(const UnobservableStore) const;
83   bool operator<(const UnobservableStore) const;
84 };
85 
86 }  // namespace
87 
88 namespace {
89 
90 // Instances of UnobservablesSet are immutable. They represent either a set of
91 // UnobservableStores, or the "unvisited empty set".
92 //
93 // We apply some sharing to save memory. The class UnobservablesSet is only a
94 // pointer wide, and a copy does not use any heap (or temp_zone) memory. Most
95 // changes to an UnobservablesSet might allocate in the temp_zone.
96 //
97 // The size of an instance should be the size of a pointer, plus additional
98 // space in the zone in the case of non-unvisited UnobservablesSets. Copying
99 // an UnobservablesSet allocates no memory.
100 class UnobservablesSet final {
101  public:
102   static UnobservablesSet Unvisited();
103   static UnobservablesSet VisitedEmpty(Zone* zone);
104   UnobservablesSet();  // unvisited
UnobservablesSet(const UnobservablesSet & other)105   UnobservablesSet(const UnobservablesSet& other) : set_(other.set_) {}
106 
107   UnobservablesSet Intersect(UnobservablesSet other, Zone* zone) const;
108   UnobservablesSet Add(UnobservableStore obs, Zone* zone) const;
109   UnobservablesSet RemoveSameOffset(StoreOffset off, Zone* zone) const;
110 
set() const111   const ZoneSet<UnobservableStore>* set() const { return set_; }
112 
IsUnvisited() const113   bool IsUnvisited() const { return set_ == nullptr; }
IsEmpty() const114   bool IsEmpty() const { return set_ == nullptr || set_->empty(); }
Contains(UnobservableStore obs) const115   bool Contains(UnobservableStore obs) const {
116     return set_ != nullptr && (set_->find(obs) != set_->end());
117   }
118 
119   bool operator==(const UnobservablesSet&) const;
120   bool operator!=(const UnobservablesSet&) const;
121 
122  private:
UnobservablesSet(const ZoneSet<UnobservableStore> * set)123   explicit UnobservablesSet(const ZoneSet<UnobservableStore>* set)
124       : set_(set) {}
125   const ZoneSet<UnobservableStore>* set_;
126 };
127 
128 }  // namespace
129 
130 namespace {
131 
132 class RedundantStoreFinder final {
133  public:
134   RedundantStoreFinder(JSGraph* js_graph, Zone* temp_zone);
135 
136   void Find();
137 
to_remove_const()138   const ZoneSet<Node*>& to_remove_const() { return to_remove_; }
139 
140   void Visit(Node* node);
141 
142  private:
143   static bool IsEffectful(Node* node);
144   void VisitEffectfulNode(Node* node);
145   UnobservablesSet RecomputeUseIntersection(Node* node);
146   UnobservablesSet RecomputeSet(Node* node, UnobservablesSet uses);
147   static bool CannotObserveStoreField(Node* node);
148 
149   void MarkForRevisit(Node* node);
150   bool HasBeenVisited(Node* node);
151 
jsgraph() const152   JSGraph* jsgraph() const { return jsgraph_; }
temp_zone() const153   Zone* temp_zone() const { return temp_zone_; }
unobservable()154   ZoneVector<UnobservablesSet>& unobservable() { return unobservable_; }
unobservable_for_id(NodeId id)155   UnobservablesSet& unobservable_for_id(NodeId id) {
156     DCHECK_LT(id, unobservable().size());
157     return unobservable()[id];
158   }
to_remove()159   ZoneSet<Node*>& to_remove() { return to_remove_; }
160 
161   JSGraph* const jsgraph_;
162   Zone* const temp_zone_;
163 
164   ZoneStack<Node*> revisit_;
165   ZoneVector<bool> in_revisit_;
166   // Maps node IDs to UnobservableNodeSets.
167   ZoneVector<UnobservablesSet> unobservable_;
168   ZoneSet<Node*> to_remove_;
169   const UnobservablesSet unobservables_visited_empty_;
170 };
171 
172 // To safely cast an offset from a FieldAccess, which has a potentially wider
173 // range (namely int).
ToOffset(int offset)174 StoreOffset ToOffset(int offset) {
175   CHECK(0 <= offset);
176   return static_cast<StoreOffset>(offset);
177 }
178 
ToOffset(const FieldAccess & access)179 StoreOffset ToOffset(const FieldAccess& access) {
180   return ToOffset(access.offset);
181 }
182 
RepSizeOf(MachineRepresentation rep)183 unsigned int RepSizeOf(MachineRepresentation rep) {
184   return 1u << ElementSizeLog2Of(rep);
185 }
RepSizeOf(FieldAccess access)186 unsigned int RepSizeOf(FieldAccess access) {
187   return RepSizeOf(access.machine_type.representation());
188 }
189 
AtMostTagged(FieldAccess access)190 bool AtMostTagged(FieldAccess access) {
191   return RepSizeOf(access) <= RepSizeOf(MachineRepresentation::kTagged);
192 }
193 
AtLeastTagged(FieldAccess access)194 bool AtLeastTagged(FieldAccess access) {
195   return RepSizeOf(access) >= RepSizeOf(MachineRepresentation::kTagged);
196 }
197 
198 }  // namespace
199 
Find()200 void RedundantStoreFinder::Find() {
201   Visit(jsgraph()->graph()->end());
202 
203   while (!revisit_.empty()) {
204     Node* next = revisit_.top();
205     revisit_.pop();
206     DCHECK_LT(next->id(), in_revisit_.size());
207     in_revisit_[next->id()] = false;
208     Visit(next);
209   }
210 
211 #ifdef DEBUG
212   // Check that we visited all the StoreFields
213   AllNodes all(temp_zone(), jsgraph()->graph());
214   for (Node* node : all.reachable) {
215     if (node->op()->opcode() == IrOpcode::kStoreField) {
216       DCHECK_EXTRA(HasBeenVisited(node), "#%d:%s", node->id(),
217                    node->op()->mnemonic());
218     }
219   }
220 #endif
221 }
222 
MarkForRevisit(Node * node)223 void RedundantStoreFinder::MarkForRevisit(Node* node) {
224   DCHECK_LT(node->id(), in_revisit_.size());
225   if (!in_revisit_[node->id()]) {
226     revisit_.push(node);
227     in_revisit_[node->id()] = true;
228   }
229 }
230 
HasBeenVisited(Node * node)231 bool RedundantStoreFinder::HasBeenVisited(Node* node) {
232   return !unobservable_for_id(node->id()).IsUnvisited();
233 }
234 
Run(JSGraph * js_graph,Zone * temp_zone)235 void StoreStoreElimination::Run(JSGraph* js_graph, Zone* temp_zone) {
236   // Find superfluous nodes
237   RedundantStoreFinder finder(js_graph, temp_zone);
238   finder.Find();
239 
240   // Remove superfluous nodes
241 
242   for (Node* node : finder.to_remove_const()) {
243     if (FLAG_trace_store_elimination) {
244       PrintF("StoreStoreElimination::Run: Eliminating node #%d:%s\n",
245              node->id(), node->op()->mnemonic());
246     }
247     Node* previous_effect = NodeProperties::GetEffectInput(node);
248     NodeProperties::ReplaceUses(node, nullptr, previous_effect, nullptr,
249                                 nullptr);
250     node->Kill();
251   }
252 }
253 
IsEffectful(Node * node)254 bool RedundantStoreFinder::IsEffectful(Node* node) {
255   return (node->op()->EffectInputCount() >= 1);
256 }
257 
258 // Recompute unobservables-set for a node. Will also mark superfluous nodes
259 // as to be removed.
260 
RecomputeSet(Node * node,UnobservablesSet uses)261 UnobservablesSet RedundantStoreFinder::RecomputeSet(Node* node,
262                                                     UnobservablesSet uses) {
263   switch (node->op()->opcode()) {
264     case IrOpcode::kStoreField: {
265       Node* stored_to = node->InputAt(0);
266       FieldAccess access = OpParameter<FieldAccess>(node->op());
267       StoreOffset offset = ToOffset(access);
268 
269       UnobservableStore observation = {stored_to->id(), offset};
270       bool isNotObservable = uses.Contains(observation);
271 
272       if (isNotObservable && AtMostTagged(access)) {
273         TRACE("  #%d is StoreField[+%d,%s](#%d), unobservable", node->id(),
274               offset, MachineReprToString(access.machine_type.representation()),
275               stored_to->id());
276         to_remove().insert(node);
277         return uses;
278       } else if (isNotObservable && !AtMostTagged(access)) {
279         TRACE(
280             "  #%d is StoreField[+%d,%s](#%d), repeated in future but too "
281             "big to optimize away",
282             node->id(), offset,
283             MachineReprToString(access.machine_type.representation()),
284             stored_to->id());
285         return uses;
286       } else if (!isNotObservable && AtLeastTagged(access)) {
287         TRACE("  #%d is StoreField[+%d,%s](#%d), observable, recording in set",
288               node->id(), offset,
289               MachineReprToString(access.machine_type.representation()),
290               stored_to->id());
291         return uses.Add(observation, temp_zone());
292       } else if (!isNotObservable && !AtLeastTagged(access)) {
293         TRACE(
294             "  #%d is StoreField[+%d,%s](#%d), observable but too small to "
295             "record",
296             node->id(), offset,
297             MachineReprToString(access.machine_type.representation()),
298             stored_to->id());
299         return uses;
300       } else {
301         UNREACHABLE();
302       }
303       break;
304     }
305     case IrOpcode::kLoadField: {
306       Node* loaded_from = node->InputAt(0);
307       FieldAccess access = OpParameter<FieldAccess>(node->op());
308       StoreOffset offset = ToOffset(access);
309 
310       TRACE(
311           "  #%d is LoadField[+%d,%s](#%d), removing all offsets [+%d] from "
312           "set",
313           node->id(), offset,
314           MachineReprToString(access.machine_type.representation()),
315           loaded_from->id(), offset);
316 
317       return uses.RemoveSameOffset(offset, temp_zone());
318       break;
319     }
320     default:
321       if (CannotObserveStoreField(node)) {
322         TRACE("  #%d:%s can observe nothing, set stays unchanged", node->id(),
323               node->op()->mnemonic());
324         return uses;
325       } else {
326         TRACE("  #%d:%s might observe anything, recording empty set",
327               node->id(), node->op()->mnemonic());
328         return unobservables_visited_empty_;
329       }
330   }
331   UNREACHABLE();
332   return UnobservablesSet::Unvisited();
333 }
334 
CannotObserveStoreField(Node * node)335 bool RedundantStoreFinder::CannotObserveStoreField(Node* node) {
336   return node->opcode() == IrOpcode::kCheckedLoad ||
337          node->opcode() == IrOpcode::kLoadElement ||
338          node->opcode() == IrOpcode::kLoad ||
339          node->opcode() == IrOpcode::kStore ||
340          node->opcode() == IrOpcode::kEffectPhi ||
341          node->opcode() == IrOpcode::kStoreElement ||
342          node->opcode() == IrOpcode::kCheckedStore ||
343          node->opcode() == IrOpcode::kUnsafePointerAdd ||
344          node->opcode() == IrOpcode::kRetain;
345 }
346 
347 // Initialize unobservable_ with js_graph->graph->NodeCount() empty sets.
RedundantStoreFinder(JSGraph * js_graph,Zone * temp_zone)348 RedundantStoreFinder::RedundantStoreFinder(JSGraph* js_graph, Zone* temp_zone)
349     : jsgraph_(js_graph),
350       temp_zone_(temp_zone),
351       revisit_(temp_zone),
352       in_revisit_(js_graph->graph()->NodeCount(), temp_zone),
353       unobservable_(js_graph->graph()->NodeCount(),
354                     UnobservablesSet::Unvisited(), temp_zone),
355       to_remove_(temp_zone),
356       unobservables_visited_empty_(UnobservablesSet::VisitedEmpty(temp_zone)) {}
357 
Visit(Node * node)358 void RedundantStoreFinder::Visit(Node* node) {
359   // All effectful nodes should be reachable from End via a sequence of
360   // control, then a sequence of effect edges. In VisitEffectfulNode we mark
361   // all effect inputs for revisiting (if they might have stale state); here
362   // we mark all control inputs at least once.
363 
364   if (!HasBeenVisited(node)) {
365     for (int i = 0; i < node->op()->ControlInputCount(); i++) {
366       Node* control_input = NodeProperties::GetControlInput(node, i);
367       if (!HasBeenVisited(control_input)) {
368         MarkForRevisit(control_input);
369       }
370     }
371   }
372 
373   bool isEffectful = (node->op()->EffectInputCount() >= 1);
374   if (isEffectful) {
375     VisitEffectfulNode(node);
376     DCHECK(HasBeenVisited(node));
377   }
378 
379   if (!HasBeenVisited(node)) {
380     // Mark as visited.
381     unobservable_for_id(node->id()) = unobservables_visited_empty_;
382   }
383 }
384 
VisitEffectfulNode(Node * node)385 void RedundantStoreFinder::VisitEffectfulNode(Node* node) {
386   if (HasBeenVisited(node)) {
387     TRACE("- Revisiting: #%d:%s", node->id(), node->op()->mnemonic());
388   }
389   UnobservablesSet after_set = RecomputeUseIntersection(node);
390   UnobservablesSet before_set = RecomputeSet(node, after_set);
391   DCHECK(!before_set.IsUnvisited());
392 
393   UnobservablesSet stored_for_node = unobservable_for_id(node->id());
394   bool cur_set_changed =
395       (stored_for_node.IsUnvisited() || stored_for_node != before_set);
396   if (!cur_set_changed) {
397     // We will not be able to update the part of this chain above any more.
398     // Exit.
399     TRACE("+ No change: stabilized. Not visiting effect inputs.");
400   } else {
401     unobservable_for_id(node->id()) = before_set;
402 
403     // Mark effect inputs for visiting.
404     for (int i = 0; i < node->op()->EffectInputCount(); i++) {
405       Node* input = NodeProperties::GetEffectInput(node, i);
406       TRACE("    marking #%d:%s for revisit", input->id(),
407             input->op()->mnemonic());
408       MarkForRevisit(input);
409     }
410   }
411 }
412 
413 // Compute the intersection of the UnobservablesSets of all effect uses and
414 // return it. This function only works if {node} has an effect use.
415 //
416 // The result UnobservablesSet will always be visited.
RecomputeUseIntersection(Node * node)417 UnobservablesSet RedundantStoreFinder::RecomputeUseIntersection(Node* node) {
418   // {first} == true indicates that we haven't looked at any elements yet.
419   // {first} == false indicates that cur_set is the intersection of at least one
420   // thing.
421 
422   bool first = true;
423   UnobservablesSet cur_set = UnobservablesSet::Unvisited();  // irrelevant
424 
425   for (Edge edge : node->use_edges()) {
426     // Skip non-effect edges
427     if (!NodeProperties::IsEffectEdge(edge)) {
428       continue;
429     }
430 
431     Node* use = edge.from();
432     UnobservablesSet new_set = unobservable_for_id(use->id());
433     // Include new_set in the intersection.
434     if (first) {
435       // Intersection of a one-element set is that one element
436       first = false;
437       cur_set = new_set;
438     } else {
439       // Take the intersection of cur_set and new_set.
440       cur_set = cur_set.Intersect(new_set, temp_zone());
441     }
442   }
443 
444   if (first) {
445     // There were no effect uses.
446     auto opcode = node->op()->opcode();
447     // List of opcodes that may end this effect chain. The opcodes are not
448     // important to the soundness of this optimization; this serves as a
449     // general sanity check. Add opcodes to this list as it suits you.
450     //
451     // Everything is observable after these opcodes; return the empty set.
452     DCHECK_EXTRA(
453         opcode == IrOpcode::kReturn || opcode == IrOpcode::kTerminate ||
454             opcode == IrOpcode::kDeoptimize || opcode == IrOpcode::kThrow,
455         "for #%d:%s", node->id(), node->op()->mnemonic());
456     USE(opcode);  // silence warning about unused variable in release mode
457 
458     return unobservables_visited_empty_;
459   } else {
460     if (cur_set.IsUnvisited()) {
461       cur_set = unobservables_visited_empty_;
462     }
463 
464     return cur_set;
465   }
466 }
467 
Unvisited()468 UnobservablesSet UnobservablesSet::Unvisited() { return UnobservablesSet(); }
469 
UnobservablesSet()470 UnobservablesSet::UnobservablesSet() : set_(nullptr) {}
471 
VisitedEmpty(Zone * zone)472 UnobservablesSet UnobservablesSet::VisitedEmpty(Zone* zone) {
473   // Create a new empty UnobservablesSet. This allocates in the zone, and
474   // can probably be optimized to use a global singleton.
475   ZoneSet<UnobservableStore>* empty_set =
476       new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
477           ZoneSet<UnobservableStore>(zone);
478   return UnobservablesSet(empty_set);
479 }
480 
481 // Computes the intersection of two UnobservablesSets. May return
482 // UnobservablesSet::Unvisited() instead of an empty UnobservablesSet for
483 // speed.
Intersect(UnobservablesSet other,Zone * zone) const484 UnobservablesSet UnobservablesSet::Intersect(UnobservablesSet other,
485                                              Zone* zone) const {
486   if (IsEmpty() || other.IsEmpty()) {
487     return Unvisited();
488   } else {
489     ZoneSet<UnobservableStore>* intersection =
490         new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
491             ZoneSet<UnobservableStore>(zone);
492     // Put the intersection of set() and other.set() in intersection.
493     set_intersection(set()->begin(), set()->end(), other.set()->begin(),
494                      other.set()->end(),
495                      std::inserter(*intersection, intersection->end()));
496 
497     return UnobservablesSet(intersection);
498   }
499 }
500 
Add(UnobservableStore obs,Zone * zone) const501 UnobservablesSet UnobservablesSet::Add(UnobservableStore obs,
502                                        Zone* zone) const {
503   bool present = (set()->find(obs) != set()->end());
504   if (present) {
505     return *this;
506   } else {
507     // Make a new empty set.
508     ZoneSet<UnobservableStore>* new_set =
509         new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
510             ZoneSet<UnobservableStore>(zone);
511     // Copy the old elements over.
512     *new_set = *set();
513     // Add the new element.
514     bool inserted = new_set->insert(obs).second;
515     DCHECK(inserted);
516     USE(inserted);  // silence warning about unused variable
517 
518     return UnobservablesSet(new_set);
519   }
520 }
521 
RemoveSameOffset(StoreOffset offset,Zone * zone) const522 UnobservablesSet UnobservablesSet::RemoveSameOffset(StoreOffset offset,
523                                                     Zone* zone) const {
524   // Make a new empty set.
525   ZoneSet<UnobservableStore>* new_set =
526       new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
527           ZoneSet<UnobservableStore>(zone);
528   // Copy all elements over that have a different offset.
529   for (auto obs : *set()) {
530     if (obs.offset_ != offset) {
531       new_set->insert(obs);
532     }
533   }
534 
535   return UnobservablesSet(new_set);
536 }
537 
538 // Used for debugging.
operator ==(const UnobservablesSet & other) const539 bool UnobservablesSet::operator==(const UnobservablesSet& other) const {
540   if (IsUnvisited() || other.IsUnvisited()) {
541     return IsEmpty() && other.IsEmpty();
542   } else {
543     // Both pointers guaranteed not to be nullptrs.
544     return *set() == *other.set();
545   }
546 }
547 
operator !=(const UnobservablesSet & other) const548 bool UnobservablesSet::operator!=(const UnobservablesSet& other) const {
549   return !(*this == other);
550 }
551 
operator ==(const UnobservableStore other) const552 bool UnobservableStore::operator==(const UnobservableStore other) const {
553   return (id_ == other.id_) && (offset_ == other.offset_);
554 }
555 
operator !=(const UnobservableStore other) const556 bool UnobservableStore::operator!=(const UnobservableStore other) const {
557   return !(*this == other);
558 }
559 
operator <(const UnobservableStore other) const560 bool UnobservableStore::operator<(const UnobservableStore other) const {
561   return (id_ < other.id_) || (id_ == other.id_ && offset_ < other.offset_);
562 }
563 
564 }  // namespace compiler
565 }  // namespace internal
566 }  // namespace v8
567