Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ Timesaving Techniques (2005) [eng].pdf
Скачиваний:
65
Добавлен:
16.08.2013
Размер:
8.35 Mб
Скачать

65 Optimizing

Your Code

Technique

Making functions inline

Avoding temporary objects

Passing by reference

Postponing variable declarations

Choosing initialization over assigment

The final stage in any development process is to optimize the code to make it run better, faster, and more efficiently. Of course, in an ideal world, you would be optimizing the code as you went along,

but for most applications this simply isn’t possible. Instead, developers first get all of the functionality in place, test it, and then optimize it. This technique explores some methods you can use in the post-development phase to optimize your code. While it will never beat developing optimized code in the first place, post-development optimization can still identify and fix many bottlenecks in the code, and give a boost to code that runs just a little too slow.

Making Functions Inline

The first optimization technique that you can use is to make your functions inline code versions. Inline functions and methods are those that are defined at the same time they are implemented, in the class header.

An inline function can be considerably faster than its non-inline brethren, but that speed comes at a cost. Inline functions make your program bigger, and can sometimes even make it slower, because they add overhead to the code. An inline function is expanded in place wherever it is called, much like a macro. This can cause the code to grow significantly, because there are many copies of the same code in the program. It could slow loading and executing of the program down, if there are enough of the copies to make the program very large in size. However, in many cases, you can speed up your code significantly by making accessor functions inline. The reason is that the compiler can optimize the code in place, so that procedures that use inline functions are more optimal than they would be with regular function calls. Accessor functions, which provide read and write access to individual data members, are excellent targets for inlining because they are very small and can be optimized readily.

408 Technique 65: Optimizing Your Code

Take a look at the following class:

class Foo

{

string _str; public:

Foo(const char *str)

{

_str = str;

}

string getString(void);

};

string Foo::getString(void)

{

return _str;

}

}

We can improve the efficiency of this method by inlining the method, like this:

string getString(void)

{

return str;

}

Although many modern compilers do this optimization for you automatically, it’s always up to you to write the best possible code. Don’t rely on the compiler to fix it up.

The rules for inlining are very simple:

Always inline a simple accessor.

Never inline a large method, as the amount of code added to your program far outweighs the savings from the inline.

Inline only when the savings from the function overhead are small compared to the overall savings. In other words, inlining functions that call other functions is generally wasteful.

Avoiding Temporary Objects

If there is a single optimization technique that you should most seriously look at in C++, it’s to avoid having to create and delete temporary objects. Every time you pass an object by value, you make a temporary copy of that object. Every time that you write code that casts a basic data type to an object, you create a temporary object. If you want to optimize your code, look through it and remove all temporary object creations. Let’s take a look at an example of code that really overdoes it with temporaries, to get an idea of what the problem really is.

1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.

In this example, the file is named ch65.cpp, although you can use whatever you choose.

2. Type the code from Listing 65-1 into your file.

Better yet, copy the code from the source file on this book’s companion Web site.

LISTING 65-1: TEMPORARY OBJECTS

#include <iostream> #include <stdlib.h> #include <time.h>

using namespace std;

class Integer

{

int _iVal; public:

Integer()

{

cout << “Void constructor” << endl; _iVal = 0;

}

Integer( int iVal )

{

Avoiding Temporary Objects 409

cout << “Normal constructor” << endl;

_iVal = iVal;

}

Integer( const Integer& aCopy )

{

cout << “Copy constructor” << endl; _iVal = aCopy._iVal;

}

~Integer()

{

cout << “Destructor” << endl;

}

int getInt() const

{

return _iVal;

}

void setInt(int iVal)

{

_iVal = iVal;

}

Integer operator+= ( const Integer& i1 )

{

setInt( i1.getInt() + getInt() ); return *this;

}

};

Integer operator+( const Integer& i1, const

{

Integer& i2 )

3

 

Integer out;

1

 

out.setInt( i1.getInt() + i2.getInt() );

}

return out;

2

Integer operator-( const Integer& i1, const { Integer& i2 ) 4

return Integer(

i1.getInt() -

i2.getInt() );

 

}

 

void func( Integer

i1 )

{

 

cout << “Integer Value: “ << i1.getInt() << endl;

}

int main(void)

{

 

Integer

i1(5), i2(3);

Integer

i3;

cout <<

“Test plus: “ << endl;

i3 = i1

+ i2;

cout <<

“Result: “ << i3.getInt() <<

endl;

 

cout <<

“Test minus: “ << endl;

Integer

i4 = i3 - i2;

cout <<

“Result: “ << i4.getInt() <<

endl;

 

cout << “Calling function” << endl; func( i4 );

}

If we look at the operator+ function that works with the Integer class, you will see that the function accepts two Integer objects by reference. No objects are created here. However, within the object, we create a temporary object that is created locally (see 1). This object is then used to set the pieces of the integer from the two

Integer objects passed into the function, before

being returned to the calling program at

2.

The result is then assigned to the result variable

in the main program (see 3). How many tem-

porary objects are being created

here? Let’s run

the program and find out.

3. Save the source file in the code editor and then close the editor application.

4. Compile the source-code file with your favorite compiler on your favorite operating system.

5. Run the program on the console window of your favorite operating system.

410 Technique 65: Optimizing Your Code

If you have done everything correctly, you should see the following output on the console window:

$ ./a.exe

Normal constructor Normal constructor Void constructor Test plus:

Void constructor Destructor Result: 8

Test minus: Normal constructor Result: 5

Calling function Copy constructor Integer Value: 5 Destructor Destructor Destructor Destructor Destructor

The output shows each time an Integer object is being created and destroyed. Take a look at the print statements, especially those between the Test plus: statement and the Test minus: statement. There is a temporary object created here, and then destroyed.

Look at the difference between the addition operator (shown in Listing 65-1 at the line marked 3) and the subtraction operator (shown in Listing 65-1 at the line marked 4); notice that the addition operator has some overhead: a void constructor and a destructor call. The subtraction call, on the other hand, has no such constructor call. The only construction is the object in the main function that is being created to hold the result. How does this happen? The answer is, the compiler is optimized to understand that a returned object that is assigned can be created in the actual space that was allocated. Because the compiler can optimize an object that is constructed in a return statement, and just

“copy” the memory into the object the result is assigned to, it will save you a lot of time and overhead if you use this optimization. Also notice that if you pass an object by value, rather than by reference — as we do in the func function — a copy is made of the object and the various constructors and destructors called. If you are going to pass an object into a function, always pass it by reference. If you do not plan to change the object within that function, pass a constant reference.

Passing Objects by Reference

One of the problems with C++ is that not all of it can be implemented within any single class. That means you either end up with stand-alone functions or other objects that must receive objects in their methods. For example, consider what happens when you pass a stream to a function to output data:

int print_my_data(ostream& out)

{

out << “Dump of my data” << endl // Code to dump the data

}

In this simple example, we are passing a stream object to a function. Note that the stream is passed by reference, using the ampersand, rather than by value, which would make a copy of the object. Why do we do it this way? Well, suppose you tried to write the code another way, such as this:

int print_my_data( ostream out )

{

cout << “Dump of my data” << endl;

}

int main()

{

print_my_data( cout );

}

Passing Objects by Reference 411

You would get a compile error, because the ostream class copy constructor is private. Creating a private constructor is one way to avoid having copies made of your objects, but an easier one is to pass the object by reference. This avoids the overhead of a temporary object, avoids the call to the copy constructor and destructor, and avoids the problems with having objects that are modified in the function. Let’s look at an example of what I am talking about here.

1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.

In this example, the file is named ch65a.cpp, although you can use whatever you choose.

2. Type the code from Listing 65-2 into your file.

Or better yet, copy the code from the source file on this book’s companion Web site.

LISTING 65-2: PASSING BY REFERENCE

#include <iostream> #include <stdlib.h> #include <time.h>

using namespace std;

class Integer

{

int _iVal; public:

Integer()

{

cout << “Void constructor” << endl; _iVal = 0;

}

Integer( int iVal )

{

cout << “Normal constructor” << endl;

_iVal = iVal;

}

Integer( const Integer& aCopy )

{

cout << “Copy constructor” << endl; _iVal = aCopy._iVal;

}

~Integer()

{

cout << “Destructor” << endl;

}

int getInt() const

{

return _iVal;

}

void setInt(int iVal)

{

_iVal = iVal;

}

};

 

 

void func1( Integer i1 )

 

5

{

 

i1.setInt( 12 );

 

 

}

 

6

void func2( Integer& i2 )

 

{

 

i2.setInt(12);

}

int main()

{

Integer i;

func1(i);

cout << “After func1, value = “ << i.getInt() << endl;

func2(i);

cout << “After func2, value = “ << i.getInt() << endl;

}

The two functions in this listing both attempt to change the value of the integer value stored in an object passed to them. In the first case (shown at 5), the object is passed by value (the entire

object is copied before sending it to the function). In the second case (shown at 6), the object is passed by reference (the address of the object is passed to the function). You would normally expect the two functions to have the same result because they do the same thing. As we will see when we run the program, however, the two have very different ending results.

3. Save the source file in the code editor and then close the editor application.

412 Technique 65: Optimizing Your Code

4. Compile the source code file with your favorite compiler on your favorite operating system.

5. Run the program on the console window of your favorite operating system.

If you have done everything correctly, you should see the following output on the console window:

$ ./a.exe

 

 

Void constructor

 

 

Copy constructor

 

 

Destructor

 

 

After func1, value = 0

 

7

After func2, value = 12

8

Destructor

 

 

This code shows that passing a value by reference avoids the overhead of creating a temporary object. More importantly, this approach avoids the problem of making changes that aren’t reflected in the original object. Notice that the func1 function does not change the value of the integer variable (shown at 7 in the output). This is because the function accepts its argument by value, which makes a copy of the original object and modifies that copy, rather than the object itself. The func2 function, shown at 8 in the output, passes its object by reference, which means that the original object is modified, and the result is reflected in the calling routine.

Postponing Variable

Declarations

If you have ever programmed in a language other than C++, you are probably already used to the process of defining a variable before you use it. This was the way to program in C, FORTRAN, and BASIC, so most programmers kept that approach when they moved to the object-oriented C++ language. Doing so is a mistake, however, because it creates potentially unnecessary overhead in the code.

For example, consider the following function:

int func( char *ptr )

{

char *newPtr = new char[200]; if ( ptr == NULL )

{

delete newPtr; return –1;

}

//Do something with the newPtr variable

return 0;

}

The code shows no reason to initialize or define the newPtr variable before we check the input. If this was some class variable that was too large to instantiate efficiently, you’d be wasting a lot of time and memory by defining this variable without knowing whether you’d be using it.

In general, here are the steps you should always follow to optimize the instantiation of variables in a function or method:

1. First, do any input data validation.

If the data requires that you exit the function, do so before you create any variables locally.

2. Pass the data to existing classes as appropriate.

If the data you are using must be passed into a constructor or other method of a class, break the constructor for that class into two parts, validation and initialization.

For example, consider the following code for a class that tries to open a file:

int open_the_file(const char *strName)

{

//See whether the input character string is valid.

if (strName==NULL || strName[0]==0) return –1;