In this excerpt from his book “Write Great Code,” Randall Hyde introduces the common optimizations that compilers perform, and when a programmer should consider using or disabling them.
Later chapters will provide complete definitions and examples of common compiler optimizations in programming contexts where compilers typically use them. But for now, here’s a quick preview of the basic types.
- Constant folding
- Constant folding computes the value of constant expressions or subexpressions at compile time rather than emitting code to compute the result at runtime.
- Constant propagation
- Constant propagation replaces a variable access by a constant value if the compiler determines that the program assigned that constant to the variable earlier in the code.
- Dead code elimination
- Dead code elimination is the removal of the object code associated with a particular source code statement when the program will never use the result of that statement, or when a conditional block will never be true.
- Common subexpression elimination
- Frequently, part of an expression will appear elsewhere in the current function. If the values of the variables appearing in this subexpression haven’t changed, the program does not need to recompute the value of the expression. The program can save the value of the subexpression on the first evaluation and then use that value everywhere else that the subexpression appears.
- Strength reduction
- Often, the CPU can directly compute a value using a different operator than the source code specifies. For example, a shift operation can implement multiplication or division by a constant that is a power of 2, and certain modulo (remainder) operations are possible using bitwise
andinstructions (theshiftandandinstructions generally execute much faster than the multiply and divide instructions). Most compiler optimizers are good at recognizing such operations and replacing the more expensive computation with a less expensive sequence of machine instructions. - Induction
- In many expressions, particularly those appearing within a loop, the value of one variable in the expression is completely dependent upon some other variable. Frequently, the compiler can eliminate the computation of the new value or merge the two computations into one for the duration of that loop.
- Loop invariants
- The optimizations so far have all been techniques a compiler can use to improve code that is already well written. Handling loop invariants, by contrast, is a compiler optimization for fixing bad code. A loop invariant is an expression that does not change on each iteration of some loop. An optimizer can compute the result of such a calculation just once, outside the loop, and then use the computed value within the loop’s body. Many optimizers are smart enough to discover loop invariant calculations and can use code motion to move the invariant calculation outside the loop.
Good compilers can perform many other optimizations. However, there are the standard optimizations that you should expect any decent compiler to do.
Controlling Compiler Optimization
By default most compilers do very little optimization: You must explicitly tell the compiler to perform any optimization. This might seem counterintuitive; after all, we generally want compilers to produce the best possible code for us. However there are many definitions of “optimal,” and no single compiler output is going to satisfy every possible definition for this term. Therefore, most compilers enable optimization only when you explicitly tell them to.
You might still question why the typical default condition for most compilers is no optimization at all. You might argue that some sort of optimization, even if it’s not the particular type you’re interested in, is better than no optimization at all. However, no optimization is the default state for a few reasons:
- Optimization is a slow process. You get quicker turnaround times on compiles when you have the optimizer turned off. This can be a big help when going through rapid edit-compile-test cycles.
- Many debuggers don’t work properly with optimized code, and you have to turn off optimization in order to use a debugger on your application.
- Most compiler defects occur in the optimizer. By emitting unoptimized code, you’re less likely to encounter defects in the compiler (then again, the compiler’s author is less likely to be notified about defects in the compiler, too).
Most compilers provide command-line options that let you control the types of optimization the compiler performs. Early C compilers under Unix used command-line arguments like -O, O1, and -O2 to control the optimization phases of the compiler. Many later compilers (C and otherwise) have adopted this same strategy, if not exactly the same command-line options. The bottom line is that you’ve generally got some control over the type of optimizations the compiler performs.
If you’re wondering why a compiler might offer multiple options to control optimization rather than just having a single option (optimization or no optimization), remember that “optimization” means different things to different people. Some people might want code that is optimized for space; others might want code that is optimized for speed (and the two optimizations could be mutually exclusive in a given situation). Some people might want a small amount of optimization, but won’t want the compiler to take forever to process their files, so they’d be willing to live with a small set of fast optimizations. Others might want to control optimization for a specific member of a CPU family (such as the Pentium 4 processor in the 80×86 family). Furthermore, some optimizations are “safe” (that is, they always produce correct code) only if the program is written in a certain way. You certainly don’t want to enable such optimizations unless the programmer guarantees that they’ve written their code in an appropriate fashion. Finally, for programmers who are carefully writing their HLL code, some optimizations the compiler performs may actually produce inferior code, so the ability to choose specific optimizations can be very handy to the programmer who wants to produce the best possible code. Therefore, most modern compilers provide considerable flexibility over the types of optimizations they perform.
The Microsoft Visual C++ compiler has 39 different command-line options to control optimization. GCC has a comparable, though much longer list, that you can view by specifying -v --help on the GCC command line. Most of the individual optimization flags begin with -f . You can also use -On , where n is a single digit integer value, to specify different levels of optimization. You should take care when using -O3 (or higher), as this may perform some unsafe optimizations in certain cases.
Comparing Different Compilers’ Optimizations
One real-world constraint on our ability to produce great code is that different compilers provide a wildly varying set of optimizations. Even when two different compilers perform the same optimizations, they differ greatly in the effectiveness of their optimizations.
Fortunately, you can visit several websites that have benchmarked various compilers. Using your favorite search engine, just search for a topic like “compiler benchmarks” or “compiler comparisons” and have fun. A very good website that compares several modern compilers is www.willus.com. (Click the Compiler Benchmarks link.)




