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

2267 lines
85 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.

This file contains Unicode characters that might be confused with other characters. 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: '03'
pages: 031-073
---
## Chapter 3 -- Assume Nothing
### Understanding and Using the Zen Timer
When you're pushing the envelope in writing optimized PC code, you're
likely to become more than a little compulsive about finding approaches
that let you wring more speed from your computer. In the process, you're
bound to make mistakes, which is fine—as long as you watch for those
mistakes and *learn* from them.
A case in point: A few years back, I came across an article about 8088
assembly language called "Optimizing for Speed." Now, "optimize" is not
a word to be used lightly; *Webster's Ninth New Collegiate Dictionary*
defines optimize as "to make as perfect, effective, or functional as
possible," which certainly leaves little room for error. The author had,
however, chosen a small, well-defined 8088 assembly language routine to
refine, consisting of about 30 instructions that did nothing more than
expand 8 bits to 16 bits by duplicating each bit.
The author of "Optimizing" had clearly fine-tuned the code with care,
examining alternative instruction sequences and adding up cycles until
he arrived at an implementation he calculated to be nearly 50 percent
faster than the original routine. In short, he had used all the
information at his disposal to improve his code, and had, as a result,
saved cycles by the bushel. There was, in fact, only one slight problem
with the optimized version of the routine....
It ran slower than the original version!
### The Costs of Ignorance
As diligent as the author had been, he had nonetheless committed a
cardinal sin of x86 assembly language programming: He had assumed that
the information available to him was both correct and complete. While
the execution times provided by Intel for its processors are indeed
correct, they are incomplete; the other—and often more important—part of
code performance is instruction *fetch* time, a topic to which I will
return in later chapters.
Had the author taken the time to measure the true performance of his
code, he wouldn't have put his reputation on the line with relatively
low-performance code. What's more, had he actually measured the
performance of his code and found it to be unexpectedly slow, curiosity
might well have led him to experiment further and thereby add to his
store of reliable information about the CPU.
> ![](images/i.jpg)
> There you have an important tenet of assembly language optimization:
> After crafting the best code possible, check it in action to see if it's
> really doing what you think it is. If it's not behaving as expected,
> that's all to the good, since solving mysteries is the path to
> knowledge. You'll learn more in this way, I assure you, than from any
> manual or book on assembly language.
*Assume nothing*. I cannot emphasize this strongly enough—when you care
about performance, do your best to improve the code and then *measure*
the improvement. If you don't measure performance, you're just guessing,
and if you're guessing, you're not very likely to write top-notch code.
Ignorance about true performance can be costly. When I wrote video games
for a living, I spent days at a time trying to wring more performance
from my graphics drivers. I rewrote whole sections of code just to save
a few cycles, juggled registers, and relied heavily on blurry-fast
register-to-register shifts and adds. As I was writing my last game, I
discovered that the program ran perceptibly faster if I used look-up
tables instead of shifts and adds for my calculations. It *shouldn't*
have run faster, according to my cycle counting, but it did. In truth,
instruction fetching was rearing its head again, as it often does, and
the fetching of the shifts and adds was taking as much as four times the
nominal execution time of those instructions.
Ignorance can also be responsible for considerable wasted effort. I
recall a debate in the letters column of one computer magazine about
exactly how quickly text can be drawn on a Color/Graphics Adapter (CGA)
screen without causing snow. The letter-writers counted every cycle in
their timing loops, just as the author in the story that started this
chapter had. Like that author, the letter-writers had failed to take the
prefetch queue into account. In fact, they had neglected the effects of
video wait states as well, so the code they discussed was actually
*much* slower than their estimates. The proper test would, of course,
have been to run the code to see if snow resulted, since the only true
measure of code performance is observing it in action.
### The Zen Timer
Clearly, one key to mastering Zen-class optimization is a tool with
which to measure code performance. The most accurate way to measure
performance is with expensive hardware, but reasonable measurements at
no cost can be made with the PC's 8253 timer chip, which counts at a
rate of slightly over 1,000,000 times per second. The 8253 can be
started at the beginning of a block of code of interest and stopped at
the end of that code, with the resulting count indicating how long the
code took to execute with an accuracy of about 1 microsecond. (A
microsecond is one millionth of a second, and is abbreviated µs). To be
precise, the 8253 counts once every 838.1 nanoseconds. (A nanosecond is
one billionth of a second, and is abbreviated ns.)
Listing 3.1 shows 8253-based timer software, consisting of three
subroutines: `ZTimerOn`, `ZTimerOff`, and `ZTimerReport`. For the
remainder of this book, I'll refer to these routines collectively as the
"Zen timer." C-callable versions of the two precision Zen timers are
presented in Chapter K on the companion CD-ROM.
**LISTING 3.1 PZTIMER.ASM**
```nasm
; The precision Zen timer (PZTIMER.ASM)
;
; Uses the 8253 timer to time the performance of code that takes
; less than about 54 milliseconds to execute, with a resolution
; of better than 10 microseconds.
;
; By Michael Abrash
;
; Externally callable routines:
;
; ZTimerOn: Starts the Zen timer, with interrupts disabled.
;
; ZTimerOff: Stops the Zen timer, saves the timer count,
; times the overhead code, and restores interrupts to the
; state they were in when ZTimerOn was called.
;
; ZTimerReport: Prints the net time that passed between starting
; and stopping the timer.
;
; Note: If longer than about 54 ms passes between ZTimerOn and
; ZTimerOff calls, the timer turns over and the count is
; inaccurate. When this happens, an error message is displayed
; instead of a count. The long-period Zen timer should be used
; in such cases.
;
; Note: Interrupts *MUST* be left off between calls to ZTimerOn
; and ZTimerOff for accurate timing and for detection of
; timer overflow.
;
; Note: These routines can introduce slight inaccuracies into the
; system clock count for each code section timed even if
; timer 0 doesn't overflow. If timer 0 does overflow, the
; system clock can become slow by virtually any amount of
; time, since the system clock can't advance while the
; precison timer is timing. Consequently, it's a good idea
; to reboot at the end of each timing session. (The
; battery-backed clock, if any, is not affected by the Zen
; timer.)
;
; All registers, and all flags except the interrupt flag, are
; preserved by all routines. Interrupts are enabled and then disabled
; by ZTimerOn, and are restored by ZTimerOff to the state they were
; in when ZTimerOn was called.
;
Code segment word public CODE'
assumecs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport
;
; Base address of the 8253 timer chip.
;
BASE_8253equ40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253 equBASE_8253 + 0
;
; The address of the mode register in the 8253.
;
MODE_8253 equBASE_8253 + 3
;
; The address of Operation Command Word 3 in the 8259 Programmable
; Interrupt Controller (PIC) (write only, and writable only when
; bit 4 of the byte written to this address is 0 and bit 3 is 1).
;
OCW3 equ20h
;
; The address of the Interrupt Request register in the 8259 PIC
; (read only, and readable only when bit 1 of OCW3 = 1 and bit 0
; of OCW3 = 0).
;
IRR equ20h
;
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts remain disabled.
;
MPOPF macro
local p1, p2
jmp short p2
p1: iret ; jump to pushed address & pop flags
p2: push cs ; construct far return address to
call p1 ; the next instruction
endm
;
; Macro to delay briefly to ensure that enough time has elapsed
; between successive I/O accesses so that the device being accessed
; can respond to both accesses even on a very fast PC.
;
DELAY macro
jmp $+2
jmp $+2
jmp $+2
endm
OriginalFlags db ? ; storage for upper byte of
; FLAGS register when
; ZTimerOn called
TimedCount dw ? ; timer 0 count when the timer
; is stopped
ReferenceCount dw ; number of counts required to
; execute timer overhead code
OverflowFlag db ? ; used to indicate whether the
; timer overflowed during the
; timing interval
;
; String printed to report results.
;
OutputStr label byte
db 0dh, 0ah, Timed count: , 5 dup (?)
ASCIICountEnd labelbyte
db microseconds', 0dh, 0ah
db $'
;
; String printed to report timer overflow.
;
OverflowStr label byte
db 0dh, 0ah
db ****************************************************'
db 0dh, 0ah
db * The timer overflowed, so the interval timed was *'
db 0dh, 0ah
db * too long for the precision timer to measure. *'
db 0dh, 0ah
db * Please perform the timing test again with the *'
db0dh, 0ah
db * long-period timer. *'
db 0dh, 0ah
db ****************************************************'
db 0dh, 0ah
db $'
; ********************************************************************
; * Routine called to start timing. *
; ********************************************************************
ZTimerOn proc near
;
; Save the context of the program being timed.
;
push ax
pushf
pop ax ; get flags so we can keep
; interrupts off when leaving
; this routine
mov cs:[OriginalFlags],ah ; remember the state of the
; Interrupt flag
and ah,0fdh ; set pushed interrupt flag
; to 0
push ax
;
; Turn on interrupts, so the timer interrupt can occur if it's
; pending.
;
sti
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also
; leaves the 8253 waiting for the initial timer 0 count to
; be loaded.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0, so we know we won't get another
; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed.
;
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
out TIMER_0_8253,al ;msb
;
; Wait before clearing interrupts to allow the interrupt generated
; when switching from mode 3 to mode 2 to be recognized. The delay
; must be at least 210 ns long to allow time for that interrupt to
; occur. Here, 10 jumps are used for the delay to ensure that the
; delay time will be more than long enough even on a very fast PC.
;
rept 10
jmp $+2
endm
;
; Disable interrupts to get an accurate count.
;
cli
;
; Set the timer count to 0 again to start the timing interval.
;
mov al,00110100b ; set up to load initial
out MODE_8253,al ; timer count
DELAY
sub al,al
out TIMER_0_8253,al ; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context and return.
;
MPOPF ; keeps interrupts off
pop ax
ret
ZTimerOn endp
;********************************************************************
;* Routine called to stop timing and get count. *
;********************************************************************
ZTimerOff proc near
;
; Save the context of the program being timed.
;
push ax
push cx
pushf
;
; Latch the count.
;
mov al,00000000b ; latch timer 0
out MODE_8253,al
;
; See if the timer has overflowed by checking the 8259 for a pending
; timer interrupt.
;
mov al,00001010b ; OCW3, set up to read
out OCW3,al; Int errupt Request register
DELAY
ina l,IRR; read Interrupt Request
; register
and al,1 ; set AL to 1 if IRQ0 (the
; timer interrupt) is pending
mov cs:[OverflowFlag],al; store the timer overflow
; status
;
; Allow interrupts to happen again.
;
sti
;
; Read out the count we latched earlier.
;
in al,TIMER_0_8253 ; least significant byte
DELAY
mov ah,al
in al,TIMER_0_8253 ; most significant byte
xchg ah,al
neg ax ; convert from countdown
; remaining to elapsed
; count
mov cs:[TimedCount],ax
; Time a zero-length code fragment, to get a reference for how
; much overhead this routine has. Time it 16 times and average it,
; for accuracy, rounding the result.
;
mov cs:[ReferenceCount],0
mov cx,16
cli ; interrupts off to allow a
; precise reference count
RefLoop:
call ReferenceZTimerOn
call ReferenceZTimerOff
loop RefLoop
sti
add cs:[ReferenceCount],8; total + (0.5 * 16)
mov cl,4
shr cs:[ReferenceCount],cl; (total) / 16 + 0.5
;
; Restore original interrupt state.
;
pop ax ; retrieve flags when called
mov ch,cs:[OriginalFlags] ; get back the original upper
; byte of the FLAGS register
and ch,not 0fdh ; only care about original
; interrupt flag...
and ah,0fdh ; ...keep all other flags in
; their current condition
or ah,ch ; make flags word with original
; interrupt flag
push ax ; prepare flags to be popped
;
; Restore the context of the program being timed and return to it.
;
MPOPF ; restore the flags with the
; original interrupt state
pop cx
pop ax
ret
ZTimerOff endp
;
; Called by ZTimerOff to start timer for overhead measurements.
;
ReferenceZTimerOnproc near
;
; Save the context of the program being timed.
;
push ax
pushf ; interrupts are already off
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
;
mov al,00110100b ; set up to load
out MODE_8253,al ; initial timer count
DELAY
;
; Set the timer count to 0.
;
sub al,al
out TIMER_0_8253,al; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
pop ax
ret
ReferenceZTimerOnendp
;
; Called by ZTimerOff to stop timer and add result to ReferenceCount
; for overhead measurements.
;
ReferenceZTimerOff proc near
;
; Save the context of the program being timed.
;
push ax
push cx
pushf
;
; Latch the count and read it.
;
mov al,00000000b ; latch timer 0
out MODE_8253,al
DELAY
in al,TIMER_0_8253 ; lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ; msb
xchg ah,al
neg ax ; convert from countdown
; remaining to amount
; counted down
add cs:[ReferenceCount],ax
;
; Restore the context of the program being timed and return to it.
;
MPOPF
pop cx
pop ax
ret
ReferenceZTimerOff endp
; ********************************************************************
; * Routine called to report timing results. *
; ********************************************************************
ZTimerReport procnear
pushf
push ax
push bx
push cx
push dx
push si
push ds
;
push cs ; DOS functions require that DS point
pop ds ; to text to be displayed on the screen
assume ds :Code
;
; Check for timer 0 overflow.
;
cmp [OverflowFlag],0
jz PrintGoodCount
mov dx,offset OverflowStr
mov ah,9
int 21h
jmp short EndZTimerReport
;
; Convert net count to decimal ASCII in microseconds.
;
PrintGoodCount:
mov ax,[TimedCount]
sub ax,[ReferenceCount]
mov si,offset ASCIICountEnd - 1
;
; Convert count to microseconds by multiplying by .8381.
;
mov dx, 8381
mul dx
mov bx, 10000
div bx ;* .8381 = * 8381 / 10000
;
; Convert time in microseconds to 5 decimal ASCII digits.
;
mov bx, 10
mov cx, 5
CTSLoop:
sub dx, dx
div bx
add dl,'0'
mov [si],dl
dec si
loop CTSLoop
;
; Print the results.
;
mov ah, 9
mov dx, offset OutputStr
int 21h
;
EndZTimerReport:
pop ds
pop si
pop dx
pop cx
pop bx
pop ax
MPOPF
ret
ZTimerReport endp
Code ends
end
```
#### The Zen Timer Is a Means, Not an End
We're going to spend the rest of this chapter seeing what the Zen timer
can do, examining how it works, and learning how to use it. I'll be
using the Zen timer again and again over the course of this book, so
it's essential that you learn what the Zen timer can do and how to use
it. On the other hand, it is by no means essential that you understand
exactly how the Zen timer works. (Interesting, yes; essential, no.)
In other words, the Zen timer isn't really part of the knowledge we
seek; rather, it's one tool with which we'll acquire that knowledge.
Consequently, you shouldn't worry if you don't fully grasp the inner
workings of the Zen timer. Instead, focus on learning how to *use* it,
and you'll be on the right road.
#### Starting the Zen Timer
`ZTimerOn` is called at the start of a segment of code to be timed.
`ZTimerOn` saves the context of the calling code, disables interrupts,
sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial
timer count to 0, restores the context of the calling code, and returns.
(I'd like to note that while Intel's documentation for the 8253 seems to
indicate that a timer won't reset to 0 until it finishes counting down,
in actual practice, timers seem to reset to 0 as soon as they're
loaded.)
Two aspects of `ZTimerOn` are worth discussing further. One point of
interest is that `ZTimerOn` disables interrupts. (`ZTimerOff` later
restores interrupts to the state they were in when `ZTimerOn` was
called.) Were interrupts not disabled by `ZTimerOn`, keyboard, mouse,
timer, and other interrupts could occur during the timing interval, and
the time required to service those interrupts would incorrectly and
erratically appear to be part of the execution time of the code being
measured. As a result, code timed with the Zen timer should not expect
any hardware interrupts to occur during the interval between any call to
`ZTimerOn` and the corresponding call to `ZTimerOff`, and should not
enable interrupts during that time.
### Time and the PC
A second interesting point about `ZTimerOn` is that it may introduce
some small inaccuracy into the system clock time whenever it is called.
To understand why this is so, we need to examine the way in which both
the 8253 and the PC's system clock (which keeps the current time) work.
The 8253 actually contains three timers, as shown in Figure 3.1. All
three timers are driven by the system board's 14.31818 MHz crystal,
divided by 12 to yield a 1.19318 MHz clock to the timers, so the timers
count once every 838.1 ns. Each of the three timers counts down in a
programmable way, generating a signal on its output pin when it counts
down to 0. Each timer is capable of being halted at any time via a 0
level on its gate input; when a timer's gate input is 1, that timer
counts constantly. All in all, the 8253's timers are inherently very
flexible timing devices; unfortunately, much of that flexibility depends
on how the timers are connected to external circuitry, and in the PC the
timers are connected with specific purposes in mind.
Timer 2 drives the speaker, although it can be used for other timing
purposes when the speaker is not in use. As shown in Figure 3.1, timer 2
is the only timer with a programmable gate input in the PC; that is,
timer 2 is the only timer that can be started and stopped under program
control in the manner specified by Intel. On the other hand, the
*output* of timer 2 is connected to nothing other than the speaker. In
particular, timer 2 cannot generate an interrupt to get the 8088's
attention.
Timer 1 is dedicated to providing dynamic RAM refresh, and should not be
tampered with lest system crashes result.
![**Figure 3.1**  *The configuration of the 8253 timer chip in the PC.*](images/03-01.jpg)
Finally, timer 0 is used to drive the system clock. As programmed by the
BIOS at power-up, every 65,536 (64K) counts, or 54.925 milliseconds,
timer 0 generates a rising edge on its output line. (A millisecond is
one-thousandth of a second, and is abbreviated ms.) This line is
connected to the hardware interrupt 0 (IRQ0) line on the system board,
so every 54.925 ms, timer 0 causes hardware interrupt 0 to occur.
The interrupt vector for IRQ0 is set by the BIOS at power-up time to
point to a BIOS routine, `TIMER_INT`, that maintains a time-of-day
count. `TIMER_INT` keeps a 16-bit count of IRQ0 interrupts in the
BIOS data area at address 0000:046C (all addresses in this book are
given in segment:offset hexadecimal pairs); this count turns over once
an hour (less a few microseconds), and when it does, `TIMER_INT`
updates a 16-bit hour count at address 0000:046E in the BIOS data area.
This count is the basis for the current time and date that DOS supports
via functions 2AH (2A hexadecimal) through 2DH and by way of the DATE
and TIME commands.
Each timer channel of the 8253 can operate in any of six modes. Timer 0
normally operates in mode 3: *square wave mode*. In square wave mode,
the initial count is counted down two at a time; when the count reaches
zero, the output state is changed. The initial count is again counted
down two at a time, and the output state is toggled back when the count
reaches zero. The result is a square wave that changes state more slowly
than the input clock by a factor of the initial count. In its normal
mode of operation, timer 0 generates an output pulse that is low for
about 27.5 ms and high for about 27.5 ms; this pulse is sent to the 8259
interrupt controller, and its rising edge generates a timer interrupt
once every 54.925 ms.
Square wave mode is not very useful for precision timing because it
counts down by two twice per timer interrupt, thereby rendering exact
timings impossible. Fortunately, the 8253 offers another timer mode,
mode 2 (divide-by-N mode), which is both a good substitute for square
wave mode and a perfect mode for precision timing.
Divide-by-N mode counts down by one from the initial count. When the
count reaches zero, the timer turns over and starts counting down again
without stopping, and a pulse is generated for a single clock period.
While the pulse is not held for nearly as long as in square wave mode,
it doesn't matter, since the 8259 interrupt controller is configured in
the PC to be edgeand hence cares only about the existence of a pulse
from timer 0, not the duration of the pulse. As a result, timer 0
continues to generate timer interrupts in divide-by-N mode, and the
system clock continues to maintain good time.
Why not use timer 2 instead of timer 0 for precision timing? After all,
timer 2 has a programmable gate input and isn't used for anything but
sound generation. The problem with timer 2 is that its output can't
generate an interrupt; in fact, timer 2 can't do anything but drive the
speaker. We need the interrupt generated by the output of timer 0 to
tell us when the count has overflowed, and we will see shortly that the
timer interrupt also makes it possible to time much longer periods than
the Zen timer shown in Listing 3.1 supports.
In fact, the Zen timer shown in Listing 3.1 can only time intervals of
up to about 54 ms in length, since that is the period of time that can
be measured by timer 0 before its count turns over and repeats.
fifty-four ms may not seem like a very long time, but even a CPU as slow
as the 8088 can perform more than 1,000 divides in 54 ms, and division
is the single instruction that the 8088 performs most slowly. If a
measured period turns out to be longer than 54 ms (that is, if timer 0
has counted down and turned over), the Zen timer will display a message
to that effect. A long-period Zen timer for use in such cases will be
presented later in this chapter.
The Zen timer determines whether timer 0 has turned over by checking to
see whether an IRQ0 interrupt is pending. (Remember, interrupts are off
while the Zen timer runs, so the timer interrupt cannot be recognized
until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt
is pending, then timer 0 has turned over and generated a timer
interrupt. Recall that `ZTimerOn` initially sets timer 0 to 0, in
order to allow for the longest possible period—about 54 ms—before timer
0 reaches 0 and generates the timer interrupt.
Now we're ready to look at the ways in which the Zen timer can introduce
inaccuracy into the system clock. Since timer 0 is initially set to 0 by
the Zen timer, and since the system clock ticks only when timer 0 counts
off 54.925 ms and reaches 0 again, an average inaccuracy of one-half of
54.925 ms, or about 27.5 ms, is incurred each time the Zen timer is
started. In addition, a timer interrupt is generated when timer 0 is
switched from mode 3 to mode 2, advancing the system clock by up to
54.925 ms, although this only happens the first time the Zen timer is
run after a warm or cold boot. Finally, up to 54.925 ms can again be
lost when `ZTimerOff` is called, since that routine again sets the
timer count to zero. Net result: The system clock will run up to 110 ms
(about a ninth of a second) slow each time the Zen timer is used.
Potentially far greater inaccuracy can be incurred by timing code that
takes longer than about 110 ms to execute. Recall that all interrupts,
including the timer interrupt, are disabled while timing code with the
Zen timer. The 8259 interrupt controller is capable of remembering at
most one pending timer interrupt, so all timer interrupts after the
first one during any given Zen timing interval are ignored.
Consequently, if a timing interval exceeds 54.9 ms, the system clock
effectively stops 54.9 ms after the timing interval starts and doesn't
restart until the timing interval ends, losing time all the while.
The effects on the system time of the Zen timer aren't a matter for
great concern, as they are temporary, lasting only until the next warm
or cold boot. System that have batteryclocks, (AT-style machines; that
is, virtually all machines in common use) automatically reset the
correct time whenever the computer is booted, and systems without
battery-clocks prompt for the correct date and time when booted.
Also,repeated use of the Zen timer usually makes the system clock slow
by at most a total of a few seconds, unless code that takes much longer
than 54 ms to run is timed (in which case the Zen timer will notify you
that the code is too long to time).
Nonetheless, it's a good idea to reboot your computer at the end of each
session with the Zen timer in order to make sure that the system clock
is correct.
### Stopping the Zen Timer
At some point after `ZTimerOn` is called, `ZTimerOff` must always be
called to mark the end of the timing interval. `ZTimerOff` saves the
context of the calling program, latches and reads the timer 0 count,
converts that count from the countdown value that the timer maintains to
the number of counts elapsed since `ZTimerOn` was called, and stores
the result. Immediately after latching the timer 0 count—and before
enabling interrupts—`ZTimerOff` checks the 8259 interrupt controller
to see if there is a pending timer interrupt, setting a flag to mark
that the timer overflowed if there is indeed a pending timer interrupt.
After that, `ZTimerOff` executes just the overhead code of
`ZTimerOn` and `ZTimerOff` 16 times, and averages and saves the
results in order to determine how many of the counts in the timing
result just obtained were incurred by the overhead of the Zen timer
rather than by the code being timed.
Finally, `ZTimerOff` restores the context of the calling program,
including the state of the interrupt flag that was in effect when
`ZTimerOn` was called to start timing, and returns.
One interesting aspect of `ZTimerOff` is the manner in which timer 0
is stopped in order to read the timer count. We don't actually have to
stop timer 0 to read the count; the 8253 provides a special latched read
feature for the specific purpose of reading the count while a time is
running. (That's a good thing, too; we've no documented way to stop
timer 0 if we wanted to, since its gate input isn't connected. Later in
this chapter, though, we'll see that timer 0 can be stopped after all.)
We simply tell the 8253 to latch the current count, and the 8253 does so
without breaking stride.
### Reporting Timing Results
`ZTimerReport` may be called to display timing results at any time
after both `ZTimerOn` and `ZTimerOff` have been called.
`ZTimerReport` first checks to see whether the timer overflowed
(counted down to 0 and turned over) before `ZTimerOff` was called; if
overflow did occur, `ZTimerOff` prints a message to that effect and
returns. Otherwise, `ZTimerReport` subtracts the reference count
(representing the overhead of the Zen timer) from the count measured
between the calls to `ZTimerOn` and `ZTimerOff`, converts the result
from timer counts to microseconds, and prints the resulting time in
microseconds to the standard output.
Note that `ZTimerReport` need not be called immediately after
`ZTimerOff`. In fact, after a given call to `ZTimerOff,
ZTimerReport` can be called at any time right up until the next call to
`ZTimerOn`.
You may want to use the Zen timer to measure several portions of a
program while it executes normally, in which case it may not be
desirable to have the text printed by `ZTimerReport` interfere with
the program's normal display. There are many ways to deal with this. One
approach is removal of the invocations of the DOS print string function
(INT 21H with AH equal to 9) from `ZTimerReport`, instead running the
program under a debugger that supports screen flipping (such as Turbo
Debugger or CodeView), placing a breakpoint at the start of
`ZTimerReport`, and directly observing the count in microseconds as
`ZTimerReport` calculates it.
A second approach is modification of `ZTimerReport` to place the
result at some safe location in memory, such as an unused portion of the
BIOS data area.
A third approach is alteration of `ZTimerReport` to print the result
over a serial port to a terminal or to another PC acting as a terminal.
Similarly, many debuggers can be run from a remote terminal via a serial
link.
Yet another approach is modification of `ZTimerReport` to send the
result to the printer via either DOS function 5 or BIOS interrupt 17H.
A final approach is to modify `ZTimerReport` to print the result to
the auxiliary output via DOS function 4, and to then write and load a
special device driver named `AUX`, to which DOS function 4 output
would automatically be directed. This device driver could send the
result anywhere you might desire. The result might go to the secondary
display adapter, over a serial port, or to the printer, or could simply
be stored in a buffer within the driver, to be dumped at a later time.
(Credit for this final approach goes to Michael Geary, and thanks go to
David Miller for passing the idea on to me.)
You may well want to devise still other approaches better suited to your
needs than those I've presented. Go to it! I've just thrown out a few
possibilities to get you started.
### Notes on the Zen Timer
The Zen timer subroutines are designed to be near-called from assembly
language code running in the public segment `Code`. The Zen timer
subroutines can, however, be called from any assembly or high-level
language code that generates OBJ files that are compatible with the
Microsoft linker, simply by modifying the segment that the timer code
runs in to match the segment used by the code being timed, or by
changing the Zen timer routines to far procedures and making far calls
to the Zen timer code from the code being timed, as discussed at the end
of this chapter. All three subroutines preserve all registers and all
flags except the interrupt flag, so calls to these routines are
transparent to the calling code.
If you do change the Zen timer routines to far procedures in order to
call them from code running in another segment, be sure to make *all*
the Zen timer routines far, including `ReferenceZTimerOn` and
`ReferenceZTimerOff`. (You'll have to put `FAR PTR` overrides on the
calls from `ZTimerOff` to the latter two routines if you do make them
far.) If the reference routines aren't the same type—near or far—as the
other routines, they won't reflect the true overhead incurred by
starting and stopping the Zen timer.
Please be aware that the inaccuracy that the Zen timer can introduce
into the system clock time does not affect the accuracy of the
performance measurements reported by the Zen timer itself. The 8253
counts once every 838 ns, giving us a count resolution of about 1µs,
although factors such as the prefetch queue (as discussed below),
dynamic RAM refresh, and internal timing variations in the 8253 make it
perhaps more accurate to describe the Zen timer as measuring code
performance with an accuracy of better than 10µs. In fact, the Zen timer
is actually most accurate in assessing code performance when timing
intervals longer than about 100 µs. At any rate, we're most interested
in using the Zen timer to assess the relative performance of various
code sequences—that is, using it to compare and tweak code—and the timer
is more than accurate enough for that purpose.
The Zen timer works on all PC-compatible computers I've tested it on,
including XTs, ATs, PS/2 computers, and 386, 486, and Pentium-based
machines. Of course, I haven't been able to test it on *all*
PC-compatibles, but I don't expect any problems; computers on which the
Zen timer doesn't run can't truly be called "PC-compatible."
On the other hand, there is certainly no guarantee that code performance
as measured by the Zen timer will be the same on compatible computers as
on genuine IBM machines, or that either absolute or relative code
performance will be similar even on different IBM models; in fact, quite
the opposite is true. For example, every PS/2 computer, even the
relatively slow Model 30, executes code much faster than does a PC or
XT. As another example, I set out to do the timings for my earlier book
*Zen of Assembly Language* on an XTcomputer, only to find that the
computer wasn't quite IBM-compatible regarding code performance. The
differences were minor, mind you, but my experience illustrates the risk
of assuming that a specific make of computer will perform in a certain
way without actually checking.
Not that this variation between models makes the Zen timer one whit less
useful—quite the contrary. The Zen timer is an excellent tool for
evaluating code performance over the entire spectrum of PC-compatible
computers.
### A Sample Use of the Zen Timer
Listing 3.2 shows a test-bed program for measuring code performance with
the Zen timer. This program sets DS equal to CS (for reasons we'll
discuss shortly), includes the code to be measured from the file
TESTCODE, and calls `ZTimerReport` to display the timing results.
Consequently, the code being measured should be in the file TESTCODE,
and should contain calls to `ZTimerOn` and `ZTimerOff` .
**LISTING 3.2 PZTEST.ASM**
```nasm
; Program to measure performance of code that takes less than
; 54 ms to execute. (PZTEST.ASM)
;
; Link with PZTIMER.ASM (Listing 3.1). PZTEST.BAT (Listing 3.4)
; can be used to assemble and link both files. Code to be
; measured must be in the file TESTCODE; Listing 3.3 shows
; a sample TESTCODE file.
;
; By Michael Abrash
;
mystack segment para stack STACK'
db 512 dup(?)
mystack ends
;
Code segment para public CODE'
assume cs:Code, ds:Code
extrnZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Start proc near
push cs
pop ds ; set DS to point to the code segment,
; so data as well as code can easily
; be included in TESTCODE
;
include TESTCODE ;code to be measured, including
; calls to ZTimerOn and ZTimerOff
;
; Display the results.
;
call ZTimerReport
;
; Terminate the program.
;
mov ah,4ch
int 21h
Start endp
Code ends
end Start
```
Listing 3.3 shows some sample code to be timed. This listing measures
the time required to execute 1,000 loads of AL from the memory variable
`MemVar` . Note that Listing 3.3 calls `ZTimerOn` to start timing,
performs 1,000 `MOV` instructions in a row, and calls `ZTimerOff` to
end timing. When Listing 3.2 is named TESTCODE and included by Listing
3.3, Listing 3.2 calls `ZTimerReport` to display the execution time
after the code in Listing 3.3 has been run.
**LISTING 3.3 LST3-3.ASM**
```nasm
; Test file;
; Measures the performance of 1,000 loads of AL from
; memory. (Use by renaming to TESTCODE, which is
; included by PZTEST.ASM (Listing 3.2). PZTIME.BAT
; (Listing 3.4) does this, along with all assembly
; and linking.)
;
jmp Skip ;jump around defined data
;
MemVar db ?
;
Skip:
;
; Start timing.
;
call ZTimerOn
;
rept 1000
mov al,[MemVar]
endm
;
; Stop timing.
;
call ZTimerOff
```
It's worth noting that Listing 3.3 begins by jumping around the memory
variable `MemVar`. This approach lets us avoid reproducing Listing 3.2
in its entirety for each code fragment we want to measure; by defining
any needed data right in the code segment and jumping around that data,
each listing becomes self-contained and can be plugged directly into
Listing 3.2 as TESTCODE. Listing 3.2 sets DS equal to CS before doing
anything else precisely so that data can be embedded in code fragments
being timed. Note that only after the initial jump is performed in
Listing 3.3 is the Zen timer started, since we don't want to include the
execution time of start-up code in the timing interval. That's why the
calls to `ZTimerOn` and `ZTimerOff` are in TESTCODE, not in
PZTEST.ASM; this way, we have full control over which portion of
TESTCODE is timed, and we can keep set-up code and the like out of the
timing interval.
Listing 3.3 is used by naming it TESTCODE, assembling both Listing 3.2
(which includes TESTCODE) and Listing 3.1 with TASM or MASM, and linking
the two resulting OBJ files together by way of the Borland orMicrosoft
linker. Listing 3.4 shows a batch file, PZTIME.BAT, which does all that;
when run, this batch file generates and runs the executable file
PZTEST.EXE. PZTIME.BAT (Listing 3.4) assumes that the file PZTIMER.ASM
contains Listing 3.1, and the file PZTEST.ASM contains Listing 3.2. The
command-line parameter to PZTIME.BAT is the name of the file to be
copied to TESTCODE and included into PZTEST.ASM. (Note that Turbo
Assembler can be substituted for MASM by replacing "masm" with "tasm"
and "link" with "tlink" in Listing 3.4. The same is true of Listing
3.7.)
**LISTING 3.4 PZTIME.BAT**
```bat
echo off
rem
rem *** Listing 3.4 ***
rem
rem ***************************************************************
rem * Batch file PZTIME.BAT, which builds and runs the precision *
rem * Zen timer program PZTEST.EXE to time the code named as the *
rem * command-line parameter. Listing 3.1 must be named *
rem * PZTIMER.ASM, and Listing 3.2 must be named PZTEST.ASM. To *
rem * time the code in LST3-3, you'd type the DOS command: *
rem * *
rem * pztime lst3-3 *
rem * *
rem * Note that MASM and LINK must be in the current directory or *
rem * on the current path in order for this batch file to work. *
rem * *
rem * This batch file can be speeded up by assembling PZTIMER.ASM *
rem * once, then removing the lines: *
rem * *
rem * masm pztimer; *
rem * if errorlevel 1 goto errorend *
rem * *
rem * from this file. *
rem * *
rem * By Michael Abrash *
rem ***************************************************************
rem
rem Make sure a file to test was specified.
rem
if not x%1==x goto ckexist
echo ***************************************************************
echo * Please specify a file to test. *
echo ***************************************************************
goto end
rem
rem Make sure the file exists.
rem
:ckexist
if exist %1 goto docopy
echo ***************************************************************
echo * The specified file, "%1," doesn't exist. *
echo ***************************************************************
goto end
rem
rem copy the file to measure to TESTCODE.
rem
:docopy
copy %1 testcode
masm pztest;
if errorlevel 1 goto errorend
masm pztimer;
if errorlevel 1 goto errorend
link pztest+pztimer;
if errorlevel 1 goto errorend
pztest
goto end
:errorend
echo ***************************************************************
echo * An error occurred while building the precision Zen timer. *
echo ***************************************************************
:end
```
Assuming that Listing 3.3 is named LST3-3.ASM and Listing 3.4 is named
PZTIME.BAT, the code in Listing 3.3 would be timed with the command:
```sh
pztime LST3-3.ASM
```
which performs all assembly and linking, and reports the execution time
of the code in Listing 3.3.
When the above command is executed on an original 4.77 MHz IBM PC, the
time reported by the Zen timer is 3619 µs, or about 3.62 µs per load of
AL from memory. (While the exact number is 3.619 µs per load of AL, I'm
going to round off that last digit from now on. No matter how many
repetitions of a given instruction are timed, there's just too much
noise in the timing process—between dynamic RAM refresh, the prefetch
queue, and the internal state of the processor at the start of
timing—for that last digit to have any significance.) Given the test
PC's 4.77 MHz clock, this works out to about 17 cycles per `MOV`,
which is actually a good bit longer than Intel's specified 10-cycle
execution time for this instruction. (See the MASM or TASM
documentation, or Intel's processor reference manuals, for official
execution times.) Fear not, the Zen timer is right—**MOV AL,[MEMVAR]**
really does take 17 cycles as used in Listing 3.3. Exactly why that is
so is just what this book is all about.
In order to perform any of the timing tests in this book, enter Listing
3.1 and name it PZTIMER.ASM, enter Listing 3.2 and name it PZTEST.ASM,
and enter Listing 3.4 and name it PZTIME.BAT. Then simply enter the
listing you wish to run into the file *filename* and enter the command:
```sh
pztime <filename>
```
In fact, that's exactly how I timed each of the listings in this book.
Code fragments you write yourself can be timed in just the same way. If
you wish to time code directly in place in your programs, rather than in
the test-bed program of Listing 3.2, simply insert calls to `ZTimerOn,
ZTimerOff`, and `ZTimerReport` in the appropriate places and link
PZTIMER to your program.
### The Long-Period Zen Timer
With a few exceptions, the Zen timer presented above will serve us well
for the remainder of this book since we'll be focusing on relatively
short code sequences that generally take much less than 54 ms to
execute. Occasionally, however, we will need to time longer intervals.
What's more, it is very likely that you will want to time code sequences
longer than 54 ms at some point in your programming career. Accordingly,
I've also developed a Zen timer for periods longer than 54 ms. The
long-period Zen timer (so named by contrast with the precision Zen timer
just presented) shown in Listing 3.5 can measure periods up to one hour
in length.
The key difference between the long-period Zen timer and the precision
Zen timer is that the long-period timer leaves interrupts enabled during
the timing period. As a result, timer interrupts are recognized by the
PC, allowing the BIOS to maintain an accurate system clock time over the
timing period. Theoretically, this enables measurement of arbitrarily
long periods. Practically speaking, however, there is no need for a
timer that can measure more than a few minutes, since the DOS time of
day and date functions (or, indeed, the DATE and TIME commands in a
batch file) serve perfectly well for longer intervals. Since very long
timing intervals aren't needed, the long-period Zen timer uses a
simplified means of calculating elapsed time that is limited to
measuring intervals of an hour or less. If a period longer than an hour
is timed, the long-period Zen timer prints a message to the effect that
it is unable to time an interval of that length.
For implementation reasons, the long-period Zen timer is also incapable
of timing code that starts before midnight and ends after midnight; if
that eventuality occurs, the long-period Zen timer reports that it was
unable to time the code because midnight was crossed. If this happens to
you, just time the code again, secure in the knowledge that at least you
won't run into the problem again for 23-odd hours.
You should not use the long-period Zen timer to time code that requires
interrupts to be disabled for more than 54 ms at a stretch during the
timing interval, since when interrupts are disabled the long-period Zen
timer is subject to the same 54 ms maximum measurement time as the
precision Zen timer.
While permitting the timer interrupt to occur allows long intervals to
be timed, that same interrupt makes the long-period Zen timer less
accurate than the precision Zen timer, since the time the BIOS spends
handling timer interrupts during the timing interval is included in the
time measured by the long-period timer. Likewise, any other interrupts
that occur during the timing interval, most notably keyboard and mouse
interrupts, will increase the measured time.
The long-period Zen timer has some of the same effects on the system
time as does the precision Zen timer, so it's a good idea to reboot the
system after a session with the long-period Zen timer. The long-period
Zen timer does not, however, have the same potential for introducing
major inaccuracy into the system clock time during a single timing run
since it leaves interrupts enabled and therefore allows the system clock
to update normally.
#### Stopping the Clock
There's a potential problem with the long-period Zen timer. The problem
is this: In order to measure times longer than 54 ms, we must maintain
not one but two timing components, the timer 0 count and the BIOS
time-of-day count. The time-of-day count measures the passage of 54.9 ms
intervals, while the timer 0 count measures time within those 54.9 ms
intervals. We need to read the two time components simultaneously in
order to get a clean reading. Otherwise, we may read the timer count
just before it turns over and generates an interrupt, then read the BIOS
time-of-day count just after the interrupt has occurred and caused the
time-of-day count to turn over, with a resulting 54 ms measurement
inaccuracy. (The opposite sequence—reading the time-of-day count and
then the timer count—can result in a 54 ms inaccuracy in the other
direction.)
The only way to avoid this problem is to stop timer 0, read both the
timer and time-of-day counts while the timer is stopped, and then
restart the timer. Alas, the gate input to timer 0 isn't
program-controllable in the PC, so there's no documented way to stop the
timer. (The latched read feature we used in Listing 3.1 doesn't stop the
timer; it latches a count, but the timer keeps running.) What should we
do?
As it turns out, an undocumented feature of the 8253 makes it possible
to stop the timer dead in its tracks. Setting the timer to a new mode
and waiting for an initial count to be loaded causes the timer to stop
until the count is loaded. Surprisingly, the timer count remains
readable and correct while the timer is waiting for the initial load.
In my experience, this approach works beautifully with fully
8253-compatible chips. However, there's no guarantee that it will always
work, since it programs the 8253 in an undocumented way. What's more,
IBM chose not to implement compatibility with this particular 8253
feature in the custom chips used in PS/2 computers. On PS/2 computers,
we have no choice but to latch the timer 0 count and then stop the BIOS
count (by disabling interrupts) as quickly as possible. We'll just have
to accept the fact that on PS/2 computers we may occasionally get a
reading that's off by 54 ms, and leave it at that.
I've set up Listing 3.5 so that it can assemble to either use or not use
the undocumented timer-stopping feature, as you please. The `PS2`
equate selects between the two modes of operation. If `PS2` is 1 (as
it is in Listing 3.5), then the latch-and-read method is used; if
`PS2` is 0, then the undocumented timer-stop approach is used. The
latch-and-read method will work on all PC-compatible computers, but may
occasionally produce results that are incorrect by 54 ms. The timer-stop
approach avoids synchronization problems, but doesn't work on all
computers.
**LISTING 3.5 LZTIMER.ASM**
```nasm
;
; The long-period Zen timer. (LZTIMER.ASM)
; Uses the 8253 timer and the BIOS time-of-day count to time the
; performance of code that takes less than an hour to execute.
; Because interrupts are left on (in order to allow the timer
; interrupt to be recognized), this is less accurate than the
; precision Zen timer, so it is best used only to time code that takes
; more than about 54 milliseconds to execute (code that the precision
; Zen timer reports overflow on). Resolution is limited by the
; occurrence of timer interrupts.
;
; By Michael Abrash
;
; Externally callable routines:
;
; ZTimerOn: Saves the BIOS time of day count and starts the
; long-period Zen timer.
;
; ZTimerOff: Stops the long-period Zen timer and saves the timer
; count and the BIOS time-of-day count.
;
; ZTimerReport: Prints the time that passed between starting and
; stopping the timer.
;
; Note: If either more than an hour passes or midnight falls between
; calls to ZTimerOn and ZTimerOff, an error is reported. For
; timing code that takes more than a few minutes to execute,
; either the DOS TIME command in a batch file before and after
; execution of the code to time or the use of the DOS
; time-of-day function in place of the long-period Zen timer is
; more than adequate.
;
; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
; PS2 must be set to 1 on PS/2 computers because the PS/2's
; timers are not compatible with an undocumented timer-stopping
; feature of the 8253; the alternative timing approach that
; must be used on PS/2 computers leaves a short window
; during which the timer 0 count and the BIOS timer count may
; not be synchronized. You should also set the PS2 symbol to
; 1 if you're getting erratic or obviously incorrect results.
;
; Note: When PS2 is 0, the code relies on an undocumented 8253
; feature to get more reliable readings. It is possible that
; the 8253 (or whatever chip is emulating the 8253) may be put
; into an undefined or incorrect state when this feature is
; used.
;
; ******************************************************************
; * If your computer displays any hint of erratic behavior *
; * after the long-period Zen timer is used, such as the floppy*
; * drive failing to operate properly, reboot the system, set *
; * PS2 to 1 and leave it that way! *
; ******************************************************************
;
; Note: Each block of code being timed should ideally be run several
; times, with at least two similar readings required to
; establish a true measurement, in order to eliminate any
; variability caused by interrupts.
;
; Note: Interrupts must not be disabled for more than 54 ms at a
; stretch during the timing interval. Because interrupts
; are enabled, keys, mice, and other devices that generate
; interrupts should not be used during the timing interval.
;
; Note: Any extra code running off the timer interrupt (such as
; some memory-resident utilities) will increase the time
; measured by the Zen timer.
;
; Note: These routines can introduce inaccuracies of up to a few
; tenths of a second into the system clock count for each
; code section timed. Consequently, it's a good idea to
; reboot at the conclusion of timing sessions. (The
; battery-backed clock, if any, is not affected by the Zen
; timer.)
;
; All registers and all flags are preserved by all routines.
;
Code segment word public CODE'
assume cs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport
;
; Set PS2 to 0 to assemble for use on a fully 8253-compatible
; system; when PS2 is 0, the readings are more reliable if the
; computer supports the undocumented timer-stopping feature,
; but may be badly off if that feature is not supported. In
; fact, timer-stopping may interfere with your computer's
; overall operation by putting the 8253 into an undefined or
; incorrect state. Use with caution!!!
;
; Set PS2 to 1 to assemble for use on non-8253-compatible
; systems, including PS/2 computers; when PS2 is 1, readings
; may occasionally be off by 54 ms, but the code will work
; properly on all systems.
;
; A setting of 1 is safer and will work on more systems,
; while a setting of 0 produces more reliable results in systems
; which support the undocumented timer-stopping feature of the
; 8253. The choice is yours.
;
PS2 equ1
;
; Base address of the 8253 timer chip.
;
BASE_8253 equ40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253 equBASE_8253 + 0
;
; The address of the mode register in the 8253.
;
MODE_8253 equBASE_8253 + 3
;
; The address of the BIOS timer count variable in the BIOS
; data segment.
;
TIMER_COUNT equ46ch
;
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts remain disabled.
;
MPOPF macro
local p1, p2
jmp short p2
p1: iret ;jump to pushed address & pop flags
p2: pushcs ;construct far return address to
call p1 ; the next instruction
endm
;
; Macro to delay briefly to ensure that enough time has elapsed
; between successive I/O accesses so that the device being accessed
; can respond to both accesses even on a very fast PC.
;
DELAY macro
jmp $+2
jmp $+2
jmp $+2
endm
StartBIOSCountLow dw ? ;BIOS count low word at the
; start of the timing period
StartBIOSCountHigh dw ? ;BIOS count high word at the
; start of the timing period
EndBIOSCountLow dw ? ;BIOS count low word at the
; end of the timing period
EndBIOSCountHigh dw ? ;BIOS count high word at the
; end of the timing period
EndTimedCount dw ? ;timer 0 count at the end of
; the timing period
ReferenceCount dw ? ;number of counts required to
; execute timer overhead code
;
; String printed to report results.
;
OutputStr labelbyte
db 0dh, 0ah, Timed count:
TimedCountStr db10 dup (?)
db' microseconds', 0dh, 0ah
db $'
;
; Temporary storage for timed count as it's divided down by powers
; of ten when converting from doubleword binary to ASCII.
;
CurrentCountLow dw ?
CurrentCountHigh dw ?
;
; Powers of ten table used to perform division by 10 when doing
; doubleword conversion from binary to ASCII.
;
PowersOfTenlabelword
dd 1
dd 10
dd 100
dd 1000
dd 10000
dd 100000
dd 1000000
dd 10000000
dd 100000000
dd 1000000000
PowersOfTenEnd labelword
;
; String printed to report that the high word of the BIOS count
; changed while timing (an hour elapsed or midnight was crossed),
; and so the count is invalid and the test needs to be rerun.
;
TurnOverStrlabelbyte
db 0dh, 0ah
db ****************************************************'
db 0dh, 0ah
db'* Either midnight passed or an hour or more passed *'
db 0dh, 0ah
db'* while timing was in progress. If the former was *'
db 0dh, 0ah
db'* the case, please rerun the test; if the latter *'
db 0dh, 0ah
db'* was the case, the test code takes too long to *'
db 0dh, 0ah
db'* run to be timed by the long-period Zen timer. *'
db 0dh, 0ah
db * Suggestions: use the DOS TIME command, the DOS *'
db 0dh, 0ah
db * time function, or a watch. *'
db 0dh, 0ah
db ****************************************************'
db 0dh, 0ah
db'$'
;********************************************************************
;* Routine called to start timing. *
;********************************************************************
ZTimerOn proc near
;
; Save the context of the program being timed.
;
push ax
pus hf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also stops
; timer 0 until the timer count is loaded, except on PS/2
; computers.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0, so we know we won't get another
; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed.
;
DELAY
subal,al
outTIMER_0_8253,al ;lsb
DELAY
outTIMER_0_8253,al ;msb
;
; In case interrupts are disabled, enable interrupts briefly to allow
; the interrupt generated when switching from mode 3 to mode 2 to be
; recognized. Interrupts must be enabled for at least 210 ns to allow
; time for that interrupt to occur. Here, 10 jumps are used for the
; delay to ensure that the delay time will be more than long enough
; even on a very fast PC.
;
pushf
sti
rept 10
jmp $+2
endm
MPOPF
;
; Store the timing start BIOS count.
; (Since the timer count was just set to 0, the BIOS count will
; stay the same for the next 54 ms, so we don't need to disable
; interrupts in order to avoid getting a half-changed count.)
;
push ds
subax, ax
movds, ax
movax, ds:[TIMER_COUNT+2]
movcs: [StartBIOSCountHigh],ax
movax, ds:[TIMER_COUNT]
movcs: [StartBIOSCountLow],ax
pop ds
;
; Set the timer count to 0 again to start the timing interval.
;
mov al,00110100b ;set up to load initial
out MODE_8253,al ; timer count
DELAY
subal, al
out TIMER_0_8253,al; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
popax
ret
ZTimerOnendp
;********************************************************************
;* Routine called to stop timing and get count. *
;********************************************************************
ZTimerOff procnear
;
; Save the context of the program being timed.
;
pushf
pushax
pushcx
;
; In case interrupts are disabled, enable interrupts briefly to allow
; any pending timer interrupt to be handled. Interrupts must be
; enabled for at least 210 ns to allow time for that interrupt to
; occur. Here, 10 jumps are used for the delay to ensure that the
; delay time will be more than long enough even on a very fast PC.
;
sti
rept 10
jmp $+2
endm
;
; Latch the timer count.
;
if PS2
mov al,00000000b
out MODE_8253,al ;latch timer 0 count
;
; This is where a one-instruction-long window exists on the PS/2.
; The timer count and the BIOS count can lose synchronization;
; since the timer keeps counting after it's latched, it can turn
; over right after it's latched and cause the BIOS count to turn
; over before interrupts are disabled, leaving us with the timer
; count from before the timer turned over coupled with the BIOS
; count from after the timer turned over. The result is a count
; that's 54 ms too long.
;
else
;
; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count
; load, which stops timer 0 until the count is loaded. (Only works
; on fully 8253-compatible chips.)
;
mov al,00110100b; mode 2
out MODE_8253,al
DELAY
mov al,00000000b ;latch timer 0 count
out MODE_8253,al
endif
cli ;stop the BIOS count
;
; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won't change.)
;
push ds
sub ax,ax
mov ds,ax
mov ax,ds:[TIMER_COUNT+2]
mov cs:[EndBIOSCountHigh],ax
mov ax,ds:[TIMER_COUNT]
mov cs:[EndBIOSCountLow],ax
pop ds
;
; Read the timer count and save it.
;
in al,TIMER_0_8253 ;lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ;msb
xchg ah,al
neg ax ;convert from countdown
; remaining to elapsed
; count
mov cs:[EndTimedCount],ax
;
; Restart timer 0, which is still waiting for an initial count
; to be loaded.
;
ife PS2
DELAY
mov al,00110100b ;mode 2, waiting to load a
; 2-byte count
out MODE_8253,al
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
mov al,ah
out TIMER_0_8253,al ;msb
DELAY
endif
sti;let the BIOS count continue
;
; Time a zero-length code fragment, to get a reference for how
; much overhead this routine has. Time it 16 times and average it,
; for accuracy, rounding the result.
;
mov cs:[ReferenceCount],0
mov cx,16
cli ;interrupts off to allow a
; precise reference count
RefLoop:
call ReferenceZTimerOn
call ReferenceZTimerOff
loop RefLoop
sti
add cs:[ReferenceCount],8; total + (0.5 * 16)
mov cl,4
shr cs:[ReferenceCount],cl;(total) / 16 + 0.5
;
; Restore the context of the program being timed and return to it.
;
popcx
popax
MPOPF
ret
ZTimerOff endp
;
; Called by ZTimerOff to start the timer for overhead measurements.
;
ReferenceZTimerOnprocnear
;
; Save the context of the program being timed.
;
pushax
pushf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0.
;
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
out TIMER_0_8253,al ;msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
popax
ret
ReferenceZTimerOnendp
;
; Called by ZTimerOff to stop the timer and add the result to
; ReferenceCount for overhead measurements. Doesn't need to look
; at the BIOS count because timing a zero-length code fragment
; isn't going to take anywhere near 54 ms.
;
ReferenceZTimerOff procnear
;
; Save the context of the program being timed.
;
pushf
pushax
pushcx
;
; Match the interrupt-window delay in ZTimerOff.
;
sti
rept10
jmp$+2
endm
mov al,00000000b
out MODE_8253,al ;latch timer
;
; Read the count and save it.
;
DELAY
in al,TIMER_0_8253 ;lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ;msb
xchg ah,al
neg ax ;convert from countdown
; remaining to elapsed
; count
add cs:[ReferenceCount],ax
;
; Restore the context and return.
;
popcx
popax
MPOPF
ret
ReferenceZTimerOff endp
;********************************************************************
;* Routine called to report timing results. *
;********************************************************************
ZTimerReportprocnear
pushf
push ax
push bx
push cx
push dx
push si
push di
push ds
;
push cs ;DOS functions require that DS point
pop ds ; to text to be displayed on the screen
assume ds :Code
;
; See if midnight or more than an hour passed during timing. If so,
; notify the user.
;
mov ax,[StartBIOSCountHigh]
cmp ax,[EndBIOSCountHigh]
jz CalcBIOSTime ;hour count didn't change,
; so everything's fine
inc ax
cmp ax,[EndBIOSCountHigh]
jnz TestTooLong ;midnight or two hour
; boundaries passed, so the
; results are no good
mov ax,[EndBIOSCountLow]
cmp ax,[StartBIOSCountLow]
jb CalcBIOSTime ;a single hour boundary
; passed--that's OK, so long as
; the total time wasn't more
; than an hour
;
; Over an hour elapsed or midnight passed during timing, which
; renders the results invalid. Notify the user. This misses the
; case where a multiple of 24 hours has passed, but we'll rely
; on the perspicacity of the user to detect that case.
;
TestTooLong:
mov ah,9
mov dx,offset TurnOverStr
int 21h
jmp short ZTimerReportDone
;
; Convert the BIOS time to microseconds.
;
CalcBIOSTime:
mov ax,[EndBIOSCountLow]
sub ax,[StartBIOSCountLow]
mov dx,54925 ;number of microseconds each
; BIOS count represents
mul dx
mov bx,ax ;set aside BIOS count in
mov cx,dx ; microseconds
;
; Convert timer count to microseconds.
;
mov ax,[EndTimedCount]
mov si,8381
mul si
mov si,10000
div si ;* .8381 = * 8381 / 10000
;
; Add timer and BIOS counts together to get an overall time in
; microseconds.
;
add bx,ax
adc cx,0
;
; Subtract the timer overhead and save the result.
;
mov ax,[ReferenceCount]
mov si,8381 ;convert the reference count
mul si ; to microseconds
mov si,10000
div si;* .8381 = * 8381 / 10000
sub bx,ax
sbb cx,0
mov [CurrentCountLow],bx
mov [CurrentCountHigh],cx
;
; Convert the result to an ASCII string by trial subtractions of
; powers of 10.
;
mov di,offset PowersOfTenEnd - offset PowersOfTen - 4
mov si,offset TimedCountStr
CTSNextDigit:
mov bl,'0'
CTSLoop:
mov ax,[CurrentCountLow]
mov dx,[CurrentCountHigh]
sub ax,PowersOfTen[di]
sbb dx,PowersOfTen[di+2]
jc CTSNextPowerDown
inc bl
mov [CurrentCountLow],ax
mov [CurrentCountHigh],dx
jmp CTSLoop
CTSNextPowerDown:
mov [si],bl
inc si
sub di,4
jns CTSNextDigit
;
;
; Print the results.
;
mov ah,9
mov dx,offset OutputStr
int 21h
;
ZTimerReportDone:
pop ds
pop di
pop si
pop dx
pop cx
pop bx
pop ax
MPOPF
ret
ZTimerReport endp
Code ends
end
```
Moreover, because it uses an undocumented feature, the timer-stop
approach could conceivably cause erratic 8253 operation, which could in
turn seriously affect your computer's operation until the next reboot.
In non-8253-compatible systems, I've observed not only wildly incorrect
timing results, but also failure of a diskette drive to operate properly
after the long-period Zen timer with `PS2` set to 0 has run, so be
alert for signs of trouble if you do set `PS2` to 0.
Rebooting should clear up any timer-related problems of the sort
described above. (This gives us another reason to reboot at the end of
each code-timing session.) You should *immediately* reboot and set the
`PS2` equate to 1 if you get erratic or obviously incorrect results
with the long-period Zen timer when `PS2` is set to 0. If you want to
set `PS2` to 0, it would be a good idea to time a few of the listings
in this book with `PS2` set first to 1 and then to 0, to make sure
that the results match. If they're consistently different, you should
set `PS2` to 1.
While the the non-PS/2 version is more dangerous than the PS/2 version,
it also produces more accurate results when it does work. If you have a
non-PS/2 PC-compatible computer, the choice between the two timing
approaches is yours.
If you do leave the `PS2` equate at 1 in Listing 3.5, you should
repeat each code-timing run several times before relying on the results
to be accurate to more than 54 ms, since variations may result from the
possible lack of synchronization between the timer 0 count and the BIOS
time-of-day count. In fact, it's a good idea to time code more than once
no matter which version of the long-period Zen timer you're using, since
interrupts, which must be enabled in order for the long-period timer to
work properly, may occur at any time and can alter execution time
substantially.
Finally, please note that the *precision* Zen timer works perfectly well
on both PS/2 and non-PS/2 computers. The PS/2 and 8253 considerations
we've just discussed apply *only* to the longZen timer.
### Example Use of the Long-Period Zen Timer
The long-period Zen timer has exactly the same calling interface as the
precision Zen timer, and can be used in place of the precision Zen timer
simply by linking it to the code to be timed in place of linking the
precision timer code. Whenever the precision Zen timer informs you that
the code being timed takes too long for the precision timer to handle,
all you have to do is link in the long-period timer instead.
Listing 3.6 shows a test-bed program for the long-period Zen timer.
While this program is similar to Listing 3.2, it's worth noting that
Listing 3.6 waits for a few seconds before calling `ZTimerOn`, thereby
allowing any pending keyboard interrupts to be processed. Since
interrupts must be left on in order to time periods longer than 54 ms,
the interrupts generated by keystrokes (including the upstroke of the
Enter key press that starts the program)—or any other interrupts, for
that matter—could incorrectly inflate the time recorded by the
long-period Zen timer. In light of this, resist the temptation to type
ahead, move the mouse, or the like while the long-period Zen timer is
timing.
**LISTING 3.6 LZTEST.ASM**
```nasm
; Program to measure performance of code that takes longer than
; 54 ms to execute. (LZTEST.ASM)
;
; Link with LZTIMER.ASM (Listing 3.5). LZTIME.BAT (Listing 3.7)
; can be used to assemble and link both files. Code to be
; measured must be in the file TESTCODE; Listing 3.8 shows
; a sample file (LST3-8.ASM) which should be named TESTCODE.
;
; By Michael Abrash
;
mystack segment para stack STACK'
db 512 dup(?)
mystack ends
;
Code segment para public CODE'
assume cs:Code, ds:Code
extrn ZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Startproc near
push cs
pop ds ;point DS to the code segment,
; so data as well as code can easily
; be included in TESTCODE
;
; Delay for 6-7 seconds, to let the Enter keystroke that started the
; program come back up.
;
mov ah,2ch
int 21h ;get the current time
mov bh,dh ;set the current time aside
DelayLoop:
mov ah,2ch
push bx ;preserve start time
int 21h ;get time
pop bx ;retrieve start time
cmp dh,bh ;is the new seconds count less than
; the start seconds count?
jnb CheckDelayTime ;no
add dh,60 ;yes, a minute must have turned over,
; so add one minute
CheckDelayTime:
sub dh,bh ;get time that's passed
cmp dh,7 ;has it been more than 6 seconds yet?
jb DelayLoop ;not yet
;
include TESTCODE ;code to be measured, including calls
; to ZTimerOn and ZTimerOff
;
; Display the results.
;
call ZTimerReport
;
; Terminate the program.
;
mov ah,4ch
int 21h
Start endp
Code ends
end Start
```
As with the precision Zen timer, the program in Listing 3.6 is used by
naming the file containing the code to be timed TESTCODE, then
assembling both Listing 3.6 and Listing 3.5 with MASM or TASM and
linking the two files together by way of the Microsoft or Borland
linker. Listing 3.7 shows a batch file, named LZTIME.BAT, which does all
of the above, generating and running the executable file LZTEST.EXE.
LZTIME.BAT assumes that the file LZTIMER.ASM contains Listing 3.5 and
the file LZTEST.ASM contains Listing 3.6.
**LISTING 3.7 LZTIME.BAT**
```bat
echo off
rem
rem *** Listing 3.7 ***
rem
rem ***************************************************************
rem * Batch file LZTIME.BAT, which builds and runs the *
rem * long-period Zen timer program LZTEST.EXE to time the code *
rem * named as the command-line parameter. Listing 3.5 must be *
rem * named LZTIMER.ASM, and Listing 3.6 must be named *
rem * LZTEST.ASM. To time the code in LST3-8, you'd type the *
rem * DOS command: *
rem * *
rem * lztime lst3-8 *
rem * *
rem * Note that MASM and LINK must be in the current directory or *
rem * on the current path in order for this batch file to work. *
rem * *
rem * This batch file can be speeded up by assembling LZTIMER.ASM *
rem * once, then removing the lines: *
rem * *
rem * masm lztimer; *
rem * if errorlevel 1 goto errorend *
rem * *
rem * from this file. *
rem * *
rem * By Michael Abrash *
rem ***************************************************************
rem
rem Make sure a file to test was specified.
rem
if not x%1==x goto ckexist
echo ***************************************************************
echo * Please specify a file to test. *
echo ***************************************************************
goto end
rem
rem Make sure the file exists.
rem
:ckexist
if exist %1 goto docopy
echo ***************************************************************
echo * The specified file, "%1," doesn't exist. *
echo ***************************************************************
goto end
rem
rem copy the file to measure to TESTCODE.
:docopy
copy %1 testcode
masm lztest;
if errorlevel 1 goto errorend
masm lztimer;
if errorlevel 1 goto errorend
link lztest+lztimer;
if errorlevel 1 goto errorend
lztest
goto end
:errorend
echo ***************************************************************
echo * An error occurred while building the long-period Zen timer. *
echo ***************************************************************
:end
```
Listing 3.8 shows sample code that can be timed with the test-bed
program of Listing 3.6. Listing 3.8 measures the time required to
execute 20,000 loads of AL from memory, a length of time too long for
the precision Zen timer to handle on the 8088.
**LISTING 3.8 LST3-8.ASM**
```nasm
;
; Measures the performance of 20,000 loads of AL from
; memory. (Use by renaming to TESTCODE, which is
; included by LZTEST.ASM (Listing 3.6). LZTIME.BAT
; (Listing 3.7) does this, along with all assembly
; and linking.)
;
; Note: takes about ten minutes to assemble on a slow PC if
;you are using MASM
;
jmpSkip;jump around defined data
;
MemVardb?
;
Skip:
;
; Start timing.
;
callZTimerOn
;
rept20000
moval,[MemVar]
endm
;
; Stop timing.
;
callZTimerOff
```
When LZTIME.BAT is run on a PC with the following command line (assuming
the code in Listing 3.8 is the file LST3-8.ASM)
```sh
lztime lst3-8.asm
```
the result is 72,544 µs, or about 3.63 µs per load of AL from memory.
This is just slightly longer than the time per load of AL measured by
the precision Zen timer, as we would expect given that interrupts are
left enabled by the long-period Zen timer. The extra fraction of a
microsecond measured per `MOV` reflects the time required to execute
the BIOS code that handles the 18.2 timer interrupts that occur each
second.
Note that the command can take as much as 10 minutes to finish on a slow
PC if you are using MASM, with most of that time spent assembling
Listing 3.8. Why? Because MASM is notoriously slow at assembling
`REPT` blocks, and the block in Listing 3.8 is repeated 20,000 times.
### Using the Zen Timer from C
The Zen timer can be used to measure code performance when programming
in C—but not right out of the box. As presented earlier, the timer is
designed to be called from assembly language; some relatively minor
modifications are required before the `ZTimerOn` (start timer),
`ZTimerOff` (stop timer), and `ZTimerReport` (display timing
results) routines can be called from C. There are two separate cases to
be dealt with here: small code model and large; I'll tackle the simpler
one, the small code model, first.
Altering the Zen timer for linking to a small code model C program
involves the following steps: `C` hange `ZTimerOn` to
`_ZTimerOn`, change `ZTimerOff` to `_ZTimerOff`, change
`ZTimerReport` to `_ZTimerReport`, and change `Code` to
`_TEXT` . Figure 3.2 shows the line numbers and new states of all
lines from Listing 3.1 that must be changed. These changes convert the
code to use C-style external label names and the small model C code
segment. (In C++, use the "C" specifier, as in
```c
extern "C" ZTimerOn(void);
```
when declaring the timer routines `extern`, so that name-mangling
doesn't occur, and the linker can find the routines' C-style names.)
That's all it takes; after doing this, you'll be able to use the Zen
timer from C, as, for example, in:
```c
ZTimerOn():
for (i=0, x=0; i<100; i++)
x += i;
ZTimerOff();
ZTimerReport();
```
(I'm talking about the precision timer here. The long-period
timer—Listing 3.5—requires the same modifications, but to different
lines.)
![**Figure 3.2**  *Changes for use with small code model C.*](images/03-02.jpg)
Altering the Zen timer for use in C's large code model is a tad more
complex, because in addition to the above changes, all functions,
including the internal reference timing routines that are used to
calculate overhead so it can be subtracted out, must be converted to
far. Figure 3.3 shows the line numbers and new states of all lines from
Listing 3.1 that must be changed in order to call the Zen timer from
large code model C. Again, the line numbers are specific to the
precision timer, but the long-period timer is very similar.
The full listings for the C-callable Zen timers are presented in Chapter
K on the companion CD-ROM.
#### Watch Out for Optimizing Assemblers!
One important safety tip when modifying the Zen timer for use with large
code model C code: Watch out for optimizing assemblers! TASM actually
replaces
```nasm
call far ptr ReferenceZTimerOn
```
with
```nasm
push cs
call near ptr ReferenceZTimerOn
```
(and likewise for `ReferenceZTimerOff` ), which works because
`ReferenceZTimerOn` is in the same segment as the calling code. This
is normally a great optimization, being both smaller and faster than a
far call. However, it's not so great for the Zen
![**Figure 3.3**  *Changes for use with large code model C.*](images/03-03.jpg)
timer, because our purpose in calling the reference timing code is to
determine exactly how much time is taken by overhead code—including the
far calls to `ZTimerOn` and `ZTimerOf`f! By converting the far calls
to push/near call pairs within the Zen timer module, TASM makes it
impossible to emulate exactly the overhead of the Zen timer, and makes
timings slightly (about 16 cycles on a 386) less accurate.
What's the solution? Put the `NOSMART` directive at the start of the
Zen timer code. This directive instructs TASM to turn off all
optimizations, including converting far calls to push/near call pairs.
By the way, there is, to the best of my knowledge, no such problem with
MASM up through version 5.10A.
In my mind, the whole business of optimizing assemblers is a mixed
blessing. In general, it's nice to have the assembler shortening jumps
and selecting sign-extended forms of instructions for you. On the other
hand, the benefits of tricks like substituting push/near call pairs for
far calls are relatively small, and those tricks can get in the way when
complete control is needed. Sure, complete control is needed very
rarely, but when it is, optimizing assemblers can cause subtle problems;
I discovered TASM's alteration of far calls only because I happened to
view the code in the debugger, and you might want to do the same if
you're using a recent version of MASM.
I've tested the changes shown in Figures 3.2 and 3.3 with TASM and
Borland C++ 4.0, and also with the latest MASM and Microsoft C/C++
compiler.
#### Further Reading
For those of you who wish to pursue the mechanics of code measurement
further, one good article about measuring code performance with the 8253
timer is "Programming Insight: High-Performance Software Analysis on the
IBM PC," by Byron Sheppard, which appeared in the January, 1987 issue of
*Byte*. For complete if somewhat cryptic information on the 8253 timer
itself, I refer you to Intel's *Microsystem Components Handbook*, which
is also a useful reference for a number of other PC components,
including the 8259 Programmable Interrupt Controller and the 8237 DMA
Controller. For details about the way the 8253 is used in the PC, as
well as a great deal of additional information about the PC's hardware
and BIOS resources, I suggest you consult IBM's series of technical
reference manuals for the PC, XT, AT, Model 30, and microchannel
computers, such as the Models 50, 60, and 80.
For our purposes, however, it's not critical that you understand exactly
how the Zen timer works. All you really need to know is what the Zen
timer can do and how to use it, and we've accomplished that in this
chapter.
#### Armed with the Zen Timer, Onward and Upward
The Zen timer is not perfect. For one thing, the finest resolution to
which it can measure an interval is at best about 1µs, a period of time
in which a 66 MHz Pentium computer can execute as many as 132
instructions (although an 8088-based PC would be hard-pressed to manage
two instructions in a microsecond). Another problem is that the timing
code itself interferes with the state of the prefetch queue and
processor cache at the start of the code being timed, because the timing
code is not necessarily fetched and does not necessarily access memory
in exactly the same time sequence as the code immediately preceding the
code under measurement normally does. This prefetch effect can introduce
as much as 3 to 4 µ of inaccuracy. Similarly, the state of the prefetch
queue at the end of the code being timed affects how long the code that
stops the timer takes to execute. Consequently, the Zen timer tends to
be more accurate for longer code sequences, since the relative magnitude
of the inaccuracy introduced by the Zen timer becomes less over longer
periods.
Imperfections notwithstanding, the Zen timer is a good tool for
exploring C code and x86 family assembly language, and it's a tool we'll
use frequently for the remainder of this book.