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

110 lines
No EOL
7.6 KiB
Markdown

Chapter 7\
Local Optimization {#Heading1}
-------------------
### Optimizing Halfway between Algorithms and Cycle Counting {#Heading2}
You might not think it, but there's much to learn about performance
programming from the Great Buffalo Sauna Fiasco. To wit:
The scene is Buffalo, New York, in the dead of winter, with the snow
piled several feet deep. Four college students, living in typical
student housing, are frozen to the bone. The third floor of their house,
uninsulated and so cold that it's uninhabitable, has an ancient
bathroom. One fabulously cold day, inspiration strikes:
"Hey—we could make that bathroom into a *sauna!*"
Pandemonium ensues. Someone rushes out and buys a gas heater, and at
considerable risk to life and limb hooks it up to an abandoned but still
live gas pipe that once fed a stove on the third floor. Someone else
gets sheets of plastic and lines the walls of the bathroom to keep the
moisture in, and yet another student gets a bucket full of rocks. The
remaining chap brings up some old wooden chairs and sets them up to make
benches along the sides of the bathroom. *Voila*—instant sauna!
They crank up the gas heater, put the bucket of rocks in front of it,
close the door, take off their clothes, and sit down to steam
themselves. Mind you, it's not yet 50 degrees Fahrenheit in this room,
but the gas heater is roaring. Surely warmer times await.
Indeed they do. The temperature climbs to 55 degrees, then 60, then 63,
then 65, and finally creeps up to 68 degrees.
And there it stops.
68 degrees is warm for an uninsulated third floor in Buffalo in the dead
of winter. Damn warm. It is not, however, particularly warm for a sauna.
Eventually someone acknowledges the obvious and allows that it might
have been a stupid idea after all, and everyone agrees, and they shut
off the heater and leave, each no doubt offering silent thanks that they
had gotten out of this without any incidents requiring major surgery.
And so we see that the best idea in the world can fail for lack of
either proper design or adequate horsepower. The primary cause of the
Great Buffalo Sauna Fiasco was a lack of horsepower; the gas heater was
flat-out undersized. This is analogous to trying to write programs that
incorporate features like bitmapped text and searching of multisegment
buffers without using high-performance assembly language. Any PC
language can perform just about any function you can think
of—eventually. That heater would eventually have heated the room to 110
degrees, too—along about the first of June or so.
The Great Buffalo Sauna Fiasco also suffered from fundamental design
flaws. A more powerful heater would indeed have made the room hotter—and
might well have burned the house down in the process. Likewise, proper
algorithm selection and good design are fundamental to performance. The
extra horsepower a superb assembly language implementation gives a
program is worth bothering with only in the context of a good design.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Assembly language optimization is a small but crucial corner of the PC programming world. Use it sparingly and only within the framework of a good design—but ignore it and you may find various portions of your anatomy out in the cold.*
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
So, drawing fortitude from the knowledge that our quest is a pure and
worthy one, let's resume our exploration of assembly language
instructions with hidden talents and instructions with well-known
talents that are less than they appear to be. In the process, we'll come
to see that there is another, very important optimization level between
the algorithm/design level and the cycle-counting/individual instruction
level. I'll call this middle level *local optimization;* it involves
focusing on optimizing sequences of instructions rather than individual
instructions, all with an eye to implementing designs as efficiently as
possible given the capabilities of the x86 family instruction set.
And yes, in case you're wondering, the above story is indeed true. Was I
there? Let me put it this way: If I were, I'd never admit it!
#### When LOOP Is a Bad Idea {#Heading3}
Let's examine first an instruction that is less than it appears to be:
**LOOP**. There's no mystery about what **LOOP** does; it decrements CX
and branches if CX doesn't decrement to zero. It's so beautifully suited
to the task of counting down loops that any experienced x86 programmer
instinctively stuffs the loop count in CX and reaches for **LOOP** when
setting up a loop. That's fine—**LOOP** does, of course, work as
advertised—but there is one problem:
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *On half of the processors in the x86 family, **LOOP** is slower than **DEC CX** followed by **JNZ**. (Granted, **DEC CX/JNZ** isn't precisely equivalent to **LOOP,** because **DEC** alters the flags and LOOP doesn't, but in most situations they're comparable.)*
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
How can this be? Don't ask me, ask Intel. On the 8088 and 80286,
**LOOP** is indeed faster than **DEC CX/JNZ** by a cycle, and **LOOP**
is generally a little faster still because it's a byte shorter and so
can be fetched faster. On the 386, however, things change; **LOOP** is
two cycles *slower* than **DEC/JNZ,** and the fetch time for one extra
byte on even an uncached 386 generally isn't significant. (Remember that
the 386 fetches four instruction bytes at a pop.) **LOOP** is three
cycles slower than **DEC/JNZ** on the 486, and the 486 executes
instructions in so few cycles that those three cycles mean that
**DEC/JNZ** is nearly *twice* as fast as **LOOP**. Then, too, unlike
**LOOP, DEC** doesn't require that **CX** be used, so the **DEC/JNZ**
solution is both faster and more flexible on the 386 and 486, and on the
Pentium as well. (By the way, all this is not just theory; I've timed
the relative performances of **LOOP** and **DEC CX/JNZ** on a cached
386, and LOOP really is slower.)
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Things are stranger still for **LOOP**'s relative **JCXZ,** which branches if and only if CX is zero. **JCXZ** is faster than **AND CX,CX/JZ** on the 8088 and 80286, and equivalent on the 80386—but is about twice as slow on the 486!*
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------