157 lines
No EOL
7.5 KiB
Markdown
157 lines
No EOL
7.5 KiB
Markdown
**LISTING 16.4 L16-4.ASM**
|
|
|
|
; Assembly subroutine for Listing 16.2. Scans through Buffer, of
|
|
; length BufferLength, counting words and updating WordCount as
|
|
; appropriate, using a lookup table-based approach. BufferLength
|
|
; must be > 0. *CharFlag and *WordCount should equal 0 on the
|
|
; first call. Tested with TASM.
|
|
; C near-callable as:
|
|
; void ScanBuffer(char *Buffer, unsigned int BufferLength,
|
|
; char *CharFlag, unsigned long *WordCount);
|
|
|
|
parms struc
|
|
dw 2 dup(?) ;pushed return address & BP
|
|
Buffer dw ? ;buffer to scan
|
|
BufferLength dw ? ;length of buffer to scan
|
|
CharFlag dw ? ;pointer to flag for state of last
|
|
;char processed on entry (0 on
|
|
;initial call). Updated on exit
|
|
WordCount dw ? ;pointer to 32-bit count of words
|
|
; found (0 on initial call)
|
|
parms ends
|
|
|
|
.model small
|
|
.data
|
|
; Table of char/not statuses for byte values 0-255 (128-255 are
|
|
; duplicates of 0-127 to effectively mask off bit 7, which some
|
|
; word processors set as an internal flag).
|
|
CharStatusTable label byte
|
|
REPT 2
|
|
db 39 dup(0)
|
|
db 1 ;apostrophe
|
|
db 8 dup(0)
|
|
db 10 dup(1) ;0-9
|
|
db 7 dup(0)
|
|
db 26 dup(1) ;A-Z
|
|
db 6 dup(0)
|
|
db 26 dup(1) ;a-z
|
|
db 5 dup(0)
|
|
ENDM
|
|
|
|
.code
|
|
public _ScanBuffer
|
|
_ScanBuffer proc near
|
|
push bp ;preserve caller's stack frame
|
|
mov bp,sp ;set up local stack frame
|
|
push si ;preserve caller's register vars
|
|
push di
|
|
|
|
mov si,[bp+Buffer] ;point to buffer to scan
|
|
mov bx,[bp+WordCount]
|
|
mov di,[bx] ;get current 32-bit word count
|
|
mov dx,[bx+2]
|
|
mov bx,[bp+CharFlag]
|
|
mov al,[bx] ;get current CharFlag
|
|
mov cx,[bp+BufferLength] ;get # of bytes to scan
|
|
mov bx,offset CharStatusTable
|
|
ScanLoop:
|
|
and al,al ;ZF=0 if last byte was a char,
|
|
; ZF=1 if not
|
|
lodsb ;get the next byte
|
|
;***doesn't change flags***
|
|
xlat ;look up its char/not status
|
|
;***doesn't change flags***
|
|
jz ScanLoopBottom ;don't count a word if last byte was
|
|
; not a character
|
|
and al,al ;last byte was a character; is the
|
|
; current byte a character?
|
|
jz CountWord ;no, so count a word
|
|
ScanLoopBottom:
|
|
dec cx ;count down buffer length
|
|
jnz ScanLoop
|
|
Done:
|
|
mov si,[bp+CharFlag]
|
|
mov [si],al ;set new CharFlag
|
|
mov bx,[bp+WordCount]
|
|
mov [bx],di ;set new word count
|
|
mov [bx+2],dx
|
|
|
|
pop di ;restore caller's register vars
|
|
pop si
|
|
pop bp ;restore caller's stack frame
|
|
ret
|
|
|
|
align 2
|
|
CountWord:
|
|
add di,1 ;increment the word count
|
|
adc dx,0
|
|
dec cx ;count down buffer length
|
|
jnz ScanLoop
|
|
jmp Done
|
|
_ScanBuffer endp
|
|
end
|
|
|
|
|
|
Listing 16.4 features several interesting tricks. First, it uses
|
|
**LODSB** and **XLAT** in succession, a very neat way to get a
|
|
pointed-to byte, advance the pointer, and look up the value indexed by
|
|
the byte in a table, all with just two instruction bytes.
|
|
(Interestingly, Listing 16.4 would probably run quite a bit better still
|
|
on an 8088, where **LODSB** and **XLAT** have a greater advantage over
|
|
conventional instructions. On the 486 and Pentium, however, **LODSB**
|
|
and **XLAT** lose much of their appeal, and should be replaced with
|
|
**MOV** instructions.) Better yet, **LODSB** and **XLAT** don't alter
|
|
the flags, so the Zero flag status set before **LODSB** is still around
|
|
to be tested after **XLAT** .
|
|
|
|
Finally, if you look closely, you will see that Listing 16.4 jumps out
|
|
of the loop to increment the word count in the case where a word is
|
|
actually found, with a duplicate of the loop-bottom code placed after
|
|
the code that increments the word count, to avoid an extra branch back
|
|
into the loop; this replaces the more intuitive approach of jumping
|
|
around the incrementing code to the loop bottom when a word isn't found.
|
|
Although this incurs a branch every time a word is found, a word is
|
|
typically found only once every 5 or 6 bytes; on average, then, a branch
|
|
is saved about two-thirds of the time. This is an excellent example of
|
|
how understanding the nature of the data you're processing allows you to
|
|
optimize in ways the compiler can't. *Know your data!*
|
|
|
|
So, gosh, Listing 16.4 is the best word-counting code in the universe,
|
|
right? Not hardly. If there's one thing my years of toil in this vale of
|
|
silicon have taught me, it's that there's never a lack of potential for
|
|
further optimization. *Never!* Off the top of my head, I can think of at
|
|
least three ways to speed up Listing 16.4; and, since Turbo Profiler
|
|
reports that even in Listing 16.4, 88 percent of the time is spent
|
|
scanning the buffer (as opposed to reading the file), there's potential
|
|
for those further optimizations to improve performance significantly.
|
|
(However, it is true that when access is performed to a hard rather than
|
|
RAM disk, disk access jumps to about half of overall execution time.)
|
|
One possible optimization is unrolling the loop, although that is truly
|
|
a last resort because it tends to make further changes extremely
|
|
difficult.
|
|
|
|
------------------- -----------------------------------------------------------
|
|
 *Exhaust all other optimizations before unrolling loops.*
|
|
------------------- -----------------------------------------------------------
|
|
|
|
### Challenges and Hazards {#Heading5}
|
|
|
|
The challenge I put to the readers of *PC TECHNIQUES* was to write a
|
|
faster module to replace Listing 16.4. The author of the code that
|
|
counted the words in my secret test file fastest on my 20 MHz cached 386
|
|
would be the winner and receive Numerous Valuable Prizes.
|
|
|
|
No listings were to be longer than 200 lines. No complete programs were
|
|
to be accepted; submissions had to be plug-compatible with Listing 16.4.
|
|
(This was to encourage people not to waste time optimizing outside the
|
|
inner loop.) Finally, the code had to produce the same results as
|
|
Listing 16.4; I didn't want to see functions that approximated the word
|
|
count by dividing the number of characters by six instead of counting
|
|
actual words!
|
|
|
|
So how did the entrants in this particular challenge stack up? More than
|
|
one claimed a speed-up over my assembly word-counting code of more than
|
|
three times. On top of the three-times speedup over the original C code
|
|
that I had already realized, we're almost up to an order of magnitude
|
|
faster. You are, of course, entitled to your own opinion, but *I*
|
|
consider an order of magnitude to be significant. |