abrash-black-book/01-02.md
2013-12-30 20:26:41 +11:00

134 lines
No EOL
6 KiB
Markdown

### Rules for Building High-Performance Code {#Heading5}
We've got the following rules for creating high-performance software:
- Know where you're going (understand the objective of the software).
- Make a big map (have an overall program design firmly in mind, so
the various parts of the program and the data structures work well
together).
- Make lots of little maps (design an algorithm for each separate part
of the overall design).
- Know the territory (understand exactly how the computer carries out
each task).
- Know when it matters (identify the portions of your programs where
performance matters, and don't waste your time optimizing the rest).
- Always consider the alternatives (don't get stuck on a single
approach; odds are there's a better way, if you're clever and
inventive enough).
- Know how to turn on the juice (optimize the code as best you know
how when it *does* matter).
Making rules is easy; the hard part is figuring out how to apply them in
the real world. For my money, examining some actual working code is
always a good way to get a handle on programming concepts, so let's look
at some of the performance rules in action.
#### Know Where You're Going {#Heading6}
If we're going to create high-performance code, first we have to know
what that code is going to do. As an example, let's write a program that
generates a 16-bit checksum of the bytes in a file. In other words, the
program will add each byte in a specified file in turn into a 16-bit
value. This checksum value might be used to make sure that a file hasn't
been corrupted, as might occur during transmission over a modem or if a
Trojan horse virus rears its ugly head. We're not going to do anything
with the checksum value other than print it out, however; right now
we're only interested in generating that checksum value as rapidly as
possible.
#### Make a Big Map {#Heading7}
How are we going to generate a checksum value for a specified file? The
logical approach is to get the file name, open the file, read the bytes
out of the file, add them together, and print the result. Most of those
actions are straightforward; the only tricky part lies in reading the
bytes and adding them together.
#### Make Lots of Little Maps {#Heading8}
Actually, we're only going to make one little map, because we only have
one program section that requires much thought—the section that reads
the bytes and adds them up. What's the best way to do this?
It would be convenient to load the entire file into memory and then sum
the bytes in one loop. Unfortunately, there's no guarantee that any
particular file will fit in the available memory; in fact, it's a sure
thing that many files *won't* fit into memory, so that approach is out.
Well, if the whole file won't fit into memory, one byte surely will. If
we read the file one byte at a time, adding each byte to the checksum
value before reading the next byte, we'll minimize memory requirements
and be able to handle any size file at all.
Sounds good, eh? Listing 1.1 shows an implementation of this approach.
Listing 1.1 uses C's **read()** function to read a single byte, adds the
byte into the checksum value, and loops back to handle the next byte
until the end of the file is reached. The code is compact, easy to
write, and functions perfectly—with one slight hitch:
It's *slow*.
**LISTING 1.1 L1-1.C**
/*
* Program to calculate the 16-bit checksum of all bytes in the
* specified file. Obtains the bytes one at a time via read(),
* letting DOS perform all data buffering.
*/
#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
int Handle;
unsigned char Byte;
unsigned int Checksum;
int ReadLength;
if ( argc != 2 ) {
printf("usage: checksum filename\n");
exit(1);
}
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf("Can't open file: %s\n", argv[1]);
exit(1);
}
/* Initialize the checksum accumulator */
Checksum = 0;
/* Add each byte in turn into the checksum accumulator */
while ( (ReadLength = read(Handle, &Byte, sizeof(Byte))) > 0 ) {
Checksum += (unsigned int) Byte;
}
if ( ReadLength == -1 ) {
printf("Error reading file %s\n", argv[1]);
exit(1);
}
/* Report the result */
printf("The checksum is: %u\n", Checksum);
exit(0);
}
Table 1.1 shows the time taken for Listing 1.1 to generate a checksum of
the WordPerfect version 4.2 thesaurus file, TH.WP (362,293 bytes in
size), on a 10 MHz AT machine of no special parentage. Execution times
are given for Listing 1.1 compiled with Borland and Microsoft compilers,
with optimization both on and off; all four times are pretty much the
same, however, and all are much too slow to be acceptable. Listing 1.1
requires over two and one-half minutes to checksum *one* file!
------------------- -----------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Listings 1.2 and 1.3 form the C/assembly equivalent to Listing 1.1, and Listings 1.6 and 1.7 form the C/assembly equivalent to Listing 1.5.*
------------------- -----------------------------------------------------------------------------------------------------------------------------------------------
These results make it clear that it's folly to rely on your compiler's
optimization to make your programs fast. Listing 1.1 is simply poorly
designed, and no amount of compiler optimization will compensate for
that failing. To drive home the point, conListings 1.2 and 1.3, which
together are equivalent to Listing 1.1 except that the entire checksum
loop is written in tight assembly code. The assembly language
implementation is indeed faster than any of the C versions, as shown in
Table 1.1, but it's less than 10 percent faster, and it's still
unacceptably slow.