Monday, April 19, 2010

C++ Code Performance

There are lots of things that a C++ programmer can do to increase the performance of the written code.

Amdahl's law:
the performance improvement to be gained from using some faster mode of execution is limited by the fraction of the time the faster mode can be used.

Overall SpeedUp = 1/( (1-f) + (f/s) )
f: fraction of a program that is enhanced
s: speedup of the enhanced portion

1. Constness

Use the keyword constant as much as possible: in variables, function arguments, return values, and member functions.
  • const int MAX = 4 vs #define MAX = 4
Functionally, they are similar, but using a const variable allows the compiler to apply the usual C++ type safety, and because variables are entered in the symbol table it will be available in the debugger.
  • Functions
const should be used in parameters to simulate pass-by-value by passing a constant reference. This will avoid any copying costs and the reference being modified:
void SetHeight( const int & height);

Also use const in return values to prevent that value to be changed:
const char * GetName();
  • Classes
You can flag a function as const to indicate that the execution of this function will not change the state of the object to which it was applied. The only member variables that can break this law are the ones marked as mutable.
const char * GetName() const;

2. Function parameters

Pass arguments by reference instead of pass-by-value to eliminate its overhead by avoiding the copy unnecessary objects. To make sure the object reference passed it is not modified use the const keyword.

void MyGame::Update( Matrix mat) {...}  // By value - expensive
void MyGame::Update( const Matrix & mat) {...} // By reference - faster

3. Constructor and destructor

Try to not call the constructor unless it is absolutely necessary. The fastest code is that which never runs.
void Function(int arg)
{
Object obj;
if (arg *= 0)
return;
//...
}

When arg is zero, we pay the cost of calling Object's constructor and destructor. If arg is often zero, and especially if Object itself allocates memory, this waste can add up in a hurry. The solution, of course, is to move the declaration of obj until after the if statement.
Two techniques very useful to reduce the call overhead are: inline them and use initialisation list.

Inefficient:

Enemy::Enemy()
{
m_strName = "game_enemy";
m_position = Vector3( 0.0f , 0.0f ,0.0f );
m_life = 100;
}
Efficient:
Enemy::Enemy():
m_strName = "game_enemy",
m_position = Vector3( 0.0f , 0.0f ,0.0f ),
m_life = 100
{}
In this case, the initialisation happens only once as the objects are constructed and initialised.

4. Function types

5. Inlining

The compiler takes care of removing the function call and embeds its content directly into the calling code in order to avoid the overhead of the function call. Inline should be used only with small, frequently used functions. However, the compiler decides whether to inline the function or not.
inline bool isDead()  const { return (m_live==0); }
It has some drawbacks:
  • The size of the executable could increase out of control, due to every part of the code that calls the function would duplicate that function's call.
  • For a function to be inlined, its definition has to be present in the header file. That means that "include" statements that could otherwise be in the .cpp have to be moved to the .h file, which results in longer compile times.
So, avoid inlining while you are developing code. Then when the code is mostly complete, profile the program and see if any small functions appear toward the top of the most-called functions. Those will be great candidates to inline.


6. Return values
7. Avoid copies and temporaries

a)
Prefer preincrement to postincrement

The problem with writing x = y++ is that the increment function has to make a
copy of the original value of y, increment y, and then return the original value.
Thus, postincrement involves the construction of a temporary object, while
preincrement doesn't. For integers, there's no additional overhead, but for userdefined types, this is wasteful. You should use preincrement whenever you have
the option. You almost always have the option in for loop iterators.


8. Operator overloading

Try to avoid binary operators, the return type is not a reference or a pointer, but an object itself. That means that the compiler first will create a temporary object, load it with the result and then will copy it into the caller variable.
const Vector3d operator+( const Vector3d & v1, const Vector3d & v2 )
{
return Vector3d( v1.x + v2.x, v1.y + v2.y, v1.z + v2.z );
}
The solution is to replace binary operators with unary operators.In this case, we are not copying any object, we are just returning a reference to the object the function acted upon.
Vector3d & Vector3d::operator+=( const  Vector3d & v )
{
x+= v.x; y+= v.y; z+=v.z;
return *this;
}
9. Cache friendly
10. Memory allocation
  • Object Pools

Bibliography

1 comment:

  1. Well written post, in this post you are moving very important issues.

    ReplyDelete