September 13, 2008

[C/C++] - Tips for Performance Enhancements

The following tips list three simple—yet rather unfamiliar—techniques to improve your program's performance, without sacrificing its readability or entailing design modifications. For example, programmers often don't know that simply by reordering data members in a class, they can significantly reduce its size. This optimization can boost performance especially if your application uses arrays of such objects. We will also learn the difference between postfix and prefix operatorsan issue of special importance when dealing with overloaded operators. Finally, we will learn a few techniques to eliminate the creation of temporary objects.

Tip 9: Optimizing Class Member Alignment

The size of a class can be changed simply by playing with the order of its members' declaration:

struct A

{

bool a;

int b;

bool c;

}; /*sizeof (A) == 12*/

On my machine, sizeof (A) equals 12. This result might seem surprising because the total size of A's members is only 6 bytes: 1+4+1 bytes. Where did the remaining 6 bytes come from? The compiler inserted 3 padding bytes after each bool member to make it align on a four-byte boundary. You can reduce A's size by reorganizing its data members as follows:

struct B

{

bool a;

bool c;

int b;

}; // sizeof (B) == 8

This time, the compiler inserted only 2 padding bytes after the member c. Because b occupies four bytes, it naturally aligns on a word boundary without necessitating additional padding bytes.

Tip 10: Differences between Postfix and Prefix Operators

The built-in ++ and operators can appear on both sides of their operand:

int n=0;

++n; /*prefix*/

n++; /*postfix*/

You probably know that a prefix operator first changes its operand before taking its value. For example:

int n=0, m=0;

n = ++m; /*first increment m, then assign its value to n*/

cout <<>

In this example, n equals 1 after the assignment because the increment operation took place before m's value was taken and assigned to n. By contrast,

int n=0, m=0;

n = m++; /*first assign m's value to n, then increment m*/

cout <<>

In this example, n equals 0 after the assignment because the increment operation took place after m's original value was taken and assigned to n.

To understand the difference between postfix and prefix operators better, examine the disassembly code generated for these operations. Even if you're not familiar with assembly languages, you can immediately see the difference between the two; simply notice where the inc (increment) assembly directive appears:

/*disassembly of the expression: m=n++;*/

mov ecx, [ebp-0x04] /*store n's value in ecx register*/

mov [ebp-0x08], ecx /*assign value in ecx to m*/

inc dword ptr [ebp-0x04] /*increment n*/


/*disassembly of the expression: m=++n;*/

inc dword ptr [ebp-0x04] /*increment n;*/

mov eax, [ebp-0x04] /*store n's value in eax register*/

mov [ebp-0x08], eax /*assign value in eax to m*/

Tip 11: Eliminating Temporary Objects

C++ creates temporary objects "behind your back" in several contexts. The overhead of a temporary can be significant because both its constructor and destructor are invoked. You can prevent the creation of a temporary object in most cases, though. In the following example, a temporary is created:

Complex x, y, z;

x=y+z; /* temporary created */

The expression y+z; results in a temporary object of type Complex that stores the result of the addition. The temporary is then assigned to x and destroyed subsequently. The generation of the temporary object can be avoided in two ways:

Complex y,z;

Complex x=y+z; /* initialization instead of assignment */

In the example above, the result of adding x and z is constructed directly into the object x, thereby eliminating the intermediary temporary. Alternatively, you can use += instead of + to get the same effect:

/* instead of x = y+z; */

x=y;

x+=z;

Although the += version is less elegant, it costs only two member function calls: assignment operator and operator +=. In contrast, the use of + results in three member function calls: a constructor call for the temporary, a copy constructor call for x, and a destructor call for the temporary.

12 and 13: Object-oriented Design

Although C++ supports several useful programming paradigms such as procedural programming, functional programming, and generic programming, object-oriented programming is unquestionably the most widely used and important paradigm. The following two tips provide guidelines for better object-oriented design and implementation. First, I will explain the importance of virtual destructors in class hierarchies. The next tip in this category shows how to deal with nested classes that are declared as friends of the enclosing class.

Tip 12: Why Inheriting from a Class That Has No Virtual Destructor is Dangerous

Classes with a non-virtual destructor aren't meant to serve as base classes (such classes are usually known as "concrete classes"). std::string, std::complex, and std::vector are concrete classes. Why is inheriting from such classes not recommended? When you use public inheritance, you create an is-a relationship between the base class and its derived classes. Consequently, pointers and references to base can actually point to a derived object. Because the destructor isn't virtual, C++ will not call the entire destructor chain when you delete such an object. For example:

class A

{

public:

~A() // non virtual

{

// ...

}

};

class B: public A /* bad; A has a non virtual dtor*/

{

public:

~B()

{

// ...

}

};

int main()

{

A * p = new B; /*seemingly OK*/

delete p; /*trouble, B's dtor not called*/

}

The result of failing to invoke an object's destructor is undefined. Therefore, you shouldn't use publicly inherit from such classes. In particular, don't derive from STL containers and std::string, as tempting as it may seem.

Tip 13: Declaring Nested Classes as Friends of Their Enclosing Class

When you declare a nested class as a friend of its containing class, place the friend declaration after the declaration of the nested class, not before it:

class A

{

private:

int i;

public:

class B /*nested class declared first*/

{

public:

B(A & a) { a.i=0;}; /*access A's private member*/

};

friend class B;/*friend declaration at the right place*/

};

If you place the friend declaration before the nested class's declaration, the compiler will discard the declaration since the friend class hasn't been seen yet.


No comments: