Showing posts with label Performance. Show all posts
Showing posts with label Performance. Show all posts

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

Sunday, April 18, 2010

Load-Hit-Store

90% of the time is spent in 10% of the code, so make that 10% the fastest code it can be.


Load-Hit-Store: is one of those quirky CPU implementation details that can cause significant performance problems in high-level code. It happens when the compiler writes data to an address 'x' and the tries to load the data from 'x' again too song.

This sequence of a memory read operation (LOAD), the assignment of the value to a register (HIT) and the actual writing of the value into a register (LOAD) is usually hidden away in stages of the pipelines, so these operations cause no stalls. However, if the memory location being read was one recently written to by a previous write operation, it can take as many at 40 cycles before the Store operation can complete.

stfs fr3, 0(r3) // Store the float - takes up to 40 cycles
lwz r9, 0(r3) // Load r3 into r9
add r9, r1, r9 // Stall: use r9 before the store operation has finished

There are different ways to generate LHS:

Using member values or references pointers as iterators in tight loops

Example A:
for( int i = 0; i < 100; i++ )
{
m_iData++; // As member function it is stored in memory
}

//-----------------------------------------------------------
Example B:
void foo( int & count ) // the variable count is memory bound
{
for( int i = 0; i < 100; i++ )
{
count++; // As member function it is stored in memory
}
}

Solution: use registers that invoke no penalty

Example A:

int iData = m_iData;
for( int i = 0; i < 100; i++ )
{
iData++; // The local variable is stores in a register
}
m_iData = iData;

//-----------------------------------------------------------
Example B:
void foo( int & output )
{
int count = output;
for( int i = 0; i < 100; i++ )
{
count++; // As member function it is stored in memory
}
output = count;
}

Conversion between int and float

Try to avoid int to float conversions like:

float fAngle = (float)i * fAngleDelta;

Solution: It will be better to have int and float duplicated members.

typedef struct ScreenSize
{
int m_iWidth;
int m_iHeight;
float m_fWidth;
float m_fHeight;
// Update both, int and float
inline void SetHeight( int iWidth)
{
m_iWidth = iWidth;
m_fWidht = static_cast(iWidth);
}
}

C++ constructors that have just one parameter automatically
perform implicit type conversion. If you pass anint when the
constructor expects a float, the compiler will add the
necessary code to convert int to float. This will cause a
Load-Hit-Store issue. It is possible to add the explicit
keyword to the constructor declaration to prevent implicit
conversions. This, forces the code to either use a parameter of
the correct type, or cast the parameter to the correct type.

Read and write in memory too close

int CauseLHS( int *ptrA )
{
int a,b;
int * ptrB = ptrA; // B and A point to the same direction
*
ptrA = 5; // Write data to address prtA
b = *ptrB; // Read that data back again
//(won't be available for 40/80 cycles)

a = b + 10;// Stall! The data b isn't available yet
}

Solution: this seems like the sort of thing the compiler should notice and fix by simply keeping content of *ptrA in a register. But it doesn't, so it is obliged to read memory back from a pointer every time yo dereference it, because any other pointer in the function might have aliased and modified the data. The keyword __restrict on a pointer promises the compiler that it has no aliases: nothing else in the function points to that same data. Thus, this keyword helps to avoid LHS.

The compiler knows that if it writes data to a pointer, it doesn't need to read it back into a register later on because nothing else could have written to that address. Without __restrict, the compiler is forced to read data from every pointer every time it is used, because another pointer may have aliased x.

This keyword is a promise you make to the compiler. If you break your promise, you can get incorrect results. If pointer pA and pB are __restrict and pA==pB that will cause mysterious bugs.

int slow( int * a, int * b)
{
*
a = 5;
*
b = 7;
return *a + *b; // Stall! The compiler doesn't know whether
// a==b, so it has to reload both
// before the add

}
int fast( int *__restrict a, int *__restrict b)
{
*
a = 5;
*
b = 7; // Restrict promises that a!=b
return *a + *b; // No stall, a & b are in registers
}

There is no way to mark references as __restrict. In this case, copy the parameters to local variables inside your function, then write the final values back out again at the end, as we saw in the previous solutions.

Bibliography

Gamasutra article
__restric