Back to C++ Optimization Techniques
2: C++ Design Considerations
When you start working on your next program and begin to think about coding conventions, compilers, libraries, and general C++ issues, there are many factors to consider. In the following section I weigh some performance issues involved with C++ design considerations.
2.1 Take Advantage of STL Containers
Take advantage of STL containers. (See
STL container efficiency table). Not only is performance good today, it's only going to get better as STL vendors focus their efforts on optimization and compiler vendors improve template compilation. There are a number of other advantages to using STL:The STL is not the be-all end-all library of containers and algorithms. You can get better performance by writing your own containers. For instance, by definition, the STL list object must be a doubly-linked list. In cases where a singly-linked list would be fine, you pay a penalty for using the list object. This table shows the difference between Microsoft's (actually
Dinkumware's) implementation of lists and SGI's implementation of an STL-compatible singly-linked list called slist.Inserting at the beginning of a singly-linked list (slist) is around 30% faster for most objects than inserting into the standard list. If you needed to insert items at the end of a list, slist is not the best choice for obvious reasons.
One other drawback of the STL is that it only provides a limited set of container objects. It does not provide hash tables, for instance. However, there are a number of good extension STL libraries available.
SGI distributes an excellent STL implementation with a number of useful containers not defined in the standard.2.2: Consider Using References Instead of Pointers
As a basic design premise, consider using references instead of pointers. A quick example for comparison:
The
Ptr function and the Ref function generate exactly the same machine language. The advantages of the Ref function:2.3: Consider Two-Phase Construction
An object with one-phase construction is fully "built" with the constructor. An object with two-phase construction is minimally initialized in the constructor and fully "built" using a class method. Frequently copied objects with expensive constructors and destructors can be serious bottlenecks and are great candidates for two-phase construction. Designing your classes to support two-phase construction, even if internally they use one-phase, will make future optimizations easy.
The following code shows two different objects, OnePhase and TwoPhase, based on a Bitmap class. They both have the same external interface. Their internals are quite different. The OnePhase object is fully initialized in the constructor. The code for OnePhase is very simple. The code for TwoPhase, on the other hand, is more complicated. The TwoPhase constructor simply initializes a pointer. The TwoPhase methods have to check the pointer and allocate the Bitmap object if necessary.
What kind of savings can you expect? It depends. If you copy many objects, especially "empty" objects, the savings can be significant. If you don't do a lot of copying, two-phase construction can have a negative impact, because it adds a new level of indirection.
Exceptions are a great way to deal with unexpected errors. But they're expensive. Scott Meyers notes that throwing an exception is about "three orders of magnitude slower" than a normal return. Programs using exceptions are about "5-10% larger and 5-10% slower."
There are a couple of options. One is avoiding exception handling altogether. As more and more core libraries make use of exceptions, this is getting harder and harder to do, but it is an option nonetheless. Avoiding exceptions means not using
try, throw or catch in your code or in library code. Use operator new with the no_throw specification. Turn off exception handling in the compiler itself.I believe the judicious use of exceptions is the best solution. Limit try blocks to a few key places and make use of the
throw() function exception specification to indicate functions that don't throw exceptions. That way the compiler won't add code for unwinding stack objects unless it really needs to.2.5: Avoid Runtime Type Identification
Runtime type identification allows you to programmatically get information about objects and classes at runtime. For instance, given a pointer to a base class, you can use RTTI to determine exactly which type of class the object really is. The
dynamic_cast operator relies on RTTI to perform the proper casting of objects.In order for RTTI to work, a program must store information about every class with one or more virtual functions. This information is stored in a
type_info object. If your project includes many classes, the overhead can make your program larger. Runtime performance is not affected by RTTI.Very few programs need to use RTTI. Don't use it unless you need it. Simply avoid the
dynamic_cast and typeid operators. Make sure that RTTI is disabled on the compiler command line as well.2.6: Prefer stdio to iostream (printf vs. cout)
C++ stream I/O is a very flexible and safe method of doing input and output. It allows you to define specific output formats for your own objects. If an object doesn't support output, the compiler will tell you. Our old friend printf, on the other hand, is not very safe. If you specify the wrong number of parameters, or give the wrong order, you crash. You can't define new output formats, either. But printf does have a few things going for it: it's fast, it's easy to use, and it's often easier to read than long lines of << operators. Consider the following two code examples.
Both examples display the same results, but printf does it more efficiently and more readably. Use the <cstdio> family of functions instead of the <iostream> family when output speed is critical.
2.7: Evaluate Alternative Libraries
As the
stream I/O example shows, there's always the possibility that another library will be more efficient. It can pay big dividends to consider alternative libraries, whether they're 3D, graphics, mathematical or I/O libraries. The libraries that came with your compiler are probably not the most efficient code available. Evaluate the possibilities. Benchmark results. Report your findings to the development community at large. And don't forget to wrap and protect your code so that changing low-libraries is as painless as possible.