• Aucun résultat trouvé

Thinking Low-Level

Dans le document CODE WRITE GREAT (Page 25-29)

When the Java language was first becoming popular in the late 1990s, complaints like the following were heard:

Java’s interpreted code is forcing me to take a lot more care when writing software; I can’t get away with using linear searches the way I could in C/C++. I have to use good (and more difficult to implement) algorithms like binary search.

Statements like that truly demonstrate the major problem with using optimizing compilers: They allow programmers to get lazy. Although optimiz-ing compilers have made tremendous strides over the past several decades, no optimizing compiler can make up for poorly written HLL source code.

Of course, many naive HLL programmers read about how marvelous the optimization algorithms are in modern compilers and assume that the compiler will produce efficient code regardless of what they feed their com-pilers. But there is one problem with this attitude: although compilers can do a great job of translating well-written HLL code into efficient machine code,

it is easy to feed the compiler poorly written source code that stymies the optimization algorithms. In fact, it is not uncommon to see C/C++ program-mers bragging about how great their compiler is, never realizing how poor a job the compiler is doing because of how they’ve written their programs.

The problem is that they’ve never actually looked at the machine code the compiler produces from their HLL source code. They blindly assume that the compiler is doing a good job because they’ve been told that “compilers produce code that is almost as good as what an expert assembly language programmer can produce.”

1.4.1 Compilers Are Only as Good as the Source Code You Feed Them

It goes without saying that a compiler won’t change your algorithms in order to improve the performance of your software. For example, if you use a linear search rather than a binary search, you cannot expect the compiler to substitute a better algorithm for you. Certainly, the optimizer may improve the speed of your linear search by a constant factor (e.g., double or triple the speed of your code), but this improvement may be nothing compared with using a better algorithm. In fact, it’s very easy to show that, given a sufficiently large database, a binary search processed by an interpreter with no optimization will run faster than a linear search algorithm processed by the best compiler.

1.4.2 Helping the Compiler Produce Better Machine Code

Let’s assume that you’ve chosen the best possible algorithm(s) for your appli-cation and you’ve spent the extra money to get the best compiler available.

Is there something you can do to write HLL code that is more efficient than you would otherwise produce? Generally, the answer is, yes, there is.

One of the best-kept secrets in the compiler world is that most compiler benchmarks are rigged. Most real-world compiler benchmarks specify an algorithm to use, but they leave it up to the compiler vendors to actually implement the algorithm in their particular language. These compiler ven-dors generally know how their compilers behave when fed certain code sequences, so they will write the code sequence that produces the best possible executable.

Some may feel that this is cheating, but it’s really not. If a compiler is capable of producing that same code sequence under normal circum-stances (that is, the code generation trick wasn’t developed specifically for the benchmark), then there is nothing wrong with showing off the compiler’s performance. And if the compiler vendor can pull little tricks like this, so can you. By carefully choosing the statements you use in your HLL source code, you can “manually optimize” the machine code the compiler produces.

Several levels of manual optimization are possible. At the most abstract level, you can optimize a program by selecting a better algorithm for the software. This technique is independent of the compiler and the language.

Dropping down a level of abstraction, the next step is to manually optimize your code based on the HLL that you’re using while keeping the optimizations independent of the particular implementation of that language.

While such optimizations may not apply to other languages, they should apply across different compilers for the same language.

Dropping down yet another level, you can start thinking about structur-ing the code so that the optimizations are only applicable to a certain vendor or perhaps only a specific version of a compiler from some vendor.

At perhaps the lowest level, you begin to consider the machine code that the compiler emits and adjust how you write statements in an HLL to force the generation of some desirable sequence of machine instruc-tions. The Linux kernel is an example of this latter approach. Legend has it that the kernel developers were constantly tweaking the C code they wrote in the Linux kernel in order to control the 80x86 machine code that the GCC compiler was producing.

Although this development process may be a bit overstated, one thing is for sure: Programmers employing this process will produce the best possible machine code. This is the type of code that is comparable to that produced by decent assembly language programmers, and it is the kind of compiler output that HLL programmers like to brag about when arguing that compilers produce code that is comparable to handwritten assembly. The fact that most people do not go to these extremes to write their HLL code never enters into the argument. Nevertheless, the fact remains that carefully written HLL code can be nearly as efficient as decent assembly code.

Will compilers ever produce code that is as good as what an expert assembly language programmer can write? The correct answer is no.

However, careful programmers writing code in high-level languages like C can come close if they write their HLL code in a manner that allows the compiler to easily translate the program into efficient machine code. So, the real question is “How do I write my HLL code so that the compiler can translate it most efficiently?” Well, answering that question is the subject of this book. But the short answer is “Think in assembly; write in a high-level language.” Let’s take a quick look at how to do this.

1.4.3 How to Think in Assembly While Writing HLL Code

HLL compilers translate statements in that language to a sequence of one or more machine language (or assembly language) instructions. The amount of space in memory that an application consumes and the amount of time that an application spends in execution are directly related to the number of machine instructions and the type of machine instructions that the compiler emits.

However, the fact that you can achieve the same result with two different code sequences in an HLL does not imply that the compiler generates the same sequence of machine instructions for each approach. The HLL if and

switch/case statements are classic examples. Most introductory programming texts suggest that a chain of if-elseif-else statements is equivalent to a switch/case statement. Let’s examine the following trivial C example:

switch( x ) {

case 1:

printf( "X=1\n" );

break;

case 2:

printf( "X=2\n" );

break;

case 3:

printf( "X=3\n" );

break;

case 4:

printf( "X=4\n" );

break;

default:

printf( "X does not equal 1, 2, 3, or 4\n" );

}

/* equivalent IF statement */

if( x == 1 )

printf( "X=1\n" );

else if( x == 2 )

printf( "X=2\n" );

else if( x == 3 )

printf( "X=3\n" );

else if( x == 4 )

printf( "X=4\n" );

else

printf( "X does not equal 1, 2, 3, or 4\n" );

Although these two code sequences might be semantically equivalent (that is, they compute the same result), there is no guarantee whatsoever at all that the compiler will generate the same sequence of machine instructions for these two examples.

Which one will be better? Unless you understand how the compiler translates statements like these into machine code, and you have a basic understanding of the different efficiencies between various machine instructions, you can’t evaluate and choose one sequence over the other.

Programmers who fully understand how a compiler will translate these two sequences can judiciously choose one or the other of these two sequences based on the quality of the code they expect the compiler to produce.

By thinking in low-level terms when writing HLL code, a programmer can help an optimizing compiler approach the code quality level achieved by hand-optimized assembly language code. Sadly, the converse is usually True as well: if a programmer does not consider the low-level ramifications of his HLL code, the compiler will rarely generate the best possible machine code.

Dans le document CODE WRITE GREAT (Page 25-29)