In computing, inline expansion, or inlining, is a manual or compiler optimization that replaces a function call site with the body of the called function. Inline expansion is similar to macro expansion, but occurs during compiling, without changing the source code (the text), while macro expansion occurs before compiling, and results in different text that is then processed by the compiler.
Inlining is an important optimization, but has complex effects on performance. As a rule of thumb, some inlining will improve speed at very minor cost of space, but excess inlining will hurt speed, due to inlined code consuming too much of the instruction cache, and also cost significant space. A survey of the modest academic literature on inlining from the 1980s and 1990s is given in Peyton Jones & Marlow 1999.
Overview Inline expansion is similar to macro expansion as the compiler places a new copy of the function in each place it is called. Inlined functions run a little faster than the normal functions as function-calling-overheads are saved, however, there is a memory penalty. If a function is inlined 10 times, there will be 10 copies of the function inserted into the code. Hence inlining is best for small functions that are called often. In C++ the member functions of a class, if defined within the class definition, are inlined by default (no need to use the inline reserved word (keyword)); otherwise, the keyword is needed. The compiler may ignore the programmer’s attempt to inline a function, mainly if it is particularly large.
Inline expansion is used to eliminate the time overhead (excess time) when a function is called. It is typically used for functions that execute frequently. It also has a space benefit for very small functions, and is an enabling transformation for other optimizations.
Without inline functions, the compiler decides which functions to inline. The programmer has little or no control over which functions are inlined and which are not. Giving this degree of control to the programmer allows for the use of application-specific knowledge in choosing which functions to inline.
Ordinarily, when a function is invoked, control is transferred to its definition by a branch or call instruction. With inlining, control drops through directly to the code for the function, without a branch or call instruction.
Compilers usually implement statements with inlining. Loop conditions and loop bodies need lazy evaluation. This property is fulfilled when the code to compute loop conditions and loop bodies is inlined. Performance considerations are another reason to inline statements.
In the context of functional programming languages, inline expansion is usually followed by the beta-reduction transformation.
A programmer might inline a function manually through copy-and-paste programming, as a one-time operation on the source code. However, other methods of controlling inlining (see below) are preferable, because they do not precipitate bugs arising when the programmer overlooks a (possibly modified) duplicated version of the original function body, while fixing a bug in the inlined function.
Effect on performance The direct effect of this optimization is to improve time performance (by eliminating call overhead), at the cost of worsening space usage (due to duplicating the function body). The code expansion due to duplicating the function body dominates, except for simple cases, and thus the direct effect of inline expansion is to improve time at the cost of space.
However, the main benefit of inline expansion is to allow further optimizations and improved scheduling, due to increasing the size of the function body, as better optimization is possible on larger functions. The ultimate impact of inline expansion on speed is complex, due to multiple effects on performance of the memory system (mainly instruction cache), which dominates performance on modern processors: depending on the specific program and cache, inlining particular functions can increase or decrease performance.
The impact of inlining varies by programming language and program, due to different degrees of abstraction. In lower-level imperative languages such as C and Fortran it is typically a 10–20% speed boost, with minor impact on code size, while in more abstract languages it can be significantly more important, due to the number of layers inlining removes, with an extreme example being Self, where one compiler saw improvement factors of 4 to 55 by inlining.
The direct benefits of eliminating a function call are: It eliminates instructions needed for a function call, both in the calling function and in the callee: placing arguments on a stack or in registers, the function call itself, the function prologue, then at return the function epilogue, the return statement, and then getting the return value back, and removing arguments from stacks and restoring registers (if needed). Due to not needing registers to pass arguments, it reduces register spilling. It eliminates having to pass references and then dereference them, when using call by reference (or call by address, or call by sharing).
The main benefit of inlining, however, is the further optimizations it allows. Optimizations that cross function boundaries can be done without requiring interprocedural optimization (IPO): once inlining has been performed, added intraprocedural optimizations ("global optimizations") become possible on the enlarged function body. For example: A constant passed as an argument can often be propagated to all instances of the matching parameter, or part of the function may be "hoisted out" of a loop (via loop-invariant code motion). Register allocation can be done across the larger function body. High-level optimizations, such as escape analysis and tail duplication, can be performed on a larger scope and be more effective, more so if the compiler implementing those optimizations relies on mainly intra-procedural analysis. These can be done without inlining, but require a significantly more complex compiler and linker (in case caller and callee are in separate compiling units).
Conversely, in some cases a language specification may allow a program to make added assumptions about arguments to procedures that it can no longer make after the procedure is inlined, preventing some optimizations. Smarter compilers (such as Glasgow Haskell Compiler (GHC)) will track this, but naive inlining loses this information.
A further benefit of inlining for the memory system is: Eliminating branches and keeping code that is executed close together in memory improves instruction cache performance by improving locality of reference (spatial locality and sequentiality of instructions). This is smaller than optimizations that specifically target sequentiality, but is significant.
The direct cost of inlining is increased code size, due to duplicating the function body at each call site. However, it does not always do so, namely in case of very short functions, where the function body is smaller than the size of a function call (at the caller, including argument and return value handling), such as trivial accessor methods or mutator methods (getters and setters); or for a function that is only used in one place, in which case it is not duplicated. Thus inlining may be minimized or eliminated if optimizing for code size, as is often the case in embedded systems.
Inlining also imposes a cost on performance, due to the code expansion (due to duplication) hurting instruction cache performance. This is most significant if, before expansion, the working set of the program (or a hot section of code) fit in one level of the memory hierarchy (e.g., L1 cache), but after expansion it no longer fits, resulting in frequent cache misses at that level. Due to the significant difference in performance at different levels of the hierarchy, this hurts performance considerably. At the highest level this can result in increased page faults, catastrophic performance degradation due to thrashing, or the program failing to run at all. This last is rare in common desktop and server applications, where code size is small relative to available memory, but can be an issue for resource-constrained environments such as embedded systems. One way to mitigate this problem is to split functions into a smaller hot inline path (fast path), and a larger cold non-inline path (slow path).
Inlining hurting performance is a problem for mainly large functions that are used in many places, but the break-even point beyond which inlining reduces performance is difficult to determine and depends in general on precise load, so it can be subject to manual optimization or profile-guided optimization.See for example the Adaptive Optimization System in the Jikes RVM for Java. This is a similar issue to other code expanding optimizations such as loop unrolling, which also reduces number of instructions processed, but can decrease performance due to poorer cache performance.
The precise effect of inlining on cache performance is complex. For small cache sizes (much smaller than the working set before expansion), the increased sequentiality dominates, and inlining improves cache performance. For cache sizes close to the working set, where inlining expands the working set so it no longer fits in cache, this dominates and cache performance decreases. For cache sizes larger than the working set, inlining has negligible impact on cache performance. Further, changes in cache design, such as load forwarding, can offset the increase in cache misses.
Compiler support Compilers use a variety of mechanisms to decide which function calls should be inlined; these can include manual hints from programmers for specific functions, together with overall control via command-line options. Inlining is done automatically by many compilers in many languages, based on judgment of whether inlining is beneficial, while in other cases it can be manually specified via compiler directives, typically using a keyword or compiler directive called inline. Typically this only hints that inlining is desired, rather than requiring inlining, with the force of the hint varying by language and compiler.
Typically, compiler developers keep the above performance issues in mind, and incorporate heuristics into their compilers that choose which functions to inline so as to improve performance, rather than worsening it, in most cases.
Implementation Once the compiler has decided to inline a particular function, performing the inlining operation itself is usually simple. Depending on whether a compiler inlines functions across code in different languages, the compiler can inline on either a high-level intermediate representation (like abstract syntax trees) or a low-level intermediate representation. In either case, the compiler simply computes the arguments, stores them in variables corresponding to the function's arguments, and then inserts the body of the function at the call site.
Linkers can also do function inlining. When a linker inlines functions, it may inline functions whose source is not available, such as library functions (see link-time optimization). A runtime system can inline a function also. Runtime inlining can use dynamic profiling information to make better decisions about which functions to inline, as in the Java HotSpot compiler. Description of the inliner used in the Graal JIT compiler for Java
Here is a simple example of inline expansion performed "by hand" at the source level in the C language: int pred(int x) { if (x == 0) { return 0; } else { return x - 1;
Before inlining: int func(int y) { return pred(y) + pred(0) + pred(y + 1); }
After inlining: int func(int y) { int tmp;
// (1) if (y == 0) { tmp = 0; } else { tmp = y - 1;
// (2) if (0 == 0) { tmp += 0; } else { tmp += 0 - 1;
// (3) if (y + 1 == 0) { tmp += 0; } else { tmp += (y + 1) - 1;
return tmp; }