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 "src/compiler/memory-optimizer.h"
6
7 #include "src/base/logging.h"
8 #include "src/codegen/interface-descriptors.h"
9 #include "src/codegen/tick-counter.h"
10 #include "src/compiler/js-graph.h"
11 #include "src/compiler/linkage.h"
12 #include "src/compiler/node-matchers.h"
13 #include "src/compiler/node-properties.h"
14 #include "src/compiler/node.h"
15 #include "src/roots/roots-inl.h"
16
17 namespace v8 {
18 namespace internal {
19 namespace compiler {
20
21 namespace {
22
CanAllocate(const Node * node)23 bool CanAllocate(const Node* node) {
24 switch (node->opcode()) {
25 case IrOpcode::kAbortCSAAssert:
26 case IrOpcode::kBitcastTaggedToWord:
27 case IrOpcode::kBitcastWordToTagged:
28 case IrOpcode::kComment:
29 case IrOpcode::kDebugBreak:
30 case IrOpcode::kDeoptimizeIf:
31 case IrOpcode::kDeoptimizeUnless:
32 case IrOpcode::kEffectPhi:
33 case IrOpcode::kIfException:
34 case IrOpcode::kLoad:
35 case IrOpcode::kLoadElement:
36 case IrOpcode::kLoadField:
37 case IrOpcode::kLoadFromObject:
38 case IrOpcode::kPoisonedLoad:
39 case IrOpcode::kProtectedLoad:
40 case IrOpcode::kProtectedStore:
41 case IrOpcode::kRetain:
42 case IrOpcode::kStackPointerGreaterThan:
43 case IrOpcode::kStaticAssert:
44 // TODO(tebbi): Store nodes might do a bump-pointer allocation.
45 // We should introduce a special bump-pointer store node to
46 // differentiate that.
47 case IrOpcode::kStore:
48 case IrOpcode::kStoreElement:
49 case IrOpcode::kStoreField:
50 case IrOpcode::kStoreToObject:
51 case IrOpcode::kTaggedPoisonOnSpeculation:
52 case IrOpcode::kUnalignedLoad:
53 case IrOpcode::kUnalignedStore:
54 case IrOpcode::kUnreachable:
55 case IrOpcode::kUnsafePointerAdd:
56 case IrOpcode::kWord32AtomicAdd:
57 case IrOpcode::kWord32AtomicAnd:
58 case IrOpcode::kWord32AtomicCompareExchange:
59 case IrOpcode::kWord32AtomicExchange:
60 case IrOpcode::kWord32AtomicLoad:
61 case IrOpcode::kWord32AtomicOr:
62 case IrOpcode::kWord32AtomicPairAdd:
63 case IrOpcode::kWord32AtomicPairAnd:
64 case IrOpcode::kWord32AtomicPairCompareExchange:
65 case IrOpcode::kWord32AtomicPairExchange:
66 case IrOpcode::kWord32AtomicPairLoad:
67 case IrOpcode::kWord32AtomicPairOr:
68 case IrOpcode::kWord32AtomicPairStore:
69 case IrOpcode::kWord32AtomicPairSub:
70 case IrOpcode::kWord32AtomicPairXor:
71 case IrOpcode::kWord32AtomicStore:
72 case IrOpcode::kWord32AtomicSub:
73 case IrOpcode::kWord32AtomicXor:
74 case IrOpcode::kWord32PoisonOnSpeculation:
75 case IrOpcode::kWord64AtomicAdd:
76 case IrOpcode::kWord64AtomicAnd:
77 case IrOpcode::kWord64AtomicCompareExchange:
78 case IrOpcode::kWord64AtomicExchange:
79 case IrOpcode::kWord64AtomicLoad:
80 case IrOpcode::kWord64AtomicOr:
81 case IrOpcode::kWord64AtomicStore:
82 case IrOpcode::kWord64AtomicSub:
83 case IrOpcode::kWord64AtomicXor:
84 case IrOpcode::kWord64PoisonOnSpeculation:
85 return false;
86
87 case IrOpcode::kCall:
88 return !(CallDescriptorOf(node->op())->flags() &
89 CallDescriptor::kNoAllocate);
90 default:
91 break;
92 }
93 return true;
94 }
95
SearchAllocatingNode(Node * start,Node * limit,Zone * temp_zone)96 Node* SearchAllocatingNode(Node* start, Node* limit, Zone* temp_zone) {
97 ZoneQueue<Node*> queue(temp_zone);
98 ZoneSet<Node*> visited(temp_zone);
99 visited.insert(limit);
100 queue.push(start);
101
102 while (!queue.empty()) {
103 Node* const current = queue.front();
104 queue.pop();
105 if (visited.find(current) == visited.end()) {
106 visited.insert(current);
107
108 if (CanAllocate(current)) {
109 return current;
110 }
111
112 for (int i = 0; i < current->op()->EffectInputCount(); ++i) {
113 queue.push(NodeProperties::GetEffectInput(current, i));
114 }
115 }
116 }
117 return nullptr;
118 }
119
CanLoopAllocate(Node * loop_effect_phi,Zone * temp_zone)120 bool CanLoopAllocate(Node* loop_effect_phi, Zone* temp_zone) {
121 Node* const control = NodeProperties::GetControlInput(loop_effect_phi);
122 // Start the effect chain walk from the loop back edges.
123 for (int i = 1; i < control->InputCount(); ++i) {
124 if (SearchAllocatingNode(loop_effect_phi->InputAt(i), loop_effect_phi,
125 temp_zone) != nullptr) {
126 return true;
127 }
128 }
129 return false;
130 }
131
EffectPhiForPhi(Node * phi)132 Node* EffectPhiForPhi(Node* phi) {
133 Node* control = NodeProperties::GetControlInput(phi);
134 for (Node* use : control->uses()) {
135 if (use->opcode() == IrOpcode::kEffectPhi) {
136 return use;
137 }
138 }
139 return nullptr;
140 }
141
WriteBarrierAssertFailed(Node * node,Node * object,const char * name,Zone * temp_zone)142 void WriteBarrierAssertFailed(Node* node, Node* object, const char* name,
143 Zone* temp_zone) {
144 std::stringstream str;
145 str << "MemoryOptimizer could not remove write barrier for node #"
146 << node->id() << "\n";
147 str << " Run mksnapshot with --csa-trap-on-node=" << name << ","
148 << node->id() << " to break in CSA code.\n";
149 Node* object_position = object;
150 if (object_position->opcode() == IrOpcode::kPhi) {
151 object_position = EffectPhiForPhi(object_position);
152 }
153 Node* allocating_node = nullptr;
154 if (object_position && object_position->op()->EffectOutputCount() > 0) {
155 allocating_node = SearchAllocatingNode(node, object_position, temp_zone);
156 }
157 if (allocating_node) {
158 str << "\n There is a potentially allocating node in between:\n";
159 str << " " << *allocating_node << "\n";
160 str << " Run mksnapshot with --csa-trap-on-node=" << name << ","
161 << allocating_node->id() << " to break there.\n";
162 if (allocating_node->opcode() == IrOpcode::kCall) {
163 str << " If this is a never-allocating runtime call, you can add an "
164 "exception to Runtime::MayAllocate.\n";
165 }
166 } else {
167 str << "\n It seems the store happened to something different than a "
168 "direct "
169 "allocation:\n";
170 str << " " << *object << "\n";
171 str << " Run mksnapshot with --csa-trap-on-node=" << name << ","
172 << object->id() << " to break there.\n";
173 }
174 FATAL("%s", str.str().c_str());
175 }
176
177 } // namespace
178
MemoryOptimizer(JSGraph * jsgraph,Zone * zone,PoisoningMitigationLevel poisoning_level,MemoryLowering::AllocationFolding allocation_folding,const char * function_debug_name,TickCounter * tick_counter)179 MemoryOptimizer::MemoryOptimizer(
180 JSGraph* jsgraph, Zone* zone, PoisoningMitigationLevel poisoning_level,
181 MemoryLowering::AllocationFolding allocation_folding,
182 const char* function_debug_name, TickCounter* tick_counter)
183 : graph_assembler_(jsgraph, zone),
184 memory_lowering_(jsgraph, zone, &graph_assembler_, poisoning_level,
185 allocation_folding, WriteBarrierAssertFailed,
186 function_debug_name),
187 jsgraph_(jsgraph),
188 empty_state_(AllocationState::Empty(zone)),
189 pending_(zone),
190 tokens_(zone),
191 zone_(zone),
192 tick_counter_(tick_counter) {}
193
Optimize()194 void MemoryOptimizer::Optimize() {
195 EnqueueUses(graph()->start(), empty_state());
196 while (!tokens_.empty()) {
197 Token const token = tokens_.front();
198 tokens_.pop();
199 VisitNode(token.node, token.state);
200 }
201 DCHECK(pending_.empty());
202 DCHECK(tokens_.empty());
203 }
204
VisitNode(Node * node,AllocationState const * state)205 void MemoryOptimizer::VisitNode(Node* node, AllocationState const* state) {
206 tick_counter_->TickAndMaybeEnterSafepoint();
207 DCHECK(!node->IsDead());
208 DCHECK_LT(0, node->op()->EffectInputCount());
209 switch (node->opcode()) {
210 case IrOpcode::kAllocate:
211 // Allocate nodes were purged from the graph in effect-control
212 // linearization.
213 UNREACHABLE();
214 case IrOpcode::kAllocateRaw:
215 return VisitAllocateRaw(node, state);
216 case IrOpcode::kCall:
217 return VisitCall(node, state);
218 case IrOpcode::kLoadFromObject:
219 return VisitLoadFromObject(node, state);
220 case IrOpcode::kLoadElement:
221 return VisitLoadElement(node, state);
222 case IrOpcode::kLoadField:
223 return VisitLoadField(node, state);
224 case IrOpcode::kStoreToObject:
225 return VisitStoreToObject(node, state);
226 case IrOpcode::kStoreElement:
227 return VisitStoreElement(node, state);
228 case IrOpcode::kStoreField:
229 return VisitStoreField(node, state);
230 case IrOpcode::kStore:
231 return VisitStore(node, state);
232 default:
233 if (!CanAllocate(node)) {
234 // These operations cannot trigger GC.
235 return VisitOtherEffect(node, state);
236 }
237 }
238 DCHECK_EQ(0, node->op()->EffectOutputCount());
239 }
240
AllocationTypeNeedsUpdateToOld(Node * const node,const Edge edge)241 bool MemoryOptimizer::AllocationTypeNeedsUpdateToOld(Node* const node,
242 const Edge edge) {
243 // Test to see if we need to update the AllocationType.
244 if (node->opcode() == IrOpcode::kStoreField && edge.index() == 1) {
245 Node* parent = node->InputAt(0);
246 if (parent->opcode() == IrOpcode::kAllocateRaw &&
247 AllocationTypeOf(parent->op()) == AllocationType::kOld) {
248 return true;
249 }
250 }
251
252 return false;
253 }
254
VisitAllocateRaw(Node * node,AllocationState const * state)255 void MemoryOptimizer::VisitAllocateRaw(Node* node,
256 AllocationState const* state) {
257 DCHECK_EQ(IrOpcode::kAllocateRaw, node->opcode());
258 const AllocateParameters& allocation = AllocateParametersOf(node->op());
259 AllocationType allocation_type = allocation.allocation_type();
260
261 // Propagate tenuring from outer allocations to inner allocations, i.e.
262 // when we allocate an object in old space and store a newly allocated
263 // child object into the pretenured object, then the newly allocated
264 // child object also should get pretenured to old space.
265 if (allocation_type == AllocationType::kOld) {
266 for (Edge const edge : node->use_edges()) {
267 Node* const user = edge.from();
268 if (user->opcode() == IrOpcode::kStoreField && edge.index() == 0) {
269 Node* child = user->InputAt(1);
270 if (child->opcode() == IrOpcode::kAllocateRaw &&
271 AllocationTypeOf(child->op()) == AllocationType::kYoung) {
272 NodeProperties::ChangeOp(child, node->op());
273 break;
274 }
275 }
276 }
277 } else {
278 DCHECK_EQ(AllocationType::kYoung, allocation_type);
279 for (Edge const edge : node->use_edges()) {
280 Node* const user = edge.from();
281 if (AllocationTypeNeedsUpdateToOld(user, edge)) {
282 allocation_type = AllocationType::kOld;
283 break;
284 }
285 }
286 }
287
288 Reduction reduction = memory_lowering()->ReduceAllocateRaw(
289 node, allocation_type, allocation.allow_large_objects(), &state);
290 CHECK(reduction.Changed() && reduction.replacement() != node);
291
292 // Replace all uses of node and kill the node to make sure we don't leave
293 // dangling dead uses.
294 NodeProperties::ReplaceUses(node, reduction.replacement(),
295 graph_assembler_.effect(),
296 graph_assembler_.control());
297 node->Kill();
298
299 EnqueueUses(state->effect(), state);
300 }
301
VisitLoadFromObject(Node * node,AllocationState const * state)302 void MemoryOptimizer::VisitLoadFromObject(Node* node,
303 AllocationState const* state) {
304 DCHECK_EQ(IrOpcode::kLoadFromObject, node->opcode());
305 memory_lowering()->ReduceLoadFromObject(node);
306 EnqueueUses(node, state);
307 }
308
VisitStoreToObject(Node * node,AllocationState const * state)309 void MemoryOptimizer::VisitStoreToObject(Node* node,
310 AllocationState const* state) {
311 DCHECK_EQ(IrOpcode::kStoreToObject, node->opcode());
312 memory_lowering()->ReduceStoreToObject(node, state);
313 EnqueueUses(node, state);
314 }
315
VisitLoadElement(Node * node,AllocationState const * state)316 void MemoryOptimizer::VisitLoadElement(Node* node,
317 AllocationState const* state) {
318 DCHECK_EQ(IrOpcode::kLoadElement, node->opcode());
319 memory_lowering()->ReduceLoadElement(node);
320 EnqueueUses(node, state);
321 }
322
VisitLoadField(Node * node,AllocationState const * state)323 void MemoryOptimizer::VisitLoadField(Node* node, AllocationState const* state) {
324 DCHECK_EQ(IrOpcode::kLoadField, node->opcode());
325 Reduction reduction = memory_lowering()->ReduceLoadField(node);
326 DCHECK(reduction.Changed());
327 // In case of replacement, the replacement graph should not require futher
328 // lowering, so we can proceed iterating the graph from the node uses.
329 EnqueueUses(node, state);
330
331 // Node can be replaced only when V8_HEAP_SANDBOX_BOOL is enabled and
332 // when loading an external pointer value.
333 DCHECK_IMPLIES(!V8_HEAP_SANDBOX_BOOL, reduction.replacement() == node);
334 if (V8_HEAP_SANDBOX_BOOL && reduction.replacement() != node) {
335 // Replace all uses of node and kill the node to make sure we don't leave
336 // dangling dead uses.
337 NodeProperties::ReplaceUses(node, reduction.replacement(),
338 graph_assembler_.effect(),
339 graph_assembler_.control());
340 node->Kill();
341 }
342 }
343
VisitStoreElement(Node * node,AllocationState const * state)344 void MemoryOptimizer::VisitStoreElement(Node* node,
345 AllocationState const* state) {
346 DCHECK_EQ(IrOpcode::kStoreElement, node->opcode());
347 memory_lowering()->ReduceStoreElement(node, state);
348 EnqueueUses(node, state);
349 }
350
VisitStoreField(Node * node,AllocationState const * state)351 void MemoryOptimizer::VisitStoreField(Node* node,
352 AllocationState const* state) {
353 DCHECK_EQ(IrOpcode::kStoreField, node->opcode());
354 memory_lowering()->ReduceStoreField(node, state);
355 EnqueueUses(node, state);
356 }
VisitStore(Node * node,AllocationState const * state)357 void MemoryOptimizer::VisitStore(Node* node, AllocationState const* state) {
358 DCHECK_EQ(IrOpcode::kStore, node->opcode());
359 memory_lowering()->ReduceStore(node, state);
360 EnqueueUses(node, state);
361 }
362
VisitCall(Node * node,AllocationState const * state)363 void MemoryOptimizer::VisitCall(Node* node, AllocationState const* state) {
364 DCHECK_EQ(IrOpcode::kCall, node->opcode());
365 // If the call can allocate, we start with a fresh state.
366 if (!(CallDescriptorOf(node->op())->flags() & CallDescriptor::kNoAllocate)) {
367 state = empty_state();
368 }
369 EnqueueUses(node, state);
370 }
371
VisitOtherEffect(Node * node,AllocationState const * state)372 void MemoryOptimizer::VisitOtherEffect(Node* node,
373 AllocationState const* state) {
374 EnqueueUses(node, state);
375 }
376
MergeStates(AllocationStates const & states)377 MemoryOptimizer::AllocationState const* MemoryOptimizer::MergeStates(
378 AllocationStates const& states) {
379 // Check if all states are the same; or at least if all allocation
380 // states belong to the same allocation group.
381 AllocationState const* state = states.front();
382 MemoryLowering::AllocationGroup* group = state->group();
383 for (size_t i = 1; i < states.size(); ++i) {
384 if (states[i] != state) state = nullptr;
385 if (states[i]->group() != group) group = nullptr;
386 }
387 if (state == nullptr) {
388 if (group != nullptr) {
389 // We cannot fold any more allocations into this group, but we can still
390 // eliminate write barriers on stores to this group.
391 // TODO(bmeurer): We could potentially just create a Phi here to merge
392 // the various tops; but we need to pay special attention not to create
393 // an unschedulable graph.
394 state = AllocationState::Closed(group, nullptr, zone());
395 } else {
396 // The states are from different allocation groups.
397 state = empty_state();
398 }
399 }
400 return state;
401 }
402
EnqueueMerge(Node * node,int index,AllocationState const * state)403 void MemoryOptimizer::EnqueueMerge(Node* node, int index,
404 AllocationState const* state) {
405 DCHECK_EQ(IrOpcode::kEffectPhi, node->opcode());
406 int const input_count = node->InputCount() - 1;
407 DCHECK_LT(0, input_count);
408 Node* const control = node->InputAt(input_count);
409 if (control->opcode() == IrOpcode::kLoop) {
410 if (index == 0) {
411 if (CanLoopAllocate(node, zone())) {
412 // If the loop can allocate, we start with an empty state at the
413 // beginning.
414 EnqueueUses(node, empty_state());
415 } else {
416 // If the loop cannot allocate, we can just propagate the state from
417 // before the loop.
418 EnqueueUses(node, state);
419 }
420 } else {
421 // Do not revisit backedges.
422 }
423 } else {
424 DCHECK_EQ(IrOpcode::kMerge, control->opcode());
425 // Check if we already know about this pending merge.
426 NodeId const id = node->id();
427 auto it = pending_.find(id);
428 if (it == pending_.end()) {
429 // Insert a new pending merge.
430 it = pending_.insert(std::make_pair(id, AllocationStates(zone()))).first;
431 }
432 // Add the next input state.
433 it->second.push_back(state);
434 // Check if states for all inputs are available by now.
435 if (it->second.size() == static_cast<size_t>(input_count)) {
436 // All inputs to this effect merge are done, merge the states given all
437 // input constraints, drop the pending merge and enqueue uses of the
438 // EffectPhi {node}.
439 state = MergeStates(it->second);
440 EnqueueUses(node, state);
441 pending_.erase(it);
442 }
443 }
444 }
445
EnqueueUses(Node * node,AllocationState const * state)446 void MemoryOptimizer::EnqueueUses(Node* node, AllocationState const* state) {
447 for (Edge const edge : node->use_edges()) {
448 if (NodeProperties::IsEffectEdge(edge)) {
449 EnqueueUse(edge.from(), edge.index(), state);
450 }
451 }
452 }
453
EnqueueUse(Node * node,int index,AllocationState const * state)454 void MemoryOptimizer::EnqueueUse(Node* node, int index,
455 AllocationState const* state) {
456 if (node->opcode() == IrOpcode::kEffectPhi) {
457 // An EffectPhi represents a merge of different effect chains, which
458 // needs special handling depending on whether the merge is part of a
459 // loop or just a normal control join.
460 EnqueueMerge(node, index, state);
461 } else {
462 Token token = {node, state};
463 tokens_.push(token);
464 }
465 }
466
graph() const467 Graph* MemoryOptimizer::graph() const { return jsgraph()->graph(); }
468
469 } // namespace compiler
470 } // namespace internal
471 } // namespace v8
472