1 // Copyright 2021 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/wasm-inlining.h"
6
7 #include "src/compiler/all-nodes.h"
8 #include "src/compiler/compiler-source-position-table.h"
9 #include "src/compiler/node-matchers.h"
10 #include "src/compiler/wasm-compiler.h"
11 #include "src/wasm/function-body-decoder.h"
12 #include "src/wasm/graph-builder-interface.h"
13 #include "src/wasm/wasm-features.h"
14 #include "src/wasm/wasm-module.h"
15 #include "src/wasm/wasm-subtyping.h"
16
17 namespace v8 {
18 namespace internal {
19 namespace compiler {
20
Reduce(Node * node)21 Reduction WasmInliner::Reduce(Node* node) {
22 switch (node->opcode()) {
23 case IrOpcode::kCall:
24 case IrOpcode::kTailCall:
25 return ReduceCall(node);
26 default:
27 return NoChange();
28 }
29 }
30
31 #define TRACE(...) \
32 if (FLAG_trace_wasm_inlining) PrintF(__VA_ARGS__)
33
Trace(Node * call,int inlinee,const char * decision)34 void WasmInliner::Trace(Node* call, int inlinee, const char* decision) {
35 TRACE("[function %d: considering node %d, call to %d: %s]\n", function_index_,
36 call->id(), inlinee, decision);
37 }
38
FindOriginatingFunction(Node * call)39 uint32_t WasmInliner::FindOriginatingFunction(Node* call) {
40 DCHECK_EQ(inlined_functions_.size(), first_node_id_.size());
41 NodeId id = call->id();
42 if (inlined_functions_.size() == 0 || id < first_node_id_[0]) {
43 return function_index_;
44 }
45 for (size_t i = 1; i < first_node_id_.size(); i++) {
46 if (id < first_node_id_[i]) return inlined_functions_[i - 1];
47 }
48 DCHECK_GE(id, first_node_id_.back());
49 return inlined_functions_.back();
50 }
51
GetCallCount(Node * call)52 int WasmInliner::GetCallCount(Node* call) {
53 if (!FLAG_wasm_speculative_inlining) return 0;
54 base::MutexGuard guard(&module()->type_feedback.mutex);
55 wasm::WasmCodePosition position =
56 source_positions_->GetSourcePosition(call).ScriptOffset();
57 uint32_t func = FindOriginatingFunction(call);
58 auto maybe_feedback =
59 module()->type_feedback.feedback_for_function.find(func);
60 if (maybe_feedback == module()->type_feedback.feedback_for_function.end()) {
61 return 0;
62 }
63 wasm::FunctionTypeFeedback feedback = maybe_feedback->second;
64 // It's possible that we haven't processed the feedback yet. Currently,
65 // this can happen for targets of call_direct that haven't gotten hot yet,
66 // and for functions where Liftoff bailed out.
67 if (feedback.feedback_vector.size() == 0) return 0;
68 auto index_in_vector = feedback.positions.find(position);
69 if (index_in_vector == feedback.positions.end()) return 0;
70 return feedback.feedback_vector[index_in_vector->second]
71 .absolute_call_frequency;
72 }
73
74 // TODO(12166): Save inlined frames for trap/--trace-wasm purposes. Consider
75 // tail calls.
ReduceCall(Node * call)76 Reduction WasmInliner::ReduceCall(Node* call) {
77 DCHECK(call->opcode() == IrOpcode::kCall ||
78 call->opcode() == IrOpcode::kTailCall);
79
80 if (seen_.find(call) != seen_.end()) {
81 TRACE("function %d: have already seen node %d, skipping\n", function_index_,
82 call->id());
83 return NoChange();
84 }
85 seen_.insert(call);
86
87 Node* callee = NodeProperties::GetValueInput(call, 0);
88 IrOpcode::Value reloc_opcode = mcgraph_->machine()->Is32()
89 ? IrOpcode::kRelocatableInt32Constant
90 : IrOpcode::kRelocatableInt64Constant;
91 if (callee->opcode() != reloc_opcode) {
92 TRACE("[function %d: considering node %d... not a relocatable constant]\n",
93 function_index_, call->id());
94 return NoChange();
95 }
96 auto info = OpParameter<RelocatablePtrConstantInfo>(callee->op());
97 uint32_t inlinee_index = static_cast<uint32_t>(info.value());
98 if (info.rmode() != RelocInfo::WASM_CALL) {
99 Trace(call, inlinee_index, "not a wasm call");
100 return NoChange();
101 }
102 if (inlinee_index < module()->num_imported_functions) {
103 Trace(call, inlinee_index, "imported function");
104 return NoChange();
105 }
106 if (inlinee_index == function_index_) {
107 Trace(call, inlinee_index, "recursive call");
108 return NoChange();
109 }
110
111 Trace(call, inlinee_index, "adding to inlining candidates!");
112
113 int call_count = GetCallCount(call);
114
115 CHECK_LT(inlinee_index, module()->functions.size());
116 const wasm::WasmFunction* inlinee = &module()->functions[inlinee_index];
117 base::Vector<const byte> function_bytes = wire_bytes_->GetCode(inlinee->code);
118
119 CandidateInfo candidate{call, inlinee_index, call_count,
120 function_bytes.length()};
121
122 inlining_candidates_.push(candidate);
123 return NoChange();
124 }
125
SmallEnoughToInline(size_t current_graph_size,uint32_t candidate_size)126 bool SmallEnoughToInline(size_t current_graph_size, uint32_t candidate_size) {
127 if (WasmInliner::graph_size_allows_inlining(current_graph_size)) {
128 return true;
129 }
130 // For truly tiny functions, let's be a bit more generous.
131 return candidate_size < 10 &&
132 WasmInliner::graph_size_allows_inlining(current_graph_size - 100);
133 }
134
Trace(const CandidateInfo & candidate,const char * decision)135 void WasmInliner::Trace(const CandidateInfo& candidate, const char* decision) {
136 TRACE(
137 " [function %d: considering candidate {@%d, index=%d, count=%d, "
138 "size=%d}: %s]\n",
139 function_index_, candidate.node->id(), candidate.inlinee_index,
140 candidate.call_count, candidate.wire_byte_size, decision);
141 }
142
Finalize()143 void WasmInliner::Finalize() {
144 TRACE("function %d %s: going though inlining candidates...\n",
145 function_index_, debug_name_);
146 if (inlining_candidates_.empty()) return;
147 while (!inlining_candidates_.empty()) {
148 CandidateInfo candidate = inlining_candidates_.top();
149 inlining_candidates_.pop();
150 Node* call = candidate.node;
151 if (call->IsDead()) {
152 Trace(candidate, "dead node");
153 continue;
154 }
155 int min_count_for_inlining = candidate.wire_byte_size / 2;
156 if (candidate.call_count < min_count_for_inlining) {
157 Trace(candidate, "not called often enough");
158 continue;
159 }
160 // We could build the candidate's graph first and consider its node count,
161 // but it turns out that wire byte size and node count are quite strongly
162 // correlated, at about 1.16 nodes per wire byte (measured for J2Wasm).
163 if (!SmallEnoughToInline(current_graph_size_, candidate.wire_byte_size)) {
164 Trace(candidate, "not enough inlining budget");
165 continue;
166 }
167 const wasm::WasmFunction* inlinee =
168 &module()->functions[candidate.inlinee_index];
169 base::Vector<const byte> function_bytes =
170 wire_bytes_->GetCode(inlinee->code);
171 // We use the signature based on the real argument types stored in the call
172 // node. This is more specific than the callee's formal signature and might
173 // enable some optimizations.
174 const wasm::FunctionSig* specialized_sig =
175 CallDescriptorOf(call->op())->wasm_sig();
176
177 #if DEBUG
178 // Check that the real signature is a subtype of the formal one.
179 const wasm::FunctionSig* formal_sig =
180 WasmGraphBuilder::Int64LoweredSig(zone(), inlinee->sig);
181 CHECK_EQ(specialized_sig->parameter_count(), formal_sig->parameter_count());
182 CHECK_EQ(specialized_sig->return_count(), formal_sig->return_count());
183 for (size_t i = 0; i < specialized_sig->parameter_count(); i++) {
184 CHECK(wasm::IsSubtypeOf(specialized_sig->GetParam(i),
185 formal_sig->GetParam(i), module()));
186 }
187 for (size_t i = 0; i < specialized_sig->return_count(); i++) {
188 CHECK(wasm::IsSubtypeOf(formal_sig->GetReturn(i),
189 specialized_sig->GetReturn(i), module()));
190 }
191 #endif
192
193 wasm::WasmFeatures detected;
194 std::vector<WasmLoopInfo> inlinee_loop_infos;
195
196 size_t subgraph_min_node_id = graph()->NodeCount();
197 Node* inlinee_start;
198 Node* inlinee_end;
199 for (const wasm::FunctionSig* sig = specialized_sig;;) {
200 const wasm::FunctionBody inlinee_body(sig, inlinee->code.offset(),
201 function_bytes.begin(),
202 function_bytes.end());
203 WasmGraphBuilder builder(env_, zone(), mcgraph_, inlinee_body.sig,
204 source_positions_);
205 Graph::SubgraphScope scope(graph());
206 wasm::DecodeResult result = wasm::BuildTFGraph(
207 zone()->allocator(), env_->enabled_features, module(), &builder,
208 &detected, inlinee_body, &inlinee_loop_infos, node_origins_,
209 candidate.inlinee_index,
210 NodeProperties::IsExceptionalCall(call)
211 ? wasm::kInlinedHandledCall
212 : wasm::kInlinedNonHandledCall);
213 if (result.ok()) {
214 builder.LowerInt64(WasmGraphBuilder::kCalledFromWasm);
215 inlinee_start = graph()->start();
216 inlinee_end = graph()->end();
217 break;
218 }
219 if (sig == specialized_sig) {
220 // One possible reason for failure is the opportunistic signature
221 // specialization. Try again without that.
222 sig = inlinee->sig;
223 inlinee_loop_infos.clear();
224 Trace(candidate, "retrying with original signature");
225 continue;
226 }
227 // Otherwise report failure.
228 Trace(candidate, "failed to compile");
229 return;
230 }
231
232 size_t additional_nodes = graph()->NodeCount() - subgraph_min_node_id;
233 Trace(candidate, "inlining!");
234 current_graph_size_ += additional_nodes;
235 inlined_functions_.push_back(candidate.inlinee_index);
236 static_assert(std::is_same_v<NodeId, uint32_t>);
237 first_node_id_.push_back(static_cast<uint32_t>(subgraph_min_node_id));
238
239 if (call->opcode() == IrOpcode::kCall) {
240 InlineCall(call, inlinee_start, inlinee_end, inlinee->sig,
241 subgraph_min_node_id);
242 } else {
243 InlineTailCall(call, inlinee_start, inlinee_end);
244 }
245 call->Kill();
246 loop_infos_->insert(loop_infos_->end(), inlinee_loop_infos.begin(),
247 inlinee_loop_infos.end());
248 // Returning after only one inlining has been tried and found worse.
249 }
250 }
251
252 /* Rewire callee formal parameters to the call-site real parameters. Rewire
253 * effect and control dependencies of callee's start node with the respective
254 * inputs of the call node.
255 */
RewireFunctionEntry(Node * call,Node * callee_start)256 void WasmInliner::RewireFunctionEntry(Node* call, Node* callee_start) {
257 Node* control = NodeProperties::GetControlInput(call);
258 Node* effect = NodeProperties::GetEffectInput(call);
259
260 for (Edge edge : callee_start->use_edges()) {
261 Node* use = edge.from();
262 switch (use->opcode()) {
263 case IrOpcode::kParameter: {
264 // Index 0 is the callee node.
265 int index = 1 + ParameterIndexOf(use->op());
266 Replace(use, NodeProperties::GetValueInput(call, index));
267 break;
268 }
269 default:
270 if (NodeProperties::IsEffectEdge(edge)) {
271 edge.UpdateTo(effect);
272 } else if (NodeProperties::IsControlEdge(edge)) {
273 // Projections pointing to the inlinee start are floating control.
274 // They should point to the graph's start.
275 edge.UpdateTo(use->opcode() == IrOpcode::kProjection
276 ? graph()->start()
277 : control);
278 } else {
279 UNREACHABLE();
280 }
281 Revisit(edge.from());
282 break;
283 }
284 }
285 }
286
InlineTailCall(Node * call,Node * callee_start,Node * callee_end)287 void WasmInliner::InlineTailCall(Node* call, Node* callee_start,
288 Node* callee_end) {
289 DCHECK_EQ(call->opcode(), IrOpcode::kTailCall);
290 // 1) Rewire function entry.
291 RewireFunctionEntry(call, callee_start);
292 // 2) For tail calls, all we have to do is rewire all terminators of the
293 // inlined graph to the end of the caller graph.
294 for (Node* const input : callee_end->inputs()) {
295 DCHECK(IrOpcode::IsGraphTerminator(input->opcode()));
296 NodeProperties::MergeControlToEnd(graph(), common(), input);
297 }
298 for (Edge edge_to_end : call->use_edges()) {
299 DCHECK_EQ(edge_to_end.from(), graph()->end());
300 edge_to_end.UpdateTo(mcgraph()->Dead());
301 }
302 callee_end->Kill();
303 call->Kill();
304 Revisit(graph()->end());
305 }
306
307 namespace {
308 // graph-builder-interface generates a dangling exception handler for each
309 // throwing call in the inlinee. This might be followed by a LoopExit node.
DanglingHandler(Node * call)310 Node* DanglingHandler(Node* call) {
311 Node* if_exception = nullptr;
312 for (Node* use : call->uses()) {
313 if (use->opcode() == IrOpcode::kIfException) {
314 if_exception = use;
315 break;
316 }
317 }
318 DCHECK_NOT_NULL(if_exception);
319
320 // If this handler is dangling, return it.
321 if (if_exception->UseCount() == 0) return if_exception;
322
323 for (Node* use : if_exception->uses()) {
324 // Otherwise, look for a LoopExit use of this handler.
325 if (use->opcode() == IrOpcode::kLoopExit) {
326 for (Node* loop_exit_use : use->uses()) {
327 if (loop_exit_use->opcode() != IrOpcode::kLoopExitEffect &&
328 loop_exit_use->opcode() != IrOpcode::kLoopExitValue) {
329 // This LoopExit has a use other than LoopExitEffect/Value, so it is
330 // not dangling.
331 return nullptr;
332 }
333 }
334 return use;
335 }
336 }
337
338 return nullptr;
339 }
340 } // namespace
341
InlineCall(Node * call,Node * callee_start,Node * callee_end,const wasm::FunctionSig * inlinee_sig,size_t subgraph_min_node_id)342 void WasmInliner::InlineCall(Node* call, Node* callee_start, Node* callee_end,
343 const wasm::FunctionSig* inlinee_sig,
344 size_t subgraph_min_node_id) {
345 DCHECK_EQ(call->opcode(), IrOpcode::kCall);
346
347 // 0) Before doing anything, if {call} has an exception handler, collect all
348 // unhandled calls in the subgraph.
349 Node* handler = nullptr;
350 std::vector<Node*> dangling_handlers;
351 if (NodeProperties::IsExceptionalCall(call, &handler)) {
352 AllNodes subgraph_nodes(zone(), callee_end, graph());
353 for (Node* node : subgraph_nodes.reachable) {
354 if (node->id() >= subgraph_min_node_id &&
355 !node->op()->HasProperty(Operator::kNoThrow)) {
356 Node* dangling_handler = DanglingHandler(node);
357 if (dangling_handler != nullptr) {
358 dangling_handlers.push_back(dangling_handler);
359 }
360 }
361 }
362 }
363
364 // 1) Rewire function entry.
365 RewireFunctionEntry(call, callee_start);
366
367 // 2) Handle all graph terminators for the callee.
368 NodeVector return_nodes(zone());
369 for (Node* const input : callee_end->inputs()) {
370 DCHECK(IrOpcode::IsGraphTerminator(input->opcode()));
371 switch (input->opcode()) {
372 case IrOpcode::kReturn:
373 // Returns are collected to be rewired into the caller graph later.
374 return_nodes.push_back(input);
375 break;
376 case IrOpcode::kDeoptimize:
377 case IrOpcode::kTerminate:
378 case IrOpcode::kThrow:
379 NodeProperties::MergeControlToEnd(graph(), common(), input);
380 Revisit(graph()->end());
381 break;
382 case IrOpcode::kTailCall: {
383 // A tail call in the callee inlined in a regular call in the caller has
384 // to be transformed into a regular call, and then returned from the
385 // inlinee. It will then be handled like any other return.
386 auto descriptor = CallDescriptorOf(input->op());
387 NodeProperties::ChangeOp(input, common()->Call(descriptor));
388 int return_arity = static_cast<int>(inlinee_sig->return_count());
389 NodeVector return_inputs(zone());
390 // The first input of a return node is always the 0 constant.
391 return_inputs.push_back(graph()->NewNode(common()->Int32Constant(0)));
392 if (return_arity == 1) {
393 return_inputs.push_back(input);
394 } else if (return_arity > 1) {
395 for (int i = 0; i < return_arity; i++) {
396 return_inputs.push_back(
397 graph()->NewNode(common()->Projection(i), input, input));
398 }
399 }
400
401 // Add effect and control inputs.
402 return_inputs.push_back(input->op()->EffectOutputCount() > 0
403 ? input
404 : NodeProperties::GetEffectInput(input));
405 return_inputs.push_back(input->op()->ControlOutputCount() > 0
406 ? input
407 : NodeProperties::GetControlInput(input));
408
409 Node* ret = graph()->NewNode(common()->Return(return_arity),
410 static_cast<int>(return_inputs.size()),
411 return_inputs.data());
412 return_nodes.push_back(ret);
413 break;
414 }
415 default:
416 UNREACHABLE();
417 }
418 }
419 callee_end->Kill();
420
421 // 3) Rewire unhandled calls to the handler.
422 int handler_count = static_cast<int>(dangling_handlers.size());
423
424 if (handler_count > 0) {
425 Node* control_output =
426 graph()->NewNode(common()->Merge(handler_count), handler_count,
427 dangling_handlers.data());
428 std::vector<Node*> effects;
429 std::vector<Node*> values;
430 for (Node* control : dangling_handlers) {
431 if (control->opcode() == IrOpcode::kIfException) {
432 effects.push_back(control);
433 values.push_back(control);
434 } else {
435 DCHECK_EQ(control->opcode(), IrOpcode::kLoopExit);
436 Node* if_exception = control->InputAt(0);
437 DCHECK_EQ(if_exception->opcode(), IrOpcode::kIfException);
438 effects.push_back(graph()->NewNode(common()->LoopExitEffect(),
439 if_exception, control));
440 values.push_back(graph()->NewNode(
441 common()->LoopExitValue(MachineRepresentation::kTagged),
442 if_exception, control));
443 }
444 }
445
446 effects.push_back(control_output);
447 values.push_back(control_output);
448 Node* value_output = graph()->NewNode(
449 common()->Phi(MachineRepresentation::kTagged, handler_count),
450 handler_count + 1, values.data());
451 Node* effect_output = graph()->NewNode(common()->EffectPhi(handler_count),
452 handler_count + 1, effects.data());
453 ReplaceWithValue(handler, value_output, effect_output, control_output);
454 } else if (handler != nullptr) {
455 // Nothing in the inlined function can throw. Remove the handler.
456 ReplaceWithValue(handler, mcgraph()->Dead(), mcgraph()->Dead(),
457 mcgraph()->Dead());
458 }
459
460 if (return_nodes.size() > 0) {
461 /* 4) Collect all return site value, effect, and control inputs into phis
462 * and merges. */
463 int const return_count = static_cast<int>(return_nodes.size());
464 NodeVector controls(zone());
465 NodeVector effects(zone());
466 for (Node* const return_node : return_nodes) {
467 controls.push_back(NodeProperties::GetControlInput(return_node));
468 effects.push_back(NodeProperties::GetEffectInput(return_node));
469 }
470 Node* control_output = graph()->NewNode(common()->Merge(return_count),
471 return_count, &controls.front());
472 effects.push_back(control_output);
473 Node* effect_output =
474 graph()->NewNode(common()->EffectPhi(return_count),
475 static_cast<int>(effects.size()), &effects.front());
476
477 // The first input of a return node is discarded. This is because Wasm
478 // functions always return an additional 0 constant as a first return value.
479 DCHECK(
480 Int32Matcher(NodeProperties::GetValueInput(return_nodes[0], 0)).Is(0));
481 int const return_arity = return_nodes[0]->op()->ValueInputCount() - 1;
482 NodeVector values(zone());
483 for (int i = 0; i < return_arity; i++) {
484 NodeVector ith_values(zone());
485 for (Node* const return_node : return_nodes) {
486 Node* value = NodeProperties::GetValueInput(return_node, i + 1);
487 ith_values.push_back(value);
488 }
489 ith_values.push_back(control_output);
490 // Find the correct machine representation for the return values from the
491 // inlinee signature.
492 MachineRepresentation repr =
493 inlinee_sig->GetReturn(i).machine_representation();
494 Node* ith_value_output = graph()->NewNode(
495 common()->Phi(repr, return_count),
496 static_cast<int>(ith_values.size()), &ith_values.front());
497 values.push_back(ith_value_output);
498 }
499 for (Node* return_node : return_nodes) return_node->Kill();
500
501 if (return_arity == 0) {
502 // Void function, no value uses.
503 ReplaceWithValue(call, mcgraph()->Dead(), effect_output, control_output);
504 } else if (return_arity == 1) {
505 // One return value. Just replace value uses of the call node with it.
506 ReplaceWithValue(call, values[0], effect_output, control_output);
507 } else {
508 // Multiple returns. We have to find the projections of the call node and
509 // replace them with the returned values.
510 for (Edge use_edge : call->use_edges()) {
511 if (NodeProperties::IsValueEdge(use_edge)) {
512 Node* use = use_edge.from();
513 DCHECK_EQ(use->opcode(), IrOpcode::kProjection);
514 ReplaceWithValue(use, values[ProjectionIndexOf(use->op())]);
515 }
516 }
517 // All value inputs are replaced by the above loop, so it is ok to use
518 // Dead() as a dummy for value replacement.
519 ReplaceWithValue(call, mcgraph()->Dead(), effect_output, control_output);
520 }
521 } else {
522 // The callee can never return. The call node and all its uses are dead.
523 ReplaceWithValue(call, mcgraph()->Dead(), mcgraph()->Dead(),
524 mcgraph()->Dead());
525 }
526 }
527
module() const528 const wasm::WasmModule* WasmInliner::module() const { return env_->module; }
529
530 #undef TRACE
531
532 } // namespace compiler
533 } // namespace internal
534 } // namespace v8
535