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 llvm::OutputTracer tracer;
108 // We avoid entering phi nodes, as we can not route from a sibling region
109 tracer.setEnterPhiNodes(false);
110
111 std::vector<rvsdg::Output *> deps;
112 for (auto & ctxvar : callee.GetContextVars())
113 {
114 auto & traced = tracer.trace(*ctxvar.input->origin());
115 auto & routed = rvsdg::RouteToRegion(traced, region);
116 deps.push_back(&routed);
117 }
118
119 return deps;
120}
121
159static void
161 rvsdg::SimpleNode & callEntryMerge,
162 rvsdg::SimpleNode & callExitSplit)
163{
164 const auto callEntryMergeOp =
166 const auto callExitSplitOp =
168 JLM_ASSERT(callEntryMergeOp);
169 JLM_ASSERT(callExitSplitOp);
170
171 // Use the output of the callEntryMerge to look for a lambdaEntrySplit
172 auto & callEntryMergeOutput = *callEntryMerge.output(0);
173 if (callEntryMergeOutput.nusers() != 1)
174 return;
175 auto & user = callEntryMergeOutput.SingleUser();
176 const auto [lambdaEntrySplit, lambdaEntrySplitOp] =
178 if (!lambdaEntrySplitOp)
179 return;
180
181 // Use the input of the callExitMerge to look for a lambdaExitMerge
182 auto & callExitSplitInput = *callExitSplit.input(0)->origin();
183 const auto [lambdaExitSplit, lambdaExitSplitOp] =
185 if (!lambdaExitSplitOp)
186 return;
187
188 // For each memory state output of the lambdaEntrySplit, move its users or create undef nodes
189 for (auto & output : lambdaEntrySplit->Outputs())
190 {
193 callEntryMerge,
194 memoryStateId);
195 if (mergeInput)
196 {
197 output.divert_users(mergeInput->origin());
198 }
199 else
200 {
201 // The call has no matching memory state going into it, so we create an undef node
202 const auto undef = UndefValueOperation::Create(*output.region(), output.Type());
203 output.divert_users(undef);
204 }
205 }
206
207 // For each memory state output of the callExitSplit, move its users
208 for (auto & output : callExitSplit.Outputs())
209 {
212 *lambdaExitSplit,
213 memoryStateId);
214 if (exitMergeInput)
215 {
216 output.divert_users(exitMergeInput->origin());
217 }
218 else
219 {
220 // the memory state id was never routed through the inside of the lambda, so route it around
222 callEntryMerge,
223 memoryStateId);
224 if (!entryMergeInput)
225 throw std::runtime_error("MemoryStateId in call exit split not found in call entry merge");
226 output.divert_users(entryMergeInput->origin());
227 }
228 }
229}
230
239static void
241 const rvsdg::LambdaNode & callee,
242 rvsdg::LambdaNode & caller,
244{
245 // All alloca operations in the callee must be on the top level, with constant count,
246 // otherwise the callee would not have qualified for being inlined
247
248 for (auto & node : callee.subregion()->Nodes())
249 {
250 if (!is<AllocaOperation>(&node))
251 continue;
252
253 // Find the same alloca in the caller
254 auto oldAllocaNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(smap.lookup(*node.output(0)));
255 JLM_ASSERT(oldAllocaNode);
256
257 auto countOrigin = AllocaOperation::getCountInput(*oldAllocaNode).origin();
258 countOrigin = &rvsdg::traceOutputIntraProcedurally(*countOrigin, false);
259 auto countNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*countOrigin);
260 if (!countNode || countNode->ninputs() != 0)
261 throw std::runtime_error("Alloca did not have a nullary count origin");
262
263 // Create copies of the count node and alloca node at the top level
264 const auto newCountNode = countNode->copy(caller.subregion(), {});
265 const auto newAllocaNode = oldAllocaNode->copy(caller.subregion(), { newCountNode->output(0) });
266
267 // Route the outputs of the new alloca to the region of the old alloca, and divert old users
268 for (size_t n = 0; n < newAllocaNode->noutputs(); n++)
269 {
270 auto & oldOutput = *oldAllocaNode->output(n);
271 auto & newOutput = *newAllocaNode->output(n);
272 auto & routed = rvsdg::RouteToRegion(newOutput, *oldAllocaNode->region());
273 oldOutput.divert_users(&routed);
274 }
275
276 // Remove the old alloca node, which is now dead
277 remove(oldAllocaNode);
278 }
279}
280
281void
283 rvsdg::SimpleNode & callNode,
284 rvsdg::LambdaNode & caller,
285 const rvsdg::LambdaNode & callee)
286{
287 JLM_ASSERT(is<CallOperation>(&callNode));
288
289 // Make note of the call's entry and exit memory state nodes, if they exist
290 auto callEntryMemoryStateMerge = CallOperation::tryGetMemoryStateEntryMerge(callNode);
291 auto callExitMemoryStateMerge = CallOperation::tryGetMemoryStateExitSplit(callNode);
292
293 // Set up substitution map for function arguments and context variables
295 auto & ioStateOperand = *CallOperation::GetIOStateInput(callNode).origin();
296 auto arguments = callee.GetFunctionArguments();
297 for (size_t n = 0; n < arguments.size(); n++)
298 {
299 auto callOperand = callNode.input(n + 1)->origin();
300 if (IsOrContains<PointerType>(*callOperand->Type()))
301 {
302 callOperand =
303 MemoryHoistBarrierOperation::createNode(*callOperand, ioStateOperand, 0).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 = MemoryHoistBarrierOperation::createNode(*dep, ioStateOperand, 0).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, false);
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}
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:406
static rvsdg::SimpleNode * tryGetMemoryStateExitSplit(const rvsdg::Node &callNode) noexcept
Definition call.hpp:424
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:282
~FunctionInlining() noexcept override
std::unique_ptr< Context > context_
Definition inlining.hpp:129
static MemoryNodeId mapOutputToMemoryNodeId(const rvsdg::Output &output)
static rvsdg::Input * tryMapMemoryNodeIdToInput(const rvsdg::SimpleNode &node, MemoryNodeId memoryNodeId)
static rvsdg::SimpleNode & createNode(rvsdg::Output &address, rvsdg::Output &ioState, const std::size_t dereferenceableSize)
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:153
Output & trace(Output &output)
Definition Trace.cpp:21
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:160
static util::StatisticsCollector statisticsCollector
static void hoistInlinedAllocas(const rvsdg::LambdaNode &callee, rvsdg::LambdaNode &caller, rvsdg::SubstitutionMap &smap)
Definition inlining.cpp:240
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
Output & traceOutputIntraProcedurally(Output &output, bool mayEnterSubregions)
Definition Trace.cpp:442
NodeType * TryGetOwnerNode(const rvsdg::Input &input) noexcept
Checks if this is an input to a node of specified type.
Definition node.hpp:872
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