abrash-black-book/chapter-02.md
2014-01-06 23:29:31 +11:00

329 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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: '02'
pages: 021-030
---
## Chapter 2 -- A World Apart
### The Unique Nature of Assembly Language Optimization
As I showed in the previous chapter, optimization is by no means always
a matter of "dropping into assembly." In fact, in performance tuning
high-level language code, assembly should be used rarely, and then only
after you've made sure a badly chosen or clumsily implemented algorithm
isn't eating you alive. Certainly if you use assembly at all, make
absolutely sure you use it *right*. The potential of assembly code to
run *slowly* is poorly understood by a lot of people, but that potential
is great, especially in the hands of the ignorant.
Truly great optimization, however, happens *only* at the assembly level,
and it happens in response to a set of dynamics that is totally
different from that governing C/C++ or Pascal optimization. I'll be
speaking of assembly-level optimization time and again in this book, but
when I do, I think it will be helpful if you have a grasp of those
assembly specific dynamics.
As usual, the best way to wade in is to present a real-world example.
### Instructions: The Individual versus the Collective
Some time ago, I was asked to work over a critical assembly subroutine
in order to make it run as fast as possible. The task of the subroutine
was to construct a nibble out of four bits read from different bytes,
rotating and combining the bits so that they ultimately ended up neatly
aligned in bits 3-0 of a single byte. (In case you're curious, the
object was to construct a 16-color pixel from bits scattered over 4
bytes.) I examined the subroutine line by line, saving a cycle here and
a cycle there, until the code truly seemed to be optimized. When I was
done, the key part of the code looked something like this:
```nasm
LoopTop:
lodsb ;get the next byte to extract a bit from
and al,ah ;isolate the bit we want
rol al,cl ;rotate the bit into the desired position
or bl,al ;insert the bit into the final nibble
dec cx ;the next bit goes 1 place to the right
dec dx ;count down the number of bits
jnz LoopTop ;process the next bit, if any
```
Now, it's hard to write code that's much faster than seven instructions,
only one of which accesses memory, and most programmers would have
called it a day at this point. Still, something bothered me, so I spent
a bit of time going over the code again. Suddenly, the answer struck
me—the code was rotating each bit into place separately, so that a
multibit rotation was being performed every time through the loop, for a
total of four separate time-consuming multibit rotations!
> ![](images/i.jpg)
> While the instructions themselves were individually optimized, the
> overall approach did not make the best possible use of the instructions.
I changed the code to the following:
```nasm
LoopTop:
lodsb ;get the next byte to extract a bit from
and al,ah ;isolate the bit we want
or bl,al ;insert the bit into the final nibble
rol bl,1 ;make room for the next bit
dec dx ;count down the number of bits
jnz LoopTop ;process the next bit, if any
rol bl,cl ;rotate all four bits into their final
; positions at the same time
```
This moved the costly multibit rotation out of the loop so that it was
performed just once, rather than four times. While the code may not look
much different from the original, and in fact still contains exactly the
same number of instructions, the performance of the entire subroutine
improved by about 10 percent from just this one change. (Incidentally,
that wasn't the end of the optimization; I eliminated the `DEC` and
`JNJ` instructions by expanding the four iterations of the loop—but
that's a tale for another chapter.)
The point is this: To write truly superior assembly programs, you need
to know what the various instructions do and which instructions execute
fastest...and more. You must also learn to look at your programming
problems from a variety of perspectives so that you can put those fast
instructions to work in the most effective ways.
### Assembly Is Fundamentally Different
Is it really so hard as all that to write good assembly code for the PC?
Yes! Thanks to the decidedly quirky nature of the x86 family CPUs,
assembly language differs fundamentally from other languages, and is
undeniably harder to work with. On the other hand, the potential of
assembly code is much greater than that of other languages, as well.
To understand why this is so, consider how a program gets written. A
programmer examines the requirements of an application, designs a
solution at some level of abstraction, and then makes that design come
alive in a code implementation. If not handled properly, the
transformation that takes place between conception and implementation
can reduce performance tremendously; for example, a programmer who
implements a routine to search a list of 100,000 sorted items with a
linear rather than binary search will end up with a disappointingly slow
program.
#### Transformation Inefficiencies
No matter how well an implementation is derived from the corresponding
design, however, high-level languages like C/C++ and Pascal inevitably
introduce additional transformation inefficiencies, as shown in Figure
2.1.
The process of turning a design into executable code by way of a
high-level language involves two transformations: one performed by the
programmer to generate source code, and another performed by the
compiler to turn source code into machine language instructions.
Consequently, the machine language code generated by compilers is
usually less than optimal given the requirements of the original design.
High-level languages provide artificial environments that lend
themselves relatively well to human programming skills, in order to ease
the transition from design to implementation. The price for this ease of
implementation is a considerable loss of efficiency in transforming
source code into machine language. This is particularly true given that
the x86 family in real and 16-bit protected mode, with its specialized
memory-addressing instructions and segmented memory architecture, does
not lend itself particularly well to compiler design. Even the 32-bit
mode of the 386 and its successors, with their more powerful addressing
modes, offer fewer registers than compilers would like.
![**Figure 2.1** *The high-level language transformation
inefficiencies.*](images/02-01.jpg)
Assembly, on the other hand, is simply a human-oriented representation
of machine language. As a result, assembly provides a difficult
programming environment—the bare hardware and systems software of the
computer—*but properly constructed assembly programs suffer no
transformation loss*, as shown in Figure 2.2.
Only one transformation is required when creating an assembler program,
and that single transformation is completely under the programmer's
control. Assemblers perform no transformation from source code to
machine language; instead, they merely map assembler instructions to
machine language instructions on a one-to-one basis. As a result, the
programmer is able to produce machine language code that's precisely
tailored to the needs of each task a given application requires.
![**Figure 2.2**  *Properly constructed assembly programs suffer no
transformation loss.*](images/02-02.jpg)
The key, of course, is the programmer, since in assembly the programmer
must essentially perform the transformation from the application
specification to machine language entirely on his or her own. (The
assembler merely handles the *direct* translation from assembly to
machine language.)
#### Self-Reliance
The first part of assembly language optimization, then, is self. An
assembler is nothing more than a tool to let you design machine-language
programs without having to think in hexadecimal codes. So assembly
language programmers—unlike all other programmers—must take full
responsibility for the quality of their code. Since assemblers provide
little help at any level higher than the generation of machine language,
the assembly programmer must be capable both of coding any programming
construct directly and of controlling the PC at the lowest practical
level—the operating system, the BIOS, even the hardware where necessary.
High-level languages handle most of this transparently to the
programmer, but in assembly everything is fair—and necessary—game, which
brings us to another aspect of assembly optimization: knowledge.
#### Knowledge
In the PC world, you can never have enough knowledge, and every item you
add to your store will make your programs better. Thorough familiarity
with both the operating system APIs and BIOS interfaces is important;
since those interfaces are well-documented and reasonably
straightforward, my advice is to get a good book or two and bring
yourself up to speed. Similarly, familiarity with the PC hardware is
required. While that topic covers a lot of ground—display adapters,
keyboards, serial ports, printer ports, timer and DMA channels, memory
organization, and more—most of the hardware is well-documented, and
articles about programming major hardware components appear frequently
in the literature, so this sort of knowledge can be acquired readily
enough.
The single most critical aspect of the hardware, and the one about which
it is hardest to learn, is the CPU. The x86 family CPUs have a complex,
irregular instruction set, and, unlike most processors, they are neither
straightforward nor wellregarding true code performance. What's more,
assembly is so difficult to learn that most articles and books that
present assembly code settle for code that just works, rather than code
that pushes the CPU to its limits. In fact, since most articles and
books are written for inexperienced assembly programmers, there is very
little information of any sort available about how to generate
high-quality assembly code for the x86 family CPUs. As a result,
knowledge about programming them effectively is by far the hardest
knowledge to gather. A good portion of this book is devoted to seeking
out such knowledge.
> ![](images/i.jpg)
> Be forewarned, though: No matter how much you learn about programming
> the PC in assembly, there's always more to discover.
### The Flexible Mind
Is the never-ending collection of information all there is to the
assembly optimization, then? Hardly. Knowledge is simply a necessary
base on which to build. Let's take a moment to examine the objectives of
good assembly programming, and the remainder of the forces that act on
assembly optimization will fall into place.
Basically, there are only two possible objectives to high-performance
assembly programming: Given the requirements of the application, keep to
a minimum either the number of processor cycles the program takes to
run, or the number of bytes in the program, or some combination of both.
We'll look at ways to achieve both objectives, but we'll more often be
concerned with saving cycles than saving bytes, for the PC generally
offers relatively more memory than it does processing horsepower. In
fact, we'll find that two-to-three times performance improvements *over
already tight assembly code* are often possible if we're willing to
spend additional bytes in order to save cycles. It's not always
desirable to use such techniques to speed up code, due to the heavy
memory requirements—but it is almost always *possible*.
You will notice that my short list of objectives for high-performance
assembly programming does not include traditional objectives such as
easy maintenance and speed of development. Those are indeed important
considerations—to persons and companies that develop and distribute
software. People who actually *buy* software, on the other hand, care
only about how well that software performs, not how it was developed nor
how it is maintained. These days, developers spend so much time focusing
on such admittedly important issues as code maintainability and
reusability, source code control, choice of development environment, and
the like that they often forget rule \#1: From the user's perspective,
*performance is fundamental*.
> ![](images/i.jpg)
> Comment your code, design it carefully, and write non-time-critical
> portions in a high-level language, if you wish—but when you write the
> portions that interact with the user and/or affect response time,
> performance must be your paramount objective, and assembly is the path
> to that goal.
Knowledge of the sort described earlier is absolutely essential to
fulfilling either of the objectives of assembly programming. What that
knowledge doesn't do by itself is meet the need to write code that both
performs to the requirements of the application at hand and also
operates as efficiently as possible in the PC environment. Knowledge
makes that possible, but your programming instincts make it happen. And
it is that intuitive, on-the-fly integration of a program specification
and a sea of facts about the PC that is the heart of the Zen-class
assembly optimization.
As with Zen of any sort, mastering that Zen of assembly language is more
a matter of learning than of being taught. You will have to find your
own path of learning, although I will start you on your way with this
book. The subtle facts and examples I provide will help you gain the
necessary experience, but you must continue the journey on your own.
Each program you create will expand your programming horizons and
increase the options available to you in meeting the next challenge. The
ability of your mind to find surprising new and better ways to craft
superior code from a concept—the flexible mind, if you will—is the
linchpin of good assembler code, and you will develop this skill only by
doing.
Never underestimate the importance of the flexible mind. Good assembly
code is better than good compiled code. Many people would have you
believe otherwise, but they're wrong. That doesn't mean that high-level
languages are useless; far from it. High-level languages are the best
choice for the majority of programmers, and for the bulk of the code of
most applications. When the *best* code—the fastest or smallest code
possible—is needed, though, assembly is the only way to go.
Simple logic dictates that no compiler can know as much about what a
piece of code needs to do or adapt as well to those needs as the person
who wrote the code. Given that superior information and adaptability, an
assembly language programmer can generate better code than a compiler,
all the more so given that compilers are constrained by the limitations
of high-level languages and by the process of transformation from
high-level to machine language. Consequently, carefully optimized
assembly is not just the language of choice but the *only* choice for
the 1percent to 10 percent of code—usually consisting of small,
well-defined subroutines—that determines overall program performance,
and it is the only choice for code that must be as compact as possible,
as well. In the run-of-the-mill, non-time-critical portions of your
programs, it makes no sense to waste time and effort on writing
optimized assembly code—concentrate your efforts on loops and the like
instead; but in those areas where you need the finest code quality,
accept no substitutes.
Note that I said that an assembly programmer *can* generate better code
than a compiler, not *will* generate better code. While it is true that
good assembly code is better than good compiled code, it is also true
that bad assembly code is often much worse than bad compiled code; since
the assembly programmer has so much control over the program, he or she
has virtually unlimited opportunities to waste cycles and bytes. The
sword cuts both ways, and good assembly code requires more, not less,
forethought and planning than good code written in a high-level
language.
The gist of all this is simply that good assembly programming is done in
the context of a solid overall framework unique to each program, and the
flexible mind is the key to creating that framework and holding it
together.
#### Where to Begin?
To summarize, the skill of assembly language optimization is a
combination of knowledge, perspective, and a way of thought that makes
possible the genesis of absolutely the fastest or the smallest code.
With that in mind, what should the first step be? Development of the
flexible mind is an obvious step. Still, the flexible mind is no better
than the knowledge at its disposal. The first step in the journey toward
mastering optimization at that exalted level, then, would seem to be
learning how to learn.