|Ned Batchelder : Blog | Code | Text | Site|
Speeding C++ links
» Home : Blog : January 2004
On a C++ project, we'd been finding the build getting slower and slower. The link phase in particular seemed to take an inordinately long time, much longer than our experience would indicate that it should. Here's what I did about it.
Watching the output of a verbose linker session, we suspected the number of exported functions, and in particular, the number of functions defined in header files. Functions defined in header files have to be compiled into the .obj files of every .cpp file the headers are included into. Then it's up to the linker to sort them all out and keep only one.
So we suspected the header-defined functions. The first thing I tried was moving a whole pile of code-generated getters and setters from the header files to the .cpp files. This improved the link time, so the theory looked like a good one.
But how to figure out which other functions were real culprits? We're developing with Microsoft Visual C++, and it turns out they provide a handy tool called DUMPBIN which will dump out the contents of various binary files produced by the compiler (.obj, .lib, .dll, etc). In particular the /DIRECTIVES option will show the linker directives in the object file:
$ dumpbin/directives strutil.obj
After running this output through UNDNAME (which undecorates C++ mangled names), we have readable names of the functions exported from the object file:
/EXPORT:public: __thiscall CStr::CStr(unsigned short const *)
An /EXPORT directive is included for each function exported from the object file. So a quick collation of the export directives for the object files will show where to try to move functions to alleviate the problem.
I wrote a quick Python script to run DUMPBIN over all the .obj files, collate the information together, and print a report showing what functions were duplicated the most among the object files:
import os, path, sys
(Note: this was a quick hack script. There are probably better ways to have done much of it. I could have run UNDNAME once over the final output rather than once for each object file. There are no comments. I misspelled "occurrences". I know. It doesn't matter: it worked.)
The script provided very useful information. It showed that there were 90 functions that were compiled into every one of our 125 object files. Some were straightforward: a commonly-included header file contained a class with two dozen small getters and setters. They were easily moved into the corresponding .cpp file.
Some were more surprising: I'd forgotten about the auto-generated copy constructors and assignment operators. Since these are generated for you by the compiler, they must be inserted into every object file, and then be resolved by the linker.
For many of our classes, we don't need the copy constructor or assignment operator at all, so we can simply suppress them completely with a macro:
// Prevent auto-creation of unneeded assignment operators.
Simply using this macro in a class definition will turn off the compiler's generation of the functions. Making them private ensures that no code uses them. You don't even need to provide a definition for them, since no one is calling them.
After much of this analyzing and fiddling, and more helper macros along the lines of NO_ASSIGN, I had reduced the list of functions in all objects to only eight:
--- 125 occurences -----
The link time had been reduced by 80%. There's more that could be done (those CThreadListeners are calling out to me!), but I think I've passed the point of diminishing returns.