Topic : Opzimizing Your Code
Author : McMillan
Page : << Previous 3  Next >>
Go to page :


afterward. The member initialization list, on the other hand, appears before any user-written code in the constructor. Because the constructor body does not contain any user-written code in this case, the transformed constructor looks similar to the following:

Person::Person(const C& c1, const C& c2) // member initialization list ctor
{
//pseudo C++ code inserted by the compiler before user-written code
  c_1.C::C(c1); //invoke copy constructor of embedded object c_1
  c_2.C::C(c2); //invoke copy constructor of embedded object c_2
//user-written code comes here (note: there's no user code)
}


You can conclude from this example that for a class that has subobjects, a member initialization list is preferable to an assignment within the constructor's body. For this reason, many programmers use member initialization lists across the board, even for data members of fundamental types.

Prefix Versus Postfix Operators
The prefix operators ++ and -- tend to be more efficient than their postfix versions because when postfix operators are used, a temporary copy is needed to retain the value of the operand before it is changed. For fundamental types, the compiler can eliminate the extra copy. However, for user-defined types, this is nearly impossible. A typical implementation of the overloaded prefix and postfix operators demonstrates the difference between the two:

class Date
{
private:
  //...
  int AddDays(int d);
public:
  Date operator++(int unused);
  Date& operator++();
};
Date Date::operator++(int unused)  //postfix
{
  Date temp(*this); //create a copy of the current object
  this->AddDays(1); //increment  current object
  return temp; //return by value a copy of the object before it was incremented
}
Date& Date::operator++()   //prefix
{
  this->AddDays(1); //increment  current object
  return *this; //return by reference the current object
}


The overloaded postfix ++ is significantly less efficient than the prefix for two reasons: It requires the creation of a temporary copy, and it returns that copy by value. Therefore, whenever you are free to choose between postfix and prefix operators of an object, choose the prefix version.

Inline Functions
Inline functions can eliminate the overhead incurred by a function call and still provide the advantages of ordinary functions. However, inlining is not a panacea. In some situations, it can even degrade the program's performance. It is important to use this feature judiciously.

Function Call Overhead
The exact cost of an ordinary function call is implementation-dependent. It usually involves storing the current stack state, pushing the arguments of the function onto the stack and initializing them, and jumping to the memory address that contains the function's instructions -- only then does the function begin to execute. When the function returns, a sequence of reverse operations also takes place. In other languages (such as Pascal and COBOL), the overhead of a function call is even more noticeable because there are additional operations that the implementation performs before and after a function call. For a member function that merely returns the value of a data member, this overhead can be unacceptable. Inline functions were added to C++ to allow efficient implementation of such accessor and mutator member functions (getters and setters, respectively). Nonmember functions can also be declared inline.

Benefits of Inline Functions
The benefits of inlining a function are significant: From a user's point of view, the inlined function looks like an ordinary function. It can have arguments and a return value; furthermore, it has its own scope, yet it does not incur the overhead of a full-blown function call. In addition, it is remarkably safer and easier to debug than using a macro. But there are even more benefits. When the body of a function is inlined, the compiler can optimize the resultant code even further by applying context-specific optimizations that it cannot perform on the function's code alone.

All member functions that are implemented inside the class body are implicitly declared inline. In addition, compiler synthesized constructors, copy constructors, assignment operators, and destructors are implicitly declared inline. For example

class A
{
private:
  int a;
public:
  int Get_a() { return a; } // implicitly inline
  virtual void Set_a(int aa) { a = aa; } //implicitly inline
  //compiler synthesized canonical member functions also declared inline
};


It is important to realize, however, that the inline specifier is merely a recommendation to the compiler. The compiler is free to ignore this recommendation and outline the function; it can also inline a function that was not explicitly declared inline. Fortunately, C++ guarantees that the function's semantics cannot be altered by the compiler just because it is or is not inlined. For example, it is possible to take the address of a function that was not declared inline, regardless of whether it was inlined by the compiler (the result, however, is the creation of an outline copy of the function). How do compilers determine which functions are to be inlined and which are not? They have proprietary heuristics that are designed to pick the best candidates for inlining, depending on various criteria. These criteria include the size of the function body, whether it declares local variables, its complexity (for example, recursion and loops usually disqualify a function from inlining), and additional implementation- and context-dependent factors.

What Happens When a Function that Is Declared inline Cannot Be Inlined?
Theoretically, when the compiler refuses to inline a function, that function is then treated like an ordinary function: The compiler generates the object code for it, and invocations of the function are transformed into a jump to its memory address. Unfortunately, the implications of outlining a function are more complicated than that. It is a common practice to define inline functions in the class declaration. For example

   // filename Time.h
#include<ctime>
#include<iostream>
using namespace std;
class Time
{
public:
  inline void Show() { for (int i = 0; i<10; i++) cout<<time(0)<<endl;}
};
   // filename Time.h


Because the member function Time::Show() contains a local variable and a for loop, the compiler is likely to ignore the inline request and treat it as an ordinary member function. However, the class declaration itself can be #included in separately compiled translation units:

    // filename f1.cpp
#include "Time.hj"
void f1()
{
  Time t1;
  t1.Show();
}
    // f1.cpp
// filename f2.cpp
#include "Time.h"
void f2()
{
  Time t2;
  t2.Show();
}
    // f2.cpp


As a result, the compiler generates two identical copies of the same member function for the same program:

void f1();
void f2();
int main()
{
  f1();
  f2();
  return 0;
}


When the program is linked, the linker is faced with two identical copies of Time::Show(). Normally, function redefinition causes a link-time error. Un-inlined functions are a special case, however. Older implementations of C++ coped with this situation by treating an un-inlined function as if it had been declared static. Consequently, each copy of the compiled function was only visible within the translation unit in which it was declared. This solved the name clashing problem at the cost of multiple local copies of the same function. In this case, the inline declaration did not boost performance; on the contrary, every call of the un-inlined function was resolved as an ordinary function call with the regular overhead. Even worse, the multiple copies of the function code increased compilation and linkage time and bloated the size of the executable. Ironically, not declaring Time::Show() inline might have yielded better performance! Remember that the programmer is generally not aware of all the actual costs of this -- the compiler strains quietly, the linker sighs silently, and the resultant executable is more bloated and sluggish than ever. But it still works, and the users scratch their heads, saying, "This object-oriented programming is really awful! I'm sure this app would run much faster if I'd written it in C!".

Fortunately, the Standard's specification regarding un-inlined functions was recently changed. A Standard compliant implementation generates only a single copy of such a function, regardless of the number of translation units that define it. In other words, an un-inlined function is treated similarly to an ordinary function. However, it might take some time for all compiler vendors to adopt the new specifications.

Additional Issues of Concern
There are two more conundrums that are associated with inline functions. The first has to do with maintenance. A function can begin its life as a slim inline function, offering the benefits that were previously described. At a later phase in the lifetime of the system, the function body can be extended to include additional functionality, resulting from changes in the implementation of its class. Suddenly, the inline substitution can become inefficient or even impossible. It is therefore important to reconsider the removal of the inline specifier from such functions. For member functions that are defined in the class body, the change is more complicated because the function definition has to be moved to a separate source file.

Another problem might arise when inline functions are used in code libraries. It is impossible to maintain binary compatibility if the definition of an inline function changes. In this case, the users must recompile their code to reflect the change. For a non-inline function, the users only need to relink their

Page : << Previous 3  Next >>