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

357 lines
17 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: '22'
pages: 413-420
---
## Chapter 22 -- Zenning and the Flexible Mind
### Taking a Spin through What You've Learned
And so we come to the end of our journey; for now, at least. What
follows is a modest bit of optimization, one which originally served to
show readers of *Zen of Assembly Language* that they had learned more
than just bits and pieces of knowledge; that they had also begun to
learn how to apply the flexible mind—unconventional, broadly integrative
thinking—to approaching high-level optimization at the algorithmic and
program design levels. You, of course, need no such reassurance, having
just spent 21 chapters learning about the flexible mind in many guises,
but I think you'll find this example instructive nonetheless. Try to
stay ahead as the level of optimization rises from instruction
elimination to instruction substitution to more creative solutions that
involve broader understanding and redesign. We'll start out by
compacting individual instructions and bits of code, but by the end
we'll come up with a solution that involves the very structure of the
subroutine, with each instruction carefully integrated into a remarkably
compact whole. It's a neat example of how optimization operates at many
levels, some much less determininstic than others—and besides, it's just
plain fun.
Enjoy!
### Zenning
In Jeff Duntemann's excellent book *Borland Pascal From Square One*
(Random House, 1993), there's a small assembly subroutine that's
designed to be called from a Turbo Pascal program in order to fill the
screen or a systemscreen buffer with a specified character/attribute
pair in text mode. This subroutine involves only 21 instructions and
works perfectly well; however, with what we know, we can compact the
subroutine tremendously and speed it up a bit as well. To coin a verb,
we can "Zen" this already-tight assembly code to an astonishing degree.
In the process, I hope you'll get a feel for how advanced your assembly
skills have become.
Jeff's original code follows as Listing 22.1 (with some text converted
to lowercase in order to match the style of this book), but the comments
are mine.
**LISTING 22.1 L22-1.ASM**
```nasm
OnStack struc ;data that's stored on the stack after PUSH BP
OldBP dw ? ;caller's BP
RetAddr dw ? ;return address
Filler dw ? ;character to fill the buffer with
Attrib dw ? ;attribute to fill the buffer with
BufSize dw ? ;number of character/attribute pairs to fill
BufOfs dw ? ;buffer offset
BufSeg dw ? ;buffer segment
EndMrk db ? ;marker for the end of the stack frame
OnStack ends
;
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
cmp word ptr [bp].BufSeg,0 ;skip the fill if a null
jne Start ; pointer is passed
cmp word ptr [bp].BufOfs,0
je Bye
Start: cld ;make STOSW count up
mov ax,[bp].Attrib ;load AX with attribute parameter
and ax,0ff00h ;prepare for merging with fill char
mov bx,[bp].Filler ;load BX with fill char
and bx,0ffh ;prepare for merging with attribute
or ax,bx ;combine attribute and fill char
mov bx,[bp].BufOfs ;load DI with target buffer offset
mov di,bx
mov bx,[bp].BufSeg ;load ES with target buffer segment
mov es,bx
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:mov sp,bp ;restore original stack pointer
pop bp ; and caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
```
The first thing you'll notice about Listing 22.1 is that `ClearS` uses
a `REP STOSW` instruction. That means that we're not going to improve
performance by any great amount, no matter how clever we are. While we
can eliminate some cycles, the bulk of the work in `ClearS` is done by
that one repeated string instruction, and there's no way to improve on
that.
Does that mean that Listing 22.1 is as good as it can be? Hardly. While
the speed of `ClearS` is very good, there's another side to the
optimization equation: size. The whole of `ClearS` is 52 bytes long as
it stands—but, as we'll see, that size is hardly set in stone.
Where do we begin with `ClearS`? For starters, there's an instruction
in there that serves no earthly purpose—`MOV SP,BP`. SP is guaranteed
to be equal to BP at that point anyway, so why reload it with the same
value? Removing that instruction saves us two bytes.
Well, that was certainly easy enough! We're not going to find any more
totally non-functional instructions in `ClearS`, however, so let's get
on to some serious optimizing. We'll look first for cases where we know
of better instructions for particular tasks than those that were chosen.
For example, there's no need to load any register, whether segment or
general, through BX; we can eliminate two instructions by loading ES and
DI directly as shown in Listing 22.2.
**LISTING 22.2 L22-2.ASM**
```nasm
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
cmp word ptr [bp].BufSeg,0 ;skip the fill if a null
jne Start ; pointer is passed
cmp word ptr [bp].BufOfs,0
je Bye
Start: cld ;make STOSW count up
mov ax,[bp].Attrib ;load AX with attribute parameter
and ax,0ff00h ;prepare for merging with fill char
mov bx,[bp].Filler ;load BX with fill char
and bx,0ffh ;prepare for merging with attribute
or ax,bx ;combine attribute and fill char
mov di,[bp].BufOfs ;load DI with target buffer offset
mov es,[bp].BufSeg ;load ES with target buffer segment
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:
pop bp ;restore caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
```
(The `OnStack` structure definition doesn't change in any of our
examples, so I'm not going clutter up this chapter by reproducing it for
each new version of `ClearS`.)
Okay, loading ES and DI directly saves another four bytes. We've
squeezed a total of 6 bytes—about 11 percent—out of `ClearS`. What
next?
Well, `LES` would serve better than two `MOV` instructions for
loading ES and DI as shown in Listing 22.3.
**LISTING 22.3 L22-3.ASM**
```nasm
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
cmp word ptr [bp].BufSeg,0 ;skip the fill if a null
jne Start ; pointer is passed
cmp word ptr [bp].BufOfs,0
je Bye
Start: cld ;make STOSW count up
mov ax,[bp].Attrib ;load AX with attribute parameter
and ax,0ff00h ;prepare for merging with fill char
mov bx,[bp].Filler ;load BX with fill char
and bx,0ffh ;prepare for merging with attribute
or ax,bx ;combine attribute and fill char
les di,dword ptr [bp].BufOfs ;load ES:DI with target buffer
;segment:offset
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:
pop bp ;restore caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
```
That's good for another three bytes. We're down to 43 bytes, and
counting.
We can save 3 more bytes by clearing the low and high bytes of AX and
BX, respectively, by using `SUB *reg8,reg8*` rather than ANDing 16-bit
values as shown in Listing 22.4.
**LISTING 22.4 L22-4.ASM**
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
cmp word ptr [bp].BufSeg,0 ;skip the fill if a null
jne Start ; pointer is passed
cmp word ptr [bp].BufOfs,0
je Bye
Start: cld ;make STOSW count up
mov ax,[bp].Attrib ;load AX with attribute parameter
sub al,al ;prepare for merging with fill char
mov bx,[bp].Filler ;load BX with fill char
sub bh,bh ;prepare for merging with attribute
or ax,bx ;combine attribute and fill char
les di,dword ptr [bp].BufOfs ;load ES:DI with target buffer
;segment:offset
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:
pop bp ;restore caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
Now we're down to 40 bytes—more than 20 percent smaller than the
original code. That's pretty much it for simple instruction
optimizations. Now let's look for instruction optimizations.
It seems strange to load a word value into AX and then throw away AL.
Likewise, it seems strange to load a word value into BX and then throw
away BH. However, those steps are necessary because the two modified
word values are ORed into a single character/attribute word value that
is then used to fill the target buffer.
Let's step back and see what this code really *does*, though. All it
does in the end is load one byte addressed relative to BP into AH and
another byte addressed relative to BP into AL. Heck, we can just do that
directly! Presto—we've saved another 6 bytes, and turned two word-sized
memory accesses into byte-sized memory accesses as well. Listing 22.5
shows the new code.
**LISTING 22.5 L22-5.ASM**
```nasm
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
cmp word ptr [bp].BufSeg,0 ;skip the fill if a null
jne Start ; pointer is passed
cmp word ptr [bp].BufOfs,0
je Bye
Start: cld ;make STOSW count up
mov ah,byte ptr [bp].Attrib[1];load AH with attribute
mov al,byte ptr [bp].Filler ;load AL with fill char
les di,dword ptr [bp].BufOfs ;load ES:DI with target buffer segment:offset
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:
pop bp ;restore caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
```
(We could get rid of yet another instruction by having the calling code
pack both the attribute and the fill value into the same word, but
that's not part of the specification for this particular routine.)
Another nifty instruction-rearrangement trick saves 6 more bytes.
`ClearS` checks to see whether the far pointer is null (zero) at the
start of the routine...then loads and uses that same far pointer later
on. Let's get that pointer into registers and keep it there; that way we
can check to see whether it's null with a single comparison, and can use
it later without having to reload it from memory. This technique is
shown in Listing 22.6.
**LISTING 22.6 L22-6.ASM**
```nasm
ClearS proc near
push bp ;save caller's BP
mov bp,sp ;point to stack frame
les di,dword ptr [bp].BufOfs ;load ES:DI with target buffer;segment:offset
mov ax,es ;put segment where we can test it
or ax,di ;is it a null pointer?
je Bye ;yes, so we're done
Start: cld ;make STOSW count up
mov ah,byte ptr [bp].Attrib[1];load AH with attribute
mov al,byte ptr [bp].Filler ;load AL with fill char
mov cx,[bp].BufSize ;load CX with buffer size
rep stosw ;fill the buffer
Bye:
pop bp ;restore caller's BP
ret EndMrk-RetAddr-2 ;return, clearing the parms from the stack
ClearS endp
```
Well. Now we're down to 28 bytes, having reduced the size of this
subroutine by nearly 50 percent. Only 13 instructions remain.
Realistically, how much smaller can we make this code?
About one-third smaller yet, as it turns out—but in order to do that, we
must stretch our minds and use the 8088's instructions in unusual ways.
Let me ask you this: What do most of the instructions in the current
version of `ClearS` do?
They either load parameters from the stack frame or set up the registers
so that the parameters can be accessed. Mind you, there's nothing wrong
with the stack-frame-oriented instructions used in `ClearS`; those
instructions access the stack frame in a highly efficient way, exactly
as the designers of the 8088 intended, and just as the code generated by
a high-level language would. That means that we aren't going to be able
to improve the code if we don't bend the rules a bit.
Let's think...the parameters are sitting on the stack, and most of our
instruction bytes are being used to read bytes off the stack with
BP-based addressing...we need a more efficient way to address the
stack...*the stack*...THE STACK!
Ye gods! That's easy—we can use the *stack pointer* to address the stack
rather than BP. While it's true that the stack pointer can't be used for
*mod-reg-rm* addressing, as BP can, it *can* be used to pop data off the
stack—and `POP` is a one-byte instruction. Instructions don't get any
shorter than that.
There is one detail to be taken care of before we can put our plan into
action: The return address—the address of the calling code—is on top of
the stack, so the parameters we want can't be reached with `POP`.
That's easily solved, however—we'll just pop the return address into an
unused register, then branch through that register when we're done, as
we learned to do in Chapter 14. As we pop the parameters, we'll also be
removing them from the stack, thereby neatly avoiding the need to
discard them when it's time to return.
With that problem dealt with, Listing 22.7 shows the Zenned version of
`ClearS`.
**LISTING 22.7 L22-7.ASM**
```nasm
ClearS procnear
pop dx ;get the return address
pop ax ;put fill char into AL
pop bx ;get the attribute
mov ah,bh ;put attribute into AH
pop cx ;get the buffer size
pop di ;get the offset of the buffer origin
pop es ;get the segment of the buffer origin
mov bx,es ;put the segment where we can test it
or bx,di ;null pointer?
je Bye ;yes, so we're done
cld ;make STOSW count up
rep stosw ;do the string store
Bye:
jmp dx ;return to the calling code
ClearS endp
```
At long last, we're down to the bare metal. This version of `ClearS`
is just 19 bytes long. That's just 37 percent as long as the original
version, *without any change whatsoever in the functionality that
`ClearS` makes available to the calling code*. The code is bound to
run a bit faster too, given that there are far fewer instruction bytes
and fewer memory accesses.
All in all, the Zenned version of `ClearS` is a vast improvement over
the original. Probably not the best possible implementation—*never say
never!*—but an awfully good one.