556 lines
26 KiB
Markdown
556 lines
26 KiB
Markdown
---
|
||
title: Michael Abrash's Graphics Programming Black Book, Special Edition
|
||
author: Michael Abrash
|
||
date: '1997-07-01'
|
||
identifier:
|
||
- scheme: ISBN
|
||
text: 1576101746
|
||
publisher: The Coriolis Group
|
||
category: 'Web and Software Development: Game Development,Web and Software Development:
|
||
Graphics and Multimedia Development'
|
||
chapter: '05'
|
||
pages: 111-123
|
||
---
|
||
|
||
## Chapter 5 -- Crossing the Border
|
||
|
||
### Searching Files with Restartable Blocks
|
||
|
||
*We just moved.* Those three little words should strike terror into the
|
||
heart of anyone who owns more than a sleeping bag and a toothbrush. Our
|
||
last move was the usual zoo—and then some. Because the distance from the
|
||
old house to the new was only five miles, we used cars to move
|
||
everything smaller than a washing machine. We have a sizable
|
||
household—cats, dogs, kids, com, you name it—so the moving process took
|
||
a number of car trips. A *large* number—33, to be exact. I personally
|
||
spent about 15 hours just driving back and forth between the two houses.
|
||
The move took days to complete.
|
||
|
||
*Never again*.
|
||
|
||
You're probably wondering two things: What does this have to do with
|
||
high-performance programming, and why on earth didn't I rent a truck and
|
||
get the move over in one or two trips, saving hours of driving? As it
|
||
happens, the second question answers the first. I didn't rent a truck
|
||
because it *seemed* easier and cheaper to use cars—no big truck to
|
||
drive, no rentals, spread the work out more manageably, and so on.
|
||
|
||
It wasn't easier, and wasn't even much cheaper. (It costs quite a bit to
|
||
drive a car 330 miles, to say nothing of the value of 15 hours of my
|
||
time.) But, at the time, it seemed as though my approach would be easier
|
||
and cheaper. In fact, I didn't realize just how much time I had wasted
|
||
driving back and forth until I sat down to write this chapter.
|
||
|
||
In Chapter 1, I briefly discussed using *restartable blocks*. This, you
|
||
might remember, is the process of handling in chunks data sets too large
|
||
to fit in memory so that they can be processed just about as fast as if
|
||
they did fit in memory. The restartable block approach is very fast but
|
||
is relatively difficult to program.
|
||
|
||
At the opposite end of the spectrum lies byte-by-byte processing,
|
||
whereby DOS (or, in less extreme cases, a group of library functions) is
|
||
allowed to do all the hard work, so that you only have to deal with one
|
||
byte at a time. Byte-by-byte processing is easy to program but can be
|
||
extremely slow, due to the vast overhead that results from invoking DOS
|
||
each time a byte must be processed.
|
||
|
||
Sound familiar? It should. I moved via the byte-by-byte approach, and
|
||
the overhead of driving back and forth made for miserable performance.
|
||
Renting a truck (the restartable block approach) would have required
|
||
more effort and forethought, but would have paid off handsomely.
|
||
|
||
> 
|
||
> The easy, familiar approach often has nothing in its favor except that
|
||
> it requires less thinking; not a great virtue when writing
|
||
> high-performance code—or when moving.
|
||
|
||
And with that, let's look at a fairly complex application of restartable
|
||
blocks.
|
||
|
||
#### Searching for Text
|
||
|
||
The application we're going to examine searches a file for a specified
|
||
string. We'll develop a program that will search the file specified on
|
||
the command line for a string (also specified on the comline), then
|
||
report whether the string was found or not. (Because the searched-for
|
||
string is obtained via `argv`, it can't contain any whitespace
|
||
characters.)
|
||
|
||
This is a *very* limited subset of what search utilities such as grep
|
||
can do, and isn't really intended to be a generally useful application;
|
||
the purpose is to provide insight into restartable blocks in particular
|
||
and optimization in general in the course of developing a search engine.
|
||
That search engine will, however, be easy to plug into any program, and
|
||
there's nothing preventing you from using it in a more fruitful context,
|
||
like searching through a user-selectable file set.
|
||
|
||
The first point to address in designing our program involves the
|
||
appropriate text-search approach to use. Literally dozens of workable
|
||
ways exist to search a file. We can immediately discard all approaches
|
||
that involve reading any byte of the file more than once, because disk
|
||
access time is orders of magnitude slower than any data handling
|
||
performed by our own code. Based on our experience in Chapter 1, we can
|
||
also discard all approaches that get bytes either one at a time or in
|
||
small sets from DOS. We want to read big "buffers-full" of bytes at a
|
||
pop from the searched file, and the bigger the buffer the better—in
|
||
order to minimize DOS's overhead. A good rough cut is a buffer that will
|
||
be between 16K and 64K, depending on the exact search approach, 64K
|
||
being the maximum size because near pointers make for superior
|
||
performance.
|
||
|
||
So we know we want to work with a large buffer, filling it as
|
||
infrequently as possible. Now we have to figure out how to search
|
||
through a file by loading it into that large buffer in chunks. To
|
||
accomplish this, we have to know how we want to do our searching, and
|
||
that's not immediately obvious. Where do we begin?
|
||
|
||
Well, it might be instructive to consider how we would search if our
|
||
search involved only one buffer, already resident in memory. In other
|
||
words, suppose we don't have to bother with file handling at all, and
|
||
further suppose that we don't have to deal with searching through
|
||
multiple blocks. After all, that's a good description of the
|
||
all-important inner loop of our searching program, where the program
|
||
will spend virtually all of its time (aside from the unavoidable disk
|
||
access overhead).
|
||
|
||
### Avoiding the String Trap
|
||
|
||
The easiest approach would be to use a C/C++ library function. The
|
||
closest match to what we need is `strstr()`, which searches one string
|
||
for the first occurrence of a second string. However, while `strstr()`
|
||
would work, it isn't ideal for our purposes. The problem is this: Where
|
||
we want to search a fixed-length buffer for the first occurrence of a
|
||
string, `strstr()` searches a *string* for the first occurrence of
|
||
another string.
|
||
|
||
We could put a zero byte at the end of our buffer to allow `strstr()`
|
||
to work, but why bother? The `strstr()` function must spend time
|
||
either checking for the end of the string being searched or determining
|
||
the length of that string—wasted effort given that we already know
|
||
exactly how long our search buffer is. Even if a given `strstr()`
|
||
implementation is well-written, its performance will suffer, at least
|
||
for our application, from unnecessary overhead.
|
||
|
||
> 
|
||
> This illustrates why you shouldn't think of C/C++ library functions as
|
||
> black boxes; understand what they do and try to figure out how they do
|
||
> it, and relate that to their performance in the context you're
|
||
> interested in.
|
||
|
||
### Brute-Force Techniques
|
||
|
||
Given that no C/C++ library function meets our needs precisely, an
|
||
obvious alternative approach is the brute-force technique that uses
|
||
`memcmp()` to compare *every* potential matching location in the
|
||
buffer to the string we're searching for, as illustrated in Figure 5.1.
|
||
|
||
By the way, we could, of course, use our own code, working with pointers
|
||
in a loop, to perform the comparison in place of `memcmp()`. But
|
||
`memcmp()` will almost certainly use the very fast `REPZ CMPS`
|
||
instruction. However, *never assume!* It wouldn't hurt to use a debugger
|
||
to check out the actual machine-code implementation of `memcmp()` from
|
||
your compiler. If necessary, you could always write your own assembly
|
||
language implementation of `memcmp()`.
|
||
|
||

|
||
|
||
Invoking `memcmp()` for each potential match location works, but
|
||
entails considerable overhead. Each comparison requires that parameters
|
||
be pushed and that a call to and return from `memcmp()` be performed,
|
||
along with a pass through the comparison loop. Surely there's a better
|
||
way!
|
||
|
||
Indeed there is. We can eliminate most calls to `memcmp()` by
|
||
performing a simple test on each potential match location that will
|
||
reject most such locations right off the bat. We'll just check whether
|
||
the first character of the potentially matching buffer location matches
|
||
the first character of the string we're searching for. We could make
|
||
this check by using a pointer in a loop to scan the buffer for the next
|
||
match for the first character, stopping to check for a match with the
|
||
rest of the string *only* when the first character matches, as shown in
|
||
Figure 5.2.
|
||
|
||
### Using memchr()
|
||
|
||
There's yet a better way to implement this approach, however. Use the
|
||
`memchr()` function, which does nothing more or less than find the
|
||
next occurrence of a specified character in a fixed-length buffer
|
||
(presumably by using the extremely efficient `REPNZ SCASB`
|
||
instruction, although again it wouldn't hurt to check). By using
|
||
`memchr()` to scan for potential matches that can then be fully tested
|
||
with `memcmp()`, we can build a highly efficient search engine that
|
||
takes good advantage of the information we have about the buffer being
|
||
searched and the string we're searching for. Our engine also relies
|
||
heavily on repeated string instructions, assuming that the `memchr()`
|
||
and `memcmp()` library functions are properly coded.
|
||
|
||

|
||
|
||
We're going to go with the this approach in our file-searching program;
|
||
the only trick lies in deciding how to integrate this approach with
|
||
restartable blocks in order to search through files larger than our
|
||
buffer. This certainly isn't the fastest-possible searching algorithm;
|
||
as one example, the Boyer-Moore algorithm, which cleverly eliminates
|
||
many buffer locations as potential matches in the process of checking
|
||
preceding locations, can be considerably faster. However, the
|
||
Boyer-Moore algorithm is quite complex to understand and implement, and
|
||
would distract us from our main focus, restartable blocks, so we'll save
|
||
it for a later chapter (Chapter 14, to be precise). Besides, I suspect
|
||
you'll find the approach we'll use to be fast enough for most purposes.
|
||
|
||
Now that we've selected a searching approach, let's integrate it with
|
||
file handling and searching through multiple blocks. In other words,
|
||
let's make it restartable.
|
||
|
||
#### Making a Search Restartable
|
||
|
||
As it happens, there's no great trick to putting the pieces of this
|
||
search program together. Basically, we'll read in a buffer of data
|
||
(we'll work with 16K at a time to avoid signed overflow problems with
|
||
integers), search it for a match with the `memchr()`/`memcmp()` engine
|
||
described, and exit with a "string found" response if the desired string
|
||
is found.
|
||
|
||
Otherwise, we'll load in another buffer full of data from the file,
|
||
search it, and so on. The only trick lies in handling potentially
|
||
matching sequences in the file that start in one buffer and end in the
|
||
next—that is, sequences that span buffers. We'll handle this by copying
|
||
the unchecked bytes at the end of one buffer to the start of the next
|
||
and reading that many fewer bytes the next time we fill the buffer.
|
||
|
||
The exact number of bytes to be copied from the end of one buffer to the
|
||
start of the next is the length of the searched-for string minus 1,
|
||
since that's how many bytes at the end of the buffer can't be checked as
|
||
possible matches (because the check would run off the end of the
|
||
buffer).
|
||
|
||
That's really all there is to it. Listing 5.1 shows the file-searching
|
||
program. As you can see, it's not particularly complex, although a few
|
||
fairly opaque lines of code are required to handle merging the end of
|
||
one block with the start of the next. The code that searches a single
|
||
block—the function `SearchForString()`—is simple and compact (as it
|
||
should be, given that it's by far the most heavily-executed code in the
|
||
listing).
|
||
|
||
Listing 5.1 nicely illustrates the core concept of restartable blocks:
|
||
Organize your program so that you can do your processing within each
|
||
block as fast as you could if there were only one block—which is to say
|
||
at top speed—and make your blocks as large as possible in order to
|
||
minimize the overhead associated with going from one block to the next.
|
||
|
||
**LISTING 5.1 SEARCH.C**
|
||
|
||
```c
|
||
/* Program to search the file specified by the first command-line
|
||
* argument for the string specified by the second command-line
|
||
* argument. Performs the search by reading and searching blocks
|
||
* of size BLOCK_SIZE. */
|
||
|
||
#include <stdio.h>
|
||
#include <fcntl.h>
|
||
#include <string.h>
|
||
#include <alloc.h> /* alloc.h for Borland compilers,
|
||
malloc.h for Microsoft compilers */
|
||
|
||
#define BLOCK_SIZE 0x4000 /* we'll process the file in 16K blocks */
|
||
|
||
/* Searches the specified number of sequences in the specified
|
||
buffer for matches to SearchString of SearchStringLength. Note
|
||
that the calling code should already have shortened SearchLength
|
||
if necessary to compensate for the distance from the end of the
|
||
buffer to the last possible start of a matching sequence in the
|
||
buffer.
|
||
*/
|
||
|
||
int SearchForString(unsigned char *Buffer, int SearchLength,
|
||
unsigned char *SearchString, int SearchStringLength)
|
||
{
|
||
unsigned char *PotentialMatch;
|
||
|
||
/* Search so long as there are potential-match locations
|
||
remaining */
|
||
while ( SearchLength ) {
|
||
/* See if the first character of SearchString can be found */
|
||
if ( (PotentialMatch =
|
||
memchr(Buffer, *SearchString, SearchLength)) == NULL ) {
|
||
break; /* No matches in this buffer */
|
||
}
|
||
/* The first character matches; see if the rest of the string
|
||
also matches */
|
||
if ( SearchStringLength == 1 ) {
|
||
return(1); /* That one matching character was the whole
|
||
search string, so we've got a match */
|
||
}
|
||
else {
|
||
/* Check whether the remaining characters match */
|
||
if ( !memcmp(PotentialMatch + 1, SearchString + 1,
|
||
SearchStringLength - 1) ) {
|
||
return(1); /* We've got a match */
|
||
}
|
||
}
|
||
/* The string doesn't match; keep going by pointing past the
|
||
potential match location we just rejected */
|
||
SearchLength -= PotentialMatch - Buffer + 1;
|
||
Buffer = PotentialMatch + 1;
|
||
}
|
||
|
||
return(0); /* No match found */
|
||
}
|
||
|
||
main(int argc, char *argv[]) {
|
||
int Done; /* Indicates whether search is done */
|
||
int Handle; /* Handle of file being searched */
|
||
int WorkingLength; /* Length of current block */
|
||
int SearchStringLength; /* Length of string to search for */
|
||
int BlockSearchLength; /* Length to search in current block */
|
||
int Found; /* Indicates final search completion
|
||
status */
|
||
int NextLoadCount; /* # of bytes to read into next block,
|
||
accounting for bytes copied from the
|
||
last block */
|
||
unsigned char *WorkingBlock; /* Block storage buffer */
|
||
unsigned char *SearchString; /* Pointer to the string to search for */
|
||
unsigned char *NextLoadPtr; /* Offset at which to start loading
|
||
the next block, accounting for
|
||
bytes copied from the last block */
|
||
|
||
/* Check for the proper number of arguments */
|
||
if ( argc != 3 ) {
|
||
printf("usage: search filename search-string\n");
|
||
exit(1);
|
||
}
|
||
|
||
/* Try to open the file to be searched */
|
||
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
|
||
printf("Can't open file: %s\n", argv[1]);
|
||
exit(1);
|
||
}
|
||
/* Calculate the length of text to search for */
|
||
SearchString = argv[2];
|
||
SearchStringLength = strlen(SearchString);
|
||
/* Try to get memory in which to buffer the data */
|
||
if ( (WorkingBlock = malloc(BLOCK_SIZE)) == NULL ) {
|
||
printf("Can't get enough memory\n");
|
||
exit(1);
|
||
}
|
||
|
||
/* Load the first block at the start of the buffer, and try to
|
||
fill the entire buffer */
|
||
NextLoadPtr = WorkingBlock;
|
||
NextLoadCount = BLOCK_SIZE;
|
||
Done = 0; /* Not done with search yet */
|
||
Found = 0; /* Assume we won't find a match */
|
||
/* Search the file in BLOCK_SIZE chunks */
|
||
do {
|
||
/* Read in however many bytes are needed to fill out the block
|
||
(accounting for bytes copied over from the last block), or
|
||
the rest of the bytes in the file, whichever is less */
|
||
if ( (WorkingLength = read(Handle, NextLoadPtr,
|
||
NextLoadCount)) == -1 ) {
|
||
printf("Error reading file %s\n", argv[1]);
|
||
exit(1);
|
||
}
|
||
/* If we didn't read all the bytes we requested, we're done
|
||
after this block, whether we find a match or not */
|
||
if ( WorkingLength != NextLoadCount ) {
|
||
Done = 1;
|
||
}
|
||
|
||
/* Account for any bytes we copied from the end of the last
|
||
block in the total length of this block */
|
||
WorkingLength += NextLoadPtr - WorkingBlock;
|
||
/* Calculate the number of bytes in this block that could
|
||
possibly be the start of a matching sequence that lies
|
||
entirely in this block (sequences that run off the end of
|
||
the block will be transferred to the next block and found
|
||
when that block is searched)
|
||
*/
|
||
if ( (BlockSearchLength =
|
||
WorkingLength - SearchStringLength + 1) <= 0 ) {
|
||
Done = 1; /* Too few characters in this block for
|
||
there to be any possible matches, so this
|
||
is the final block and we're done without
|
||
finding a match
|
||
*/
|
||
}
|
||
else {
|
||
/* Search this block */
|
||
if ( SearchForString(WorkingBlock, BlockSearchLength,
|
||
SearchString, SearchStringLength) ) {
|
||
Found = 1; /* We've found a match */
|
||
Done = 1;
|
||
}
|
||
else {
|
||
/* Copy any bytes from the end of the block that start
|
||
potentially-matching sequences that would run off
|
||
the end of the block over to the next block */
|
||
if ( SearchStringLength > 1 ) {
|
||
memcpy(WorkingBlock,
|
||
WorkingBlock+BLOCK_SIZE - SearchStringLength + 1,
|
||
SearchStringLength - 1);
|
||
}
|
||
/* Set up to load the next bytes from the file after the
|
||
bytes copied from the end of the current block */
|
||
NextLoadPtr = WorkingBlock + SearchStringLength - 1;
|
||
NextLoadCount = BLOCK_SIZE - SearchStringLength + 1;
|
||
}
|
||
}
|
||
} while ( !Done );
|
||
|
||
/* Report the results */
|
||
if ( Found ) {
|
||
printf("String found\n");
|
||
} else {
|
||
printf("String not found\n");
|
||
}
|
||
exit(Found); /* Return the found/not found status as the
|
||
DOS errorlevel */
|
||
}
|
||
```
|
||
|
||
### Interpreting Where the Cycles Go
|
||
|
||
To boost the overall performance of Listing 5.1, I would normally
|
||
convert `SearchForString()` to assembly language at this point.
|
||
However, I'm not going to do that, and the reason is as important a
|
||
lesson as any discussion of optimized assembly code is likely to be.
|
||
Take a moment to examine some interesting performance aspects of the C
|
||
implementation, and all should become much clearer.
|
||
|
||
As you'll recall from Chapter 1, one of the important rules for
|
||
optimization involves knowing when optimization is worth bothering with
|
||
at all. Another rule involves understanding where most of a program's
|
||
execution time is going. That's more true for Listing 5.1 than you might
|
||
think.
|
||
|
||
When Listing 5.1 is run on a 1 MB assembly source file, it takes about
|
||
three seconds to find the string "xxxend" (which is at the end of the
|
||
file) on a 20 MHz 386 machine, with the entire file in a disk cache. If
|
||
`BLOCK_SIZE` is trimmed from 16K to 4K, *execution time does not
|
||
increase perceptibly!* At 2K, the program slows slightly; it's not until
|
||
the block size shrinks to 64 bytes that execution time becomes
|
||
approximately double that of the 16K buffer.
|
||
|
||
So the first thing we've discovered is that, while bigger blocks do make
|
||
for the best performance, the increment in performance may not be very
|
||
large, and might not justify the extra memory required for those larger
|
||
blocks. Our next discovery is that, even though we read the file in
|
||
large chunks, most of the execution time of Listing 5.1 is nonetheless
|
||
spent in executing the `read()` function.
|
||
|
||
When I replaced the `read()` function call in Listing 5.1 with code
|
||
that simply fools the program into thinking that a 1 MB file is being
|
||
read, the program ran almost instantaneously—in less than 1/2 second,
|
||
even when the searched-for string wasn't anywhere to be found. By
|
||
contrast, Listing 5.1 requires three seconds to run even when searching
|
||
for a single character that isn't found anywhere in the file, the case
|
||
in which a single call to `memchr()` (and thus a single `REPNZ
|
||
SCASB`) can eliminate an entire block at a time.
|
||
|
||
All in all, the time required for DOS disk access calls is taking up at
|
||
least 80 percent of execution time, and search time is less than 20
|
||
percent of overall execution time. In fact, search time is probably a
|
||
good deal less than 20 percent of the total, given that the overhead of
|
||
loading the program, running through the C startup code, opening the
|
||
file, executing `printf()`, and exiting the program and returning to
|
||
the DOS shell are also included in my timings. Given which, it should be
|
||
apparent why converting to assembly language isn't worth the trouble—the
|
||
best we could do by speeding up the search is a 10 percent or so
|
||
improvement, and that would require more than doubling the performance
|
||
of code that already uses repeated string instructions to do most of the
|
||
work.
|
||
|
||
Not likely.
|
||
|
||
#### Knowing When Assembly Is Pointless
|
||
|
||
So that's why we're not going to go to assembly language in this
|
||
example—which is not to say it would never be worth converting the
|
||
search engine in Listing 5.1 to assembly.
|
||
|
||
If, for example, your application will typically search buffers in which
|
||
the first character of the search string occurs frequently as might be
|
||
the case when searching a text buffer for a string starting with the
|
||
space character an assembly implementation might be several times
|
||
faster. Why? Because assembly code can switch from `REPNZ SCASB` to
|
||
match the first character to `REPZ CMPS` to check the remaining
|
||
characters in just a few instructions.
|
||
|
||
In contrast, Listing 5.1 must return from `memchr()`, set up
|
||
parameters, and call `memcmp()` in order to do the same thing.
|
||
Likewise, assembly can switch back to `REPNZ SCASB` after a non-match
|
||
much more quickly than Listing 5.1. The switching overhead is high; when
|
||
searching a file completely filled with the character z for the string
|
||
"zy," Listing 5.1 takes almost 1/2 minute, or nearly an order of
|
||
magnitude longer than when searching a file filled with normal text.
|
||
|
||
It might also be worth converting the search engine to assembly for
|
||
searches performed entirely in memory; with the overhead of file access
|
||
eliminated, improvements in search-engine performance would translate
|
||
directly into significantly faster overall performance. One such
|
||
application that would have much the same structure as Listing 5.1 would
|
||
be searching through expanded memory buffers, and another would be
|
||
searching through huge (segment-spanning) buffers.
|
||
|
||
And so we find, as we so often will, that optimization is definitely not
|
||
a cut-and-dried matter, and that there is no such thing as a single
|
||
"best" approach.
|
||
|
||
> 
|
||
> You must know what your application will typically do, and you must know
|
||
> whether you're more concerned with average or worst-case performance
|
||
> before you can decide how best to speed up your program—and, indeed,
|
||
> whether speeding it up is worth doing at all.
|
||
|
||
By the way, don't think that just because very large block sizes don't
|
||
much improve performance, it wasn't worth using restartable blocks in
|
||
Listing 5.1. Listing 5.1 runs more than three times more slowly with a
|
||
block size of 32 bytes than with a block size of 4K, and any
|
||
byte-by-byte approach would surely be slower still, due to the overhead
|
||
of repeated calls to DOS and/or the C stream I/O library.
|
||
|
||
Restartable blocks do minimize the overhead of DOS file-access calls in
|
||
Listing 5.1; it's just that there's no way to reduce that overhead to
|
||
the point where it becomes worth attempting to further improve the
|
||
performance of our relatively efficient search engine. Although the
|
||
search engine is by no means fully optimized, it's nonetheless as fast
|
||
as there's any reason for it to be, given the balance of performance
|
||
among the components of this program.
|
||
|
||
### Always Look Where Execution Is Going
|
||
|
||
I've explained two important lessons: Know when it's worth optimizing
|
||
further, and use restartable blocks to process large data sets as a
|
||
series of blocks, with each block handled at high speed. The first
|
||
lesson is less obvious than it seems.
|
||
|
||
When I set out to write this chapter, I fully intended to write an
|
||
assembly language version of Listing 5.1, and I expected the assembly
|
||
version to be much faster. When I actually looked at where execution
|
||
time was going (which I did by modifying the program to remove the calls
|
||
to the `read()` function, but a code profiler could be used to do the
|
||
same thing much more easily), I found that the best code in the world
|
||
wouldn't make much difference.
|
||
|
||
> 
|
||
> When you try to speed up code, take a moment to identify the hot spots
|
||
> in your program so that you know where optimization is needed and
|
||
> whether it will make a significant difference before you invest your
|
||
> time.
|
||
|
||
As for restartable blocks: Here we tackled a considerably more complex
|
||
application of restartable blocks than we did in Chapter 1—which turned
|
||
out not to be so difficult after all. Don't let irregularities in the
|
||
programming tasks you tackle, such as strings that span blocks, fluster
|
||
you into settling for easy, general—and slow—solutions. Focus on making
|
||
the inner loop—the code that handles each block—as efficient as
|
||
possible, then structure the rest of your code to support the inner
|
||
loop.
|
||
|
||
Programming with restartable blocks isn't easy, but when speed is an
|
||
issue, using restartable blocks in the right places more than pays for
|
||
itself with greatly improved performance. And when speed is *not* an
|
||
issue, of course, or in code that's not time-critical, you wouldn't
|
||
dream of wasting your time on optimization.
|
||
|
||
Would you?
|