Limit most inlining to small, frequently called functions to facilitate debugging and binary upgradability, minimize potential code bloat, and maximize the chances of greater program speed.
Performance benifits
Inline functions have some wonderful advantages:
- They act like functions.
- Yet we call them without having to incur the overhead of a function call.
- Compilers might perfom context-specific optimizations (such as stretches of the code) on the body of the inline function thanks to the lack “outlined” function call
- If an inline function body is very short, the code generated for the function body may be smaller than the code generated for a function call, leading to smaller object code and a higher instruction cache hit rate.
However, on the other hand, it is also likely to increase the size of our object code. Overzealous inlining can give rise to programs too big for the available space on machines with limited memory. Even with virtual memory, inline-induced code bloat can lead to additional paging, a reduced instruction cache hit rate, and the performance penalties that accompany these things.
Details on inline functions
There are two ways to give such a request: implicitly or explicitly:
-
Implicitly inline request
1 2 3 4 5 6 7 8
class Person { public: ... int age() const { return theAge; } // an implicit inline request ... private: int theAge; };
Apart from the member functions, we can also define friend functions inside classes (item 46), resulting to implicitly inline declaration.
-
Explicitly inline request (and its relationship with template)
We can explicitly daclare an inline function with the
inline
keyword, such as how the standardmax
template from<algorithm>
is implemeted:1 2 3
template<typename T> // an explicit inline inline const T& std::max(const T& a, const T& b) // request:: std::max is preceded by "inline" { return a < b ? b : a;}
It’s true that both template and inline functions are typically defined in header files, but this doesn’t mean that function templates must be inline. In fact, template instantiation is independent of inlining:
- inline functions must typically be in header files, because most build environments do inlining during compilation. In order to replace a function call with the body of the called function, compilers must know waht the function looks like 1.
- templates are typically in header file, because compilers need to know what a template looks like in order to instantiate it when it’s used 2.
Thus, when we’re writing a template:
- if we belive that all the functions instantiated from the template should be inlined, declare the template
inline
just like what’s done with thestd::max
implamentation above; - if we have no reason to want inlined after considering its cost and potential code bloat (which is particularly important for template authors - see item 44), avoid declaring the template inline (either explicitly or implicitly)
Causes of un-inlined inline function
Bear in mind that inline
is a request to compilers, rather than a command, so compilers may ignore it. Whether a given inline function is actually inlined depends on the build environment we’re using - primarily on the compiler, and most compilers have a diagnostic level that will result in a warning (item 53) if they fail to inline a function we’ve asked them to. When they refuse to inline a function, the typical reasons may be:
- compilers deem the function too complicated (e.g., those containing loogs or are recursive)
- the function is virtual (
virtual
means runtime, whileinline
usually happens before execution) - when the program will take the address of an inline function:
|
|
Inline constructors and destructors?
Note that even if we never use function pointers, sometimes compilers will generate those pointers: compilers may generate out-of-line copies of constructors and destructors so that they can get pointers to those functions for use during construction and destruction of objects in arrays. Let alone that constructors and destructors are often worse candidates of inlining than a casual examination would indicate:
|
|
|
|
The constructor of Derived
seems empty, so it may seem a good candidate for inlining. But looks can be deceiving:
C++ makes various guarantees about object construction and corresponding destruction. To name a few: when we create an object, each base class of and each data member in that object is automatically constructed; if an exception is thrown during construction, any parts of the object that have already been fully constructed are automatically destroyed. Similar but reverse process will occur when an object is destroyed.
Therefore, in the example above, extra code will be generated by compilers and added into the empty Derived
constructor in order to offer those guarantees, ending up with some code equivalent to the following conceptual implementation (the real one will certainly be more sophisticated):
|
|
The same reasoning applies to the Base
constructor, so if Base
’s constructor is inlined, and string
constructor also happens to be inlined, the Derived
constructor will gain five copies of string
’s constructor, one for each of the five strings in a Derived
object - that is a big code bloat. Similar considerations apply to Derived
’s destructor.
Other costs of inlining
Apart from the cost of possible code bloat, there are other impact of declaring functions inline
:
- on binary upgradability: for library designers, it’s impossible to provide binary upgrades to the client visible inline functions in a library, after clients of the library compile the body of
inline
functionf
into their application. Once the imlementation off
changes, all clients who’ve usedf
must recompile. - on debugging: most debuggers have trouble with inline functions: after all, it is hard to set a breakpoint in a function that isn’t there3.
To inline or not to inline?
In summary, the logical strategy of determining which functions should be declared inline and which should not:
- initially don’t inline anything
- detect those functions that must be inline (item 46) or are truly trivial (such as
Person::age
in this item), inline them cautiously as a hand-applied optimization - as the empirically determined rule of 80-20 suggests: identify the 20% of your code that can increase your program’s overall performance, and try to tweak those critical functions with
inline
or other techniques
-
There are exceptions: some build environments can inline during linking, and a few can actually inline at runtime (e.g., managed environments based on the .NET Common Language Infrastructure -
CLI
). Of course, such exceptions are not the rule, and inlining in most C++ programs is a compile-time activity. ↩︎ -
Again, some build environments perform template instantiation during linking. However, compile-time instantiation is more common. ↩︎
-
Some build environments manage to support debugging of inlined functions, but many environments simply disable inlining for debug builds. ↩︎