intelliproject logo

Location: Desktop development - C/C++    License: The Intelliproject Open License (IPOL)

C++ Optimization Techniques

Posted by cocoa

This article describe some strategies used to optimize your c++ applications speed.

Skill: Beginner

Posted: 27/09/2010

Views: 666

Rating: 4.00 /5

Popularity: 0.00

Sign Up to vote for this article

Introduction

This article describes some techniques and strategies which can be used to optimize your c++ applications.

These techniques cover the following chapters:

  • Parameter passing
  • Constructors and Destructors

  • Virtual functions

  • Return Value

  • Temporary Objects

  • Inlining

Parameter passing

C++ normally passes arguments by value (as C does) . For example:

K a;
void foo(K x); 

The call foo(a) copies argument a to x before transferring control to foo. The parameter x will be placed on the function stack. The function parameters are pushed on the stack before the function is called and the parameters are pushed from right to left.

If type K is a built-in type such as int, double,float and so on , passing by value is inexpensive - as inexpensive as it is in C.

However, if K is a class type, passing by value copies a to x by invoking the copy constructor of the class. Calling the copy constructor generate in most of cases an unnecessary overhead and can be very expensive.

C++ offers several ways to avoid this case. In C and C++ you can pass parameters by address. To do this, you can declare foo as:

void foo(K *x); 

For large objects, passing by address is clearly faster that passing by value. Unfortunately, you must then write the call as foo(&a), which changes its appearance. It also changes the semantics of the call by making it possible for foo to change the value of a. Using the const qualifier you ensure that foo does not change the value of its actual argument.

void foo(K const *x);

For objects of modest size (8 to 16 bytes), it isn't always clear that passing by address will actually be faster than passing by value. Switching between pass- by-address and pass-by-value poses a maintenance problem. Not only must you change the function declarations, as in:

void foo(K x); => void foo(K const *x); 

but you must also change all the function calls, as in:

foo(a); => foo(&a); 

The alternative is to declare foo using a reference-to-const parameter, as in:

void foo(K const &x);

This lets you write the call as foo(a) (like passing by value), but keeps the speed of passing by address. Also, the const qualifier ensures that foo can't change the value of its argument.

Passing by reference-to-const may have a surprising cost. When passing by reference- to-const the compiler may construct a temporary object holding a copy of the argument.

Temporary objects are unnamed objects created on the stack by the compiler. They are created and used during reference initialization and during evaluation of expressions including standard type conversions, argument passing, function returns, and evaluation of the throw expression.

The alternative is to pass a reference-to-non-const:

void foo(K &x);

In that case the compile will not create any temporary object. But your function is allowed to change the content of your passed parameter.

Constructors and Destructors

The performance of constructors and destructors in most cases are poor due to the fact that an object's constructor (destructor) may call the constructors (destructors) of member objects and parent objects. This can result in constructors (destructors) that take a long time to execute, especially with objects in complex hierarchies or objects that contain several member objects.

Use the following hints to make your constructors and destructors more efficiently:

  • Objects should be only created when they are used. A good technique is to declare objects in the block where they are used. This prevents unnecessary constructors and destructors from being called.
  • Using the initializer list functionality that C++ offers is very important for efficiency. All member objects that are not in the initializer list are by default created by the compiler using their respective default constructors. By calling an object's constructor in the initializer list, you avoid having to call an object's default constructor and the overhead from an assignment operator inside the constructor. Also, using the initializer list may reduce the number of temporaries needed to construct the object.
  • Always use inline constructors and destructors to avoid the calling method overhead . When a function is called the following things will happen:
    1. All arguments are saved on the stack
    2. Program execution jumps to the address of the called function.
    3. Inside the function, registers ESI, EDI, EBX, and EBP are saved on the stack.
    4. The function-specific code is executed, and the return value is placed into the EAX register
    5. Registers ESI, EDI, EBX, and EBP are restored from the stack.
    6. Arguments are removed from the stack.

    The steps described above can be different from a lot of reason: the calling convention used, the operating system, etc.
    If yo declare your constructor and destructor all the steps described above will be eliminated , and your code will become faster.

Virtual functions

Virtual functions affect performance in 3 main ways:

  • The constructor must initialize the vptr table (a table of pointers to its member functions).
  • Virtual functions are called using pointer indirection technique, which results in a few extra instructions per method invocation as compared to a non-virtual method invocation.
  • Virtual functions whose resolution is only known at run-time cannot be inlined.

To avoid the overhead of virtual functions, you can use a template class in place of inheritance. A template class does not use the vptr table because the type of class is known at compile-time instead of having to be determined at run-time. Also, the non-virtual methods in a template class can be inlined.

The cost of using virtual functions is usually not a factor in calling methods that take a long time to execute since the call overhead is dominated by the method itself. In smaller methods, for example accessor methods, the cost of virtual functions is more important.

Return Value

Returning an object of built-in type from a function usually carries little to no overhead, since the object typically fits in a CPU register.Returning a larger object of class type may require more expensive copying from one memory location to another. To achieve this, an implementation may create a hidden object in the caller's stack frame, and pass the address of this object to the function. The function's return value is then copied into the hidden object since constructing this object takes time, you should try to avoid this it if possible.

There are several ways to accomplish this:

  • Instead of returning an object, add another parameter to the method which allows the programmer to pass in the object in which the programmer wants the result stored. This technique is called Return Value Optimization (RVO).
  • Whether or not RVO will result in an actual optimization is up to the compiler. Different compilers handle this differently. One way to help the compiler is to use a computational constructor. A computational constructor can be used in place of a method that returns an object. The computational constructor takes the same parameters as the method to be optimized, but instead of returning an object based on the parameters, it initializes itself based on the values of the parameters.

Temporary Objects

As I have described above, temporary objects are unnamed objects created on the stack by the compiler. They are created in various situations like:

  1. during reference initialization
  2. during evaluation of expressions including standard type conversions
  3. during argument passing
  4. during function returns
  5. during evaluation of the throw expression


When you pass an object to a method by value the compiler will create an temporary object. To avoid this pass by address or pass by reference. Read "Parameter passing" chapter.

Compilers may create a temporary object in assignment of an object. For example, a constructor that takes an int as an argument may be assigned an int. The compiler will create a temporary object using the int as the parameter and then call the assignment operator on the object. You can prevent the compiler from doing this behind your back by using the explicit keyword in the declaration of the constructor.

When objects are returned by value, temporaries are often used. Read "Return Value" section for more on this.

Temporaries can be avoided by using  <op>=  operators. For example, the code:

a = b + c;

could be written as:

a=b;
a+=c; 

Inlining

When a function is declared inline, the function is expanded at the compile time. The function is not treated as a separate unit like other normal functions. But a compiler is free to decide, if a function qualifies to be an inline function. If the inline function is found to have larger chunk of code, it will not be treated as an inline function.

Inlining is one of the easiest optimizations to use in C++ and it can result in the most dramatic improvements in execution speed.

Advantages

  • Inlining your code where it is needed, your program will avoid the function call and return overhead. It will make your code to run faster, but it will increase your executable code size. Inlining trivial accessors could be an example of effective inlining.
  • By marking it as inline, you can put a function definition in a header file (i.e. it can be included in multiple compilation unit, without the linker complaining)

Disadvanteges

  • It can make your code larger (i.e. if you use inline for non-trivial functions). As such, it could provoke paging and defeat optimizations from the compiler.
  • It slightly breaks your encapsulation : C++ inlining is resolved at compile time. Which means that should you change the code of the inlined function, you would need to recompile all the code using it to be sure it will be updated .
  • When used in a header, it makes your header file larger, and thus, will hide interesting informations (like the list of a class methods) with code the user don't care about (this is the reason that You should declare inlined functions inside a class, but will define it in an header after the class body, and never inside the class body). 
  • Excessive inlining can drastically increase code size, which can result in increased execution times because of a resulting lower cache hit rate.

The main thing to know when using inlining is when you should inline a method and when you shouldn't inline:

  • There is always a link between code size and execution speed when inlining. In general, small methods should be inlined and large methods should not be inlined.
  • If you are not sure of whether or not a given method should be inlined, the best way to decide is to profile the code. Run test samples of the code, timing inlining and non-inlining versions.
  • Watch out for inlined methods that make calls to other inlined methods. This can make the code size unexpectedly larger.
  • Singleton methods, methods that are only called from one place in a program, are ideal for inlining. The code size does not get any bigger and execution speed only gets better.
  • Recursive calls cannot be inlined, but you can rewrite them as iterative methods which can be inlined.

License

This article, along with any associated source code and files, is licensed under The Intelliproject Open License (IPOL)

About the author

cocoa

Location: Romania
Ocupation: Software Developer

Sign up to post message on the article message board!