Jlm
Loading...
Searching...
No Matches
inlining.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2017 Nico Reißmann <nico.reissmann@gmail.com>
3 * See COPYING for terms of redistribution.
4 */
5
13#include <jlm/llvm/ir/Trace.hpp>
15#include <jlm/rvsdg/gamma.hpp>
17#include <jlm/rvsdg/theta.hpp>
18#include <jlm/rvsdg/Trace.hpp>
21#include <jlm/util/time.hpp>
22
23namespace jlm::llvm
24{
25
27{
28 // The total number of lambda nodes
29 static constexpr const char * NumFunctions_ = "#Functions";
30 // The total number of lambda nodes that are marked as possible to inline
31 static constexpr const char * NumInlineableFunctions_ = "#InlineableFunctions";
32 // The total number of call operations
33 static constexpr const char * NumFunctionCalls_ = "#FunctionCalls";
34 // The number of call operations that could in theory be inlined
35 static constexpr const char * NumInlineableCalls_ = "#InlinableCalls";
36 // The number of call operations that were actually inlined
37 static constexpr const char * NumCallsInlined_ = "#CallsInlined";
38
39public:
40 ~Statistics() override = default;
41
42 explicit Statistics(const util::FilePath & sourceFile)
43 : util::Statistics(Id::FunctionInlining, sourceFile)
44 {}
45
46 void
48 {
49 AddTimer(Label::Timer).start();
50 }
51
52 void
54 size_t numFunctions,
55 size_t numInlineableFunctions,
56 size_t numFunctionCalls,
57 size_t numInlineableCalls,
58 size_t numCallsInlined)
59 {
60 GetTimer(Label::Timer).stop();
61 AddMeasurement(NumFunctions_, numFunctions);
62 AddMeasurement(NumInlineableFunctions_, numInlineableFunctions);
63 AddMeasurement(NumFunctionCalls_, numFunctionCalls);
64 AddMeasurement(NumInlineableCalls_, numInlineableCalls);
65 AddMeasurement(NumCallsInlined_, numCallsInlined);
66 }
67
68 static std::unique_ptr<Statistics>
69 create(const util::FilePath & sourceFile)
70 {
71 return std::make_unique<Statistics>(sourceFile);
72 }
73};
74
76{
77 // Functions that are possible to inline
78 // Just because a function is on this list, does not mean it should be inlined
80
81 // Functions that are not exported from the module, and only called once
83
84 // Used for statistics
85 size_t numFunctions = 0;
86 size_t numFunctionCalls = 0;
88 size_t numInlinedCalls = 0;
89};
90
91FunctionInlining::~FunctionInlining() noexcept = default;
92
94 : Transformation("FunctionInlining")
95{}
96
104static std::vector<rvsdg::Output *>
106{
107 constexpr bool enableCaching = false;
108 llvm::OutputTracer tracer(enableCaching);
109 // We avoid entering phi nodes, as we can not route from a sibling region
110 tracer.setEnterPhiNodes(false);
111
112 std::vector<rvsdg::Output *> deps;
113 for (auto & ctxvar : callee.GetContextVars())
114 {
115 auto & traced = tracer.trace(*ctxvar.input->origin());
116 auto & routed = rvsdg::RouteToRegion(traced, region);
117 deps.push_back(&routed);
118 }
119
120 return deps;
121}
122
160static void
162 rvsdg::SimpleNode & callEntryMerge,
163 rvsdg::SimpleNode & callExitSplit)
164{
165 const auto callEntryMergeOp =
167 const auto callExitSplitOp =
169 JLM_ASSERT(callEntryMergeOp);
170 JLM_ASSERT(callExitSplitOp);
171
172 // Use the output of the callEntryMerge to look for a lambdaEntrySplit
173 auto & callEntryMergeOutput = *callEntryMerge.output(0);
174 if (callEntryMergeOutput.nusers() != 1)
175 return;
176 auto & user = callEntryMergeOutput.SingleUser();
177 const auto [lambdaEntrySplit, lambdaEntrySplitOp] =
179 if (!lambdaEntrySplitOp)
180 return;
181
182 // Use the input of the callExitMerge to look for a lambdaExitMerge
183 auto & callExitSplitInput = *callExitSplit.input(0)->origin();
184 const auto [lambdaExitSplit, lambdaExitSplitOp] =
186 if (!lambdaExitSplitOp)
187 return;
188
189 // For each memory state output of the lambdaEntrySplit, move its users or create undef nodes
190 for (auto & output : lambdaEntrySplit->Outputs())
191 {
194 callEntryMerge,
195 memoryStateId);
196 if (mergeInput)
197 {
198 output.divert_users(mergeInput->origin());
199 }
200 else
201 {
202 // The call has no matching memory state going into it, so we create an undef node
203 const auto undef = UndefValueOperation::Create(*output.region(), output.Type());
204 output.divert_users(undef);
205 }
206 }
207
208 // For each memory state output of the callExitSplit, move its users
209 for (auto & output : callExitSplit.Outputs())
210 {
213 *lambdaExitSplit,
214 memoryStateId);
215 if (exitMergeInput)
216 {
217 output.divert_users(exitMergeInput->origin());
218 }
219 else
220 {
221 // the memory state id was never routed through the inside of the lambda, so route it around
223 callEntryMerge,
224 memoryStateId);
225 if (!entryMergeInput)
226 throw std::runtime_error("MemoryStateId in call exit split not found in call entry merge");
227 output.divert_users(entryMergeInput->origin());
228 }
229 }
230}
231
240static void
242 const rvsdg::LambdaNode & callee,
243 rvsdg::LambdaNode & caller,
245{
246 // All alloca operations in the callee must be on the top level, with constant count,
247 // otherwise the callee would not have qualified for being inlined
248
249 for (auto & node : callee.subregion()->Nodes())
250 {
251 if (!is<AllocaOperation>(&node))
252 continue;
253
254 // Find the same alloca in the caller
255 auto oldAllocaNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(smap.lookup(*node.output(0)));
256 JLM_ASSERT(oldAllocaNode);
257
258 auto countOrigin = AllocaOperation::getCountInput(*oldAllocaNode).origin();
259 countOrigin = &rvsdg::traceOutputIntraProcedurally(*countOrigin);
260 auto countNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*countOrigin);
261 if (!countNode || countNode->ninputs() != 0)
262 throw std::runtime_error("Alloca did not have a nullary count origin");
263
264 // Create copies of the count node and alloca node at the top level
265 const auto newCountNode = countNode->copy(caller.subregion(), {});
266 const auto newAllocaNode = oldAllocaNode->copy(caller.subregion(), { newCountNode->output(0) });
267
268 // Route the outputs of the new alloca to the region of the old alloca, and divert old users
269 for (size_t n = 0; n < newAllocaNode->noutputs(); n++)
270 {
271 auto & oldOutput = *oldAllocaNode->output(n);
272 auto & newOutput = *newAllocaNode->output(n);
273 auto & routed = rvsdg::RouteToRegion(newOutput, *oldAllocaNode->region());
274 oldOutput.divert_users(&routed);
275 }
276
277 // Remove the old alloca node, which is now dead
278 remove(oldAllocaNode);
279 }
280}
281
282void
284 rvsdg::SimpleNode & callNode,
285 rvsdg::LambdaNode & caller,
286 const rvsdg::LambdaNode & callee)
287{
288 JLM_ASSERT(is<CallOperation>(&callNode));
289
290 // Make note of the call's entry and exit memory state nodes, if they exist
291 auto callEntryMemoryStateMerge = CallOperation::tryGetMemoryStateEntryMerge(callNode);
292 auto callExitMemoryStateMerge = CallOperation::tryGetMemoryStateExitSplit(callNode);
293
294 // Set up substitution map for function arguments and context variables
296 auto & ioStateOperand = *CallOperation::GetIOStateInput(callNode).origin();
297 auto arguments = callee.GetFunctionArguments();
298 for (size_t n = 0; n < arguments.size(); n++)
299 {
300 auto callOperand = callNode.input(n + 1)->origin();
301 if (IsOrContains<PointerType>(*callOperand->Type()))
302 {
303 callOperand = IOBarrierOperation::createNode(*callOperand, ioStateOperand).output(0);
304 }
305
306 smap.insert(arguments[n], callOperand);
307 }
308
309 const auto routedDeps = routeContextVariablesToRegion(*callNode.region(), callee);
310 const auto contextVars = callee.GetContextVars();
311 JLM_ASSERT(contextVars.size() == routedDeps.size());
312 for (size_t n = 0; n < contextVars.size(); n++)
313 {
314 auto dep = routedDeps[n];
315 if (IsOrContains<PointerType>(*dep->Type()))
316 {
317 dep = IOBarrierOperation::createNode(*dep, ioStateOperand).output(0);
318 }
319 smap.insert(contextVars[n].inner, dep);
320 }
321
322 // Use the substitution map to copy the function body into the caller region
323 callee.subregion()->copy(callNode.region(), smap);
324
325 // Move all users of the call node's outputs to the callee's result origins
326 const auto calleeResults = callee.GetFunctionResults();
327 JLM_ASSERT(callNode.noutputs() == calleeResults.size());
328 for (size_t n = 0; n < callNode.noutputs(); n++)
329 {
330 const auto resultOrigin = calleeResults[n]->origin();
331 const auto newOrigin = &smap.lookup(*resultOrigin);
332 callNode.output(n)->divert_users(newOrigin);
333 }
334
335 // If the callee was copied into a structural node within the caller function,
336 // hoist any copied alloca nodes to the top level region of the caller function
337 if (callNode.region() != caller.subregion())
338 {
339 hoistInlinedAllocas(callee, caller, smap);
340 }
341
342 // The call node is now dead. Remove it
343 remove(&callNode);
344
345 // If the call had memory state merge and split nodes,
346 // try connecting memory state edges directly instead
347 if (callEntryMemoryStateMerge && callExitMemoryStateMerge)
348 {
349 tryRerouteMemoryStateMergeAndSplit(*callEntryMemoryStateMerge, *callExitMemoryStateMerge);
350 }
351}
352
353void
355{
356 auto & caller = rvsdg::getSurroundingLambdaNode(callNode);
357 inlineCall(callNode, caller, callee);
358}
359
360bool
361FunctionInlining::canBeInlined(rvsdg::Region & region, bool topLevelRegion)
362{
363 for (auto & node : region.Nodes())
364 {
365 if (const auto structural = dynamic_cast<rvsdg::StructuralNode *>(&node))
366 {
367 for (auto & subregion : structural->Subregions())
368 {
369 if (!canBeInlined(subregion, false))
370 return false;
371 }
372 }
373 else if (is<AllocaOperation>(&node))
374 {
375 // Having allocas that are not on the top level of the function disqualifies from inlining
376 if (!topLevelRegion)
377 return false;
378
379 // Having allocation sizes that are not compile time constants also disqualifies from inlining
380 auto countOutput = AllocaOperation::getCountInput(node).origin();
381 countOutput = &rvsdg::traceOutputIntraProcedurally(*countOutput);
382 auto countNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*countOutput);
383
384 // The count must come from a node, and it must be nullary
385 if (!countNode || countNode->ninputs() != 0)
386 return false;
387 }
388 else if (const auto [simple, callOp] =
390 simple && callOp)
391 {
392 const auto classification = CallOperation::ClassifyCall(*simple);
393 if (classification->isSetjmpCall())
394 {
395 // Calling setjmp weakens guarantees about local variables in the caller,
396 // but not local variables in the caller's caller. Inlining would mix them up.
397 return false;
398 }
399 if (classification->isVaStartCall())
400 {
401 // Calling va_start requires parameters to be passed in as expected by the ABI.
402 // This gets broken if we start inlining.
403 return false;
404 }
405 }
406 }
407
408 return true;
409}
410
411bool
413{
414 return canBeInlined(*callee.subregion(), true);
415}
416
417bool
419 [[maybe_unused]] rvsdg::SimpleNode & callNode,
420 [[maybe_unused]] rvsdg::LambdaNode & caller,
421 rvsdg::LambdaNode & callee)
422{
423 // For now the inlining heuristic is very simple: Inline functions that are called exactly once
424 return context_->functionsCalledOnce.Contains(&callee);
425}
426
427void
429 rvsdg::SimpleNode & callNode,
430 rvsdg::LambdaNode & callerLambda)
431{
432 context_->numFunctionCalls++;
433
434 auto classification = CallOperation::ClassifyCall(callNode);
435 if (!classification->IsDirectCall())
436 return;
437
438 auto & calleeOutput = classification->GetLambdaOutput();
439 auto callee = rvsdg::TryGetOwnerNode<rvsdg::LambdaNode>(calleeOutput);
440 JLM_ASSERT(callee != nullptr);
441
442 // We can not inline a function into itself
443 if (callee == &callerLambda)
444 return;
445
446 // We can only inline functions that have been marked as inlineable
447 if (!context_->inlineableFunctions.Contains(callee))
448 return;
449
450 // At this point we know that it is technically possible to do inlining
451 context_->numInlineableCalls++;
452 if (shouldInline(callNode, callerLambda, *callee))
453 {
454 context_->numInlinedCalls++;
455 inlineCall(callNode, callerLambda, *callee);
456 }
457}
458
459void
461{
462 for (auto node : rvsdg::TopDownTraverser(&region))
463 {
465 *node,
466 [&](rvsdg::StructuralNode & structural)
467 {
468 for (auto & subregion : structural.Subregions())
469 {
470 visitIntraProceduralRegion(subregion, lambda);
471 }
472 },
473 [&](rvsdg::SimpleNode & simple)
474 {
475 if (is<CallOperation>(&simple))
476 {
477 considerCallForInlining(simple, lambda);
478 }
479 });
480 }
481}
482
483void
485{
486 context_->numFunctions++;
487
488 // Visits the lambda's body and performs inlining of calls when determined to be beneficial
489 visitIntraProceduralRegion(*lambda.subregion(), lambda);
490
491 // After doing inlining inside lambda, we check if the function is eligible for being inlined
492 if (canBeInlined(lambda))
493 context_->inlineableFunctions.insert(&lambda);
494
495 // Check if the function is only called once, and not exported from the module.
496 // In which case inlining it is "free" in terms of total code size
497 auto callSummary = ComputeCallSummary(lambda);
498 if (callSummary.HasOnlyDirectCalls() && callSummary.NumDirectCalls() == 1)
499 context_->functionsCalledOnce.insert(&lambda);
500}
501
502void
504{
505 for (auto node : rvsdg::TopDownTraverser(&region))
506 {
508 *node,
509 [&](rvsdg::PhiNode & phi)
510 {
512 },
513 [&](rvsdg::LambdaNode & lambda)
514 {
515 visitLambda(lambda);
516 });
517 }
518}
519
520void
522{
523 auto statistics = Statistics::create(module.SourceFilePath().value_or(util::FilePath("")));
524
525 context_ = std::make_unique<Context>();
526 statistics->start();
528 statistics->stop(
529 context_->numFunctions,
530 context_->inlineableFunctions.Size(),
531 context_->numFunctionCalls,
532 context_->numInlineableCalls,
533 context_->numInlinedCalls);
534
535 statisticsCollector.CollectDemandedStatistics(std::move(statistics));
536
537 context_.reset();
538}
539
540}
static jlm::util::StatisticsCollector statisticsCollector
util::HashSet< rvsdg::Output * > arguments
static rvsdg::Input & getCountInput(rvsdg::Node &node)
Definition alloca.hpp:67
static rvsdg::Input * tryMapMemoryNodeIdToInput(const rvsdg::SimpleNode &node, MemoryNodeId memoryNodeId)
static MemoryNodeId mapOutputToMemoryNodeId(const rvsdg::Output &output)
static std::unique_ptr< CallTypeClassifier > ClassifyCall(const rvsdg::SimpleNode &callNode)
Classifies a call node.
Definition call.cpp:49
static rvsdg::SimpleNode * tryGetMemoryStateEntryMerge(const rvsdg::Node &callNode) noexcept
Definition call.hpp:388
static rvsdg::SimpleNode * tryGetMemoryStateExitSplit(const rvsdg::Node &callNode) noexcept
Definition call.hpp:406
static rvsdg::Input & GetIOStateInput(const rvsdg::Node &node) noexcept
Definition call.hpp:333
static std::unique_ptr< Statistics > create(const util::FilePath &sourceFile)
Definition inlining.cpp:69
static constexpr const char * NumCallsInlined_
Definition inlining.cpp:37
void stop(size_t numFunctions, size_t numInlineableFunctions, size_t numFunctionCalls, size_t numInlineableCalls, size_t numCallsInlined)
Definition inlining.cpp:53
static constexpr const char * NumFunctions_
Definition inlining.cpp:29
static constexpr const char * NumInlineableFunctions_
Definition inlining.cpp:31
Statistics(const util::FilePath &sourceFile)
Definition inlining.cpp:42
static constexpr const char * NumInlineableCalls_
Definition inlining.cpp:35
static constexpr const char * NumFunctionCalls_
Definition inlining.cpp:33
Performs function inlining on functions that are determined to be good candidates,...
Definition inlining.hpp:25
bool shouldInline(rvsdg::SimpleNode &callNode, rvsdg::LambdaNode &caller, rvsdg::LambdaNode &callee)
Definition inlining.cpp:418
void visitInterProceduralRegion(rvsdg::Region &region)
Definition inlining.cpp:503
void visitLambda(rvsdg::LambdaNode &lambda)
Definition inlining.cpp:484
void considerCallForInlining(rvsdg::SimpleNode &callNode, rvsdg::LambdaNode &callerLambda)
Definition inlining.cpp:428
void Run(rvsdg::RvsdgModule &module, util::StatisticsCollector &statisticsCollector) override
Perform RVSDG transformation.
Definition inlining.cpp:521
static bool canBeInlined(rvsdg::Region &region, bool topLevelRegion)
Definition inlining.cpp:361
void visitIntraProceduralRegion(rvsdg::Region &region, rvsdg::LambdaNode &lambda)
Definition inlining.cpp:460
static void inlineCall(rvsdg::SimpleNode &callNode, rvsdg::LambdaNode &caller, const rvsdg::LambdaNode &callee)
Definition inlining.cpp:283
~FunctionInlining() noexcept override
std::unique_ptr< Context > context_
Definition inlining.hpp:129
static rvsdg::SimpleNode & createNode(rvsdg::Output &value, rvsdg::Output &ioState)
Definition IOBarrier.hpp:87
static MemoryNodeId mapOutputToMemoryNodeId(const rvsdg::Output &output)
static rvsdg::Input * tryMapMemoryNodeIdToInput(const rvsdg::SimpleNode &node, MemoryNodeId memoryNodeId)
static jlm::rvsdg::Output * Create(rvsdg::Region &region, std::shared_ptr< const jlm::rvsdg::Type > type)
Region & GetRootRegion() const noexcept
Definition graph.hpp:99
Output * origin() const noexcept
Definition node.hpp:58
std::vector< rvsdg::Output * > GetFunctionArguments() const
Definition lambda.cpp:58
std::vector< rvsdg::Input * > GetFunctionResults() const
Definition lambda.cpp:70
rvsdg::Region * subregion() const noexcept
Definition lambda.hpp:138
std::vector< ContextVar > GetContextVars() const noexcept
Gets all bound context variables.
Definition lambda.cpp:120
OutputIteratorRange Outputs() noexcept
Definition node.hpp:657
rvsdg::Region * region() const noexcept
Definition node.hpp:761
size_t noutputs() const noexcept
Definition node.hpp:644
void setEnterPhiNodes(bool value) noexcept
Definition Trace.hpp:83
Output & trace(Output &output)
Definition Trace.cpp:22
rvsdg::Input & SingleUser() noexcept
Definition node.hpp:347
void divert_users(jlm::rvsdg::Output *new_origin)
Definition node.hpp:301
A phi node represents the fixpoint of mutually recursive definitions.
Definition Phi.hpp:46
rvsdg::Region * subregion() const noexcept
Definition Phi.hpp:320
Represent acyclic RVSDG subgraphs.
Definition region.hpp:213
void copy(Region *target, SubstitutionMap &smap) const
Copy a region with substitutions.
Definition region.cpp:317
NodeRange Nodes() noexcept
Definition region.hpp:375
const std::optional< util::FilePath > & SourceFilePath() const noexcept
Graph & Rvsdg() noexcept
NodeInput * input(size_t index) const noexcept
NodeOutput * output(size_t index) const noexcept
SubregionIteratorRange Subregions()
void insert(const Output *original, Output *substitute)
Output & lookup(const Output &original) const
void CollectDemandedStatistics(std::unique_ptr< Statistics > statistics)
Statistics Interface.
util::Timer & GetTimer(const std::string &name)
util::Timer & AddTimer(std::string name)
void AddMeasurement(std::string name, T value)
void start() noexcept
Definition time.hpp:54
void stop() noexcept
Definition time.hpp:67
#define JLM_ASSERT(x)
Definition common.hpp:16
Global memory state passed between functions.
static void tryRerouteMemoryStateMergeAndSplit(rvsdg::SimpleNode &callEntryMerge, rvsdg::SimpleNode &callExitSplit)
Definition inlining.cpp:161
static void hoistInlinedAllocas(const rvsdg::LambdaNode &callee, rvsdg::LambdaNode &caller, rvsdg::SubstitutionMap &smap)
Definition inlining.cpp:241
static std::vector< rvsdg::Output * > routeContextVariablesToRegion(rvsdg::Region &region, const rvsdg::LambdaNode &callee)
Definition inlining.cpp:105
CallSummary ComputeCallSummary(const rvsdg::LambdaNode &lambdaNode)
void MatchType(T &obj, const Fns &... fns)
Pattern match over subclass type of given object.
Output & RouteToRegion(Output &output, Region &region)
Definition node.cpp:381
NodeType * TryGetOwnerNode(const rvsdg::Input &input) noexcept
Checks if this is an input to a node of specified type.
Definition node.hpp:872
Output & traceOutputIntraProcedurally(Output &output)
Definition Trace.cpp:283
rvsdg::LambdaNode & getSurroundingLambdaNode(rvsdg::Node &node)
Definition lambda.cpp:273
detail::TopDownTraverserGeneric< false > TopDownTraverser
Traverser for visiting every node in a region in a top down order.
util::HashSet< const rvsdg::LambdaNode * > inlineableFunctions
Definition inlining.cpp:79
util::HashSet< const rvsdg::LambdaNode * > functionsCalledOnce
Definition inlining.cpp:82