• Tidak ada hasil yang ditemukan

Understand the ins and outs of inlining

Dalam dokumen Book Praise for Effective C++, Third Edition (Halaman 155-161)

ptg7544714 guarantee only if calls to legacy code leave you no choice. Document

your decisions, both for clients of your functions and for future main- tainers. A function’s exception-safety guarantee is a visible part of its interface, so you should choose it as deliberately as you choose all other aspects of a function’s interface.

Forty years ago, goto-laden code was considered perfectly good prac- tice. Now we strive to write structured control flows. Twenty years ago, globally accessible data was considered perfectly good practice. Now we strive to encapsulate data. Ten years ago, writing functions with- out thinking about the impact of exceptions was considered perfectly good practice. Now we strive to write exception-safe code.

Time goes on. We live. We learn.

Things to Remember

Exception-safe functions leak no resources and allow no data struc- tures to become corrupted, even when exceptions are thrown. Such functions offer the basic, strong, or nothrow guarantees.

The strong guarantee can often be implemented via copy-and-swap, but the strong guarantee is not practical for all functions.

A function can usually offer a guarantee no stronger than the weak- est guarantee of the functions it calls.

ptg7544714 space. 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.

On the other hand, if an inline function body is very short, the code generated for the function body may be smaller than the code gener- ated for a function call. If that is the case, inlining the function may actually lead to smaller object code and a higher instruction cache hit rate!

Bear in mind that inline is a request to compilers, not a command. The request can be given implicitly or explicitly. The implicit way is to define a function inside a class definition:

class Person { public:

...

int age() const { return theAge; } // an implicit inline request: age is

... // defined in a class definition

private:

int theAge;

};

Such functions are usually member functions, but Item 46 explains that friend functions can also be defined inside classes. When they are, they’re also implicitly declared inline.

The explicit way to declare an inline function is to precede its defini- tion with the inline keyword. For example, this is how the standard max template (from <algorithm>) is often implemented:

template<typename T> // an explicit inline inline const T& std::max(const T& a, const T& b) // request: std::max is { return a < b ? b : a; } // preceded by “inline”

The fact that max is a template brings up the observation that both inline functions and templates are typically defined in header files.

This leads some programmers to conclude that function templates must be inline. This conclusion is both invalid and potentially harm- ful, so it’s worth looking into it a bit.

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 what the function looks like. (Some build environments can inline during linking, and a few — e.g., managed environments based on the .NET Common Language Infrastructure (CLI) — can actually inline at runtime. Such environments are the exception, however, not the rule. Inlining in most C++ programs is a compile-time activity.)

ptg7544714 Templates are typically in header files, because compilers need to

know what a template looks like in order to instantiate it when it’s used. (Again, this is not universal. Some build environments perform template instantiation during linking. However, compile-time instanti- ation is more common.)

Template instantiation is independent of inlining. If you’re writing a template and you believe that all the functions instantiated from the template should be inlined, declare the template inline; that’s what’s done with the std::max implementation above. But if you’re writing a template for functions that you have no reason to want inlined, avoid declaring the template inline (either explicitly or implicitly). Inlining has costs, and you don’t want to incur them without forethought.

We’ve already mentioned how inlining can cause code bloat (a particu- larly important consideration for template authors — see Item 44), but there are other costs, too, which we’ll discuss in a moment.

Before we do that, let’s finish the observation that inline is a request that compilers may ignore. Most compilers refuse to inline functions they deem too complicated (e.g., those that contain loops or are recur- sive), and all but the most trivial calls to virtual functions defy inlin- ing. This latter observation shouldn’t be a surprise. virtual means “wait until runtime to figure out which function to call,” and inline means

“before execution, replace the call site with the called function.” If compilers don’t know which function will be called, you can hardly blame them for refusing to inline the function’s body.

It all adds up to this: whether a given inline function is actually inlined depends on the build environment you’re using — primarily on the compiler. Fortunately, most compilers have a diagnostic level that will result in a warning (see Item 53) if they fail to inline a function you’ve asked them to.

Sometimes compilers generate a function body for an inline function even when they are perfectly willing to inline the function. For exam- ple, if your program takes the address of an inline function, compilers must typically generate an outlined function body for it. How can they come up with a pointer to a function that doesn’t exist? Coupled with the fact that compilers typically don’t perform inlining across calls through function pointers, this means that calls to an inline function may or may not be inlined, depending on how the calls are made:

inline void f() {...} // assume compilers are willing to inline calls to f void (*pf)() = f; // pf points to f

...

f(); // this call will be inlined, because it’s a “normal” call

ptg7544714 pf(); // this call probably won’t be, because it’s through

// a function pointer

The specter of un-inlined inline functions can haunt you even if you never use function pointers, because programmers aren’t necessarily the only ones asking for pointers to functions. Sometimes compilers 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.

In fact, constructors and destructors are often worse candidates for inlining than a casual examination would indicate. For example, con- sider the constructor for class Derived below:

class Base { public:

...

private:

std::string bm1, bm2; // base members 1 and 2 };

class Derived: public Base { public:

Derived() {} // Derived’s ctor is empty — or is it?

...

private:

std::string dm1, dm2, dm3; // derived members 1–3 };

This constructor looks like an excellent candidate for inlining, since it contains no code. But looks can be deceiving.

C++ makes various guarantees about things that happen when objects are created and destroyed. When you use new, for example, your dynamically created objects are automatically initialized by their constructors, and when you use delete, the corresponding destructors are invoked. When you create an object, each base class of and each data member in that object is automatically constructed, and the reverse process regarding destruction automatically occurs when an object is destroyed. If an exception is thrown during construction of an object, any parts of the object that have already been fully con- structed are automatically destroyed. In all these scenarios, C++ says what must happen, but it doesn’t say how. That’s up to compiler implementers, but it should be clear that those things don’t happen by themselves. There has to be some code in your program to make those things happen, and that code — the code written by compilers and inserted into your program during compilation — has to go some- where. Sometimes it ends up in constructors and destructors, so we

ptg7544714 can imagine implementations generating code equivalent to the follow-

ing for the allegedly empty Derived constructor above:

Derived::Derived() // conceptual implementation of

{ // “empty” Derived ctor

Base::Base(); // initialize Base part

try { dm1.std::string::string(); } // try to construct dm1

catch (...) { // if it throws,

Base::~Base(); // destroy base class part and

throw; // propagate the exception

}

try { dm2.std::string::string(); } // try to construct dm2

catch(...) { // if it throws,

dm1.std::string::~string(); // destroy dm1,

Base::~Base(); // destroy base class part, and

throw; // propagate the exception

}

try { dm3.std::string::string(); } // construct dm3

catch(...) { // if it throws,

dm2.std::string::~string(); // destroy dm2, dm1.std::string::~string(); // destroy dm1,

Base::~Base(); // destroy base class part, and

throw; // propagate the exception

} }

This code is unrepresentative of what real compilers emit, because real compilers deal with exceptions in more sophisticated ways. Still, this accurately reflects the behavior that Derived’s “empty” constructor must offer. No matter how sophisticated a compiler’s exception imple- mentation, Derived’s constructor must at least call constructors for its data members and base class, and those calls (which might them- selves be inlined) could affect its attractiveness for inlining.

The same reasoning applies to the Base constructor, so if it’s inlined, all the code inserted into it is also inserted into the Derived construc- tor (via the Derived constructor’s call to the Base constructor). And if the string constructor also happens to be inlined, the Derived construc- tor will gain five copies of that function’s code, one for each of the five strings in a Derived object (the two it inherits plus the three it declares itself). Perhaps now it’s clear why it’s not a no-brain decision whether to inline Derived’s constructor. Similar considerations apply to Derived’s destructor, which, one way or another, must see to it that all the objects initialized by Derived’s constructor are properly destroyed.

Library designers must evaluate the impact of declaring functions inline, because it’s impossible to provide binary upgrades to the client-

ptg7544714 visible inline functions in a library. In other words, if f is an inline

function in a library, clients of the library compile the body of f into their applications. If a library implementer later decides to change f, all clients who’ve used f must recompile. This is often undesirable. On the other hand, if f is a non-inline function, a modification to f requires only that clients relink. This is a substantially less onerous burden than recompiling and, if the library containing the function is dynamically linked, one that may be absorbed in a way that’s com- pletely transparent to clients.

For purposes of program development, it is important to keep all these considerations in mind, but from a practical point of view during cod- ing, one fact dominates all others: most debuggers have trouble with inline functions. This should be no great revelation. How do you set a breakpoint in a function that isn’t there? Although some build envi- ronments manage to support debugging of inlined functions, many environments simply disable inlining for debug builds.

This leads to a logical strategy for determining which functions should be declared inline and which should not. Initially, don’t inline any- thing, or at least limit your inlining to those functions that must be inline (see Item 46) or are truly trivial (such as Person::age on page 135). By employing inlines cautiously, you facilitate your use of a debugger, but you also put inlining in its proper place: as a hand- applied optimization. Don’t forget the empirically determined rule of 80-20, which states that a typical program spends 80% of its time executing only 20% of its code. It’s an important rule, because it reminds you that your goal as a software developer is to identify the 20% of your code that can increase your program’s overall perfor- mance. You can inline and otherwise tweak your functions until the cows come home, but it’s wasted effort unless you’re focusing on the right functions.

Things to Remember

Limit most inlining to small, frequently called functions. This facili- tates debugging and binary upgradability, minimizes potential code bloat, and maximizes the chances of greater program speed.

Don’t declare function templates inline just because they appear in header files.

ptg7544714

Item 31: Minimize compilation dependencies between

Dalam dokumen Book Praise for Effective C++, Third Edition (Halaman 155-161)

Dokumen terkait