Decimal parse optimisations (#45)

* Optimised parsing functions and other minor optimisations

UnsetZ has been reduced by a byte, and between 17 and 28 cycles saved based on branching. Since branching is based on a being 0, it shouldn't have to branch very often and so be 28 cycles saved most the time. Including the initial call, the old version was 60 cycles, so this should be nearly twice as fast. 
fmtHex has been reduced by 4 bytes and between 3 and 8 cycles based on branching.
fmtHexPair had a redundant "and" removed, saving two bytes and seven cycles.
parseHex has been reduced by 7 bytes. Due to so much branching, it's hard to say if it's faster, but it should be since it's fewer operations and now conditional returns are used which are a cycle faster than conditional jumps. I think there's more to improve here, but I haven't come up with anything yet.

* Major parsing optimisations

Totally reworked both parseDecimal and parseDecimalDigit
parseDecimalDigit no longer exists, as it could be replaced by an inline alternative in the 4 places it appeared. This saves one byte overall, as the inline version is 4 bytes, 1 byte more than a call, and removing the function saved 5 bytes. It has been reduced from between 52 and 35 cycles (35 on error, so we'd expect 52 cycles to be more common unless someone's really bad at programming) to 14 cycles, so 2-3 times faster.
parseDecimal has been reduced by a byte, and now the main loop is just about twice as fast, but with increased overhead. To put this into perspective, if we ignore error cases:
For decimals of length 1 it'll be 1.20x faster, for decimals of length 2, 1.41x faster, for length 3, 1.51x faster, for length 4, 1.57x faster, and for length 5 and above, at least 1.48x faster (even faster if there's leading zeroes or not the worst case scenario).
I believe there is still room for improvement, since the first iteration can be nearly replaced with "ld l, c" since 0*10=0, but when I tried this I could either add a zero check into the main loop, adding around 40 cycles and 10 bytes, or add 20 bytes to the overhead, and I don't think either of those options are worth it.

* Inlined parseDecimalDigit

See previous commit, and /lib/parse.asm, for details

* Fixed tabs and spacing

* Fixed tabs and spacing

* Better explanation and layout

* Corrected error in comments, and a new parseHex

5 bytes saved in parseHex, again hard to say what that does to speed, the shortest possible speed is probably a little slower but I think non-error cases should be around 9 cycles faster for decimal and 18 cycles faster for hex as there's now only two conditional returns and no compliment carries.

* Fixed the new parseHex

I accidentally did `add 0xe9` without specifying `a`

* Commented the use of daa

I made the comments surrounding my use of daa much clearer, so it isn't quite so mystical what's being done here.

* Removed skip leading zeroes, added skip first multiply

Now instead of skipping leading zeroes, the first digit is loaded directly into hl without first multiplying by 10. This means the first loop is skipped in the overhead, making the method 2-3 times faster overall, and is now faster for the more common fewer digit cases too. The number of bytes is exactly the same, and the inner loop is slightly faster too thanks to no longer needing to load a into c.
To be more precise about the speed increase over the current code, for decimals of length 1 it'll be 3.18x faster, for decimals of length 2, 2.50x faster, for length 3, 2.31x faster, for length 4, 2.22x faster, and for length 5 and above, at least 2.03x faster. In terms of cycles, this is around 100+(132*length) cycles saved per decimal.

* Fixed erroring out for all number >0x1999

I fixed the errors for numbers >0x1999, sadly it is now 6 bytes bigger, so 5 bytes larger than the original, but the speed increases should still hold.

* Fixed more errors, clearer choice of constants

* Clearer choice of constants

* Moved and indented comment about fmtHex's method

* Marked inlined parseDecimalDigit uses

* Renamed .error, removed trailing whitespace, more verbose comments.
This commit is contained in:
Clanmaster21 2019-10-24 12:58:32 +01:00 committed by Virgil Dupras
parent a7aa2fc702
commit 38333e9e07
4 changed files with 103 additions and 77 deletions

View File

@ -89,7 +89,11 @@ cmdParse:
jr z, .dot
cp '$'
jr z, .eof
call parseDecimalDigit
; inline parseDecimalDigit
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
sub 0xff-9 ; maps to 0-9 and carries if not a digit
jr c, .notHandled
; straight number
ld a, ABSOLUTE
@ -121,7 +125,11 @@ cmdParse:
inc hl ; advance cmd cursor
ld a, (hl)
ld de, 1 ; if .pmNoSuffix
call parseDecimalDigit
; inline parseDecimalDigit
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
sub 0xff-9 ; maps to 0-9 and carries if not a digit
jr c, .pmNoSuffix
call .parseDecimalM ; --> DE
.pmNoSuffix:
@ -149,7 +157,11 @@ cmdParse:
.loop:
inc hl
ld a, (hl)
call parseDecimalDigit
; inline parseDecimalDigit
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
sub 0xff-9 ; maps to 0-9 and carries if not a digit
jr nc, .loop
; We're at the first non-digit char. Let's save it because we're going
; to temporarily replace it with a null.

View File

@ -1,5 +1,5 @@
; *** Requirements ***
; unsetZ
; None
;
; *** Code ***
@ -7,58 +7,71 @@
; result in A.
;
; On success, the carry flag is reset. On error, it is set.
parseDecimalDigit:
; First, let's see if we have an easy 0-9 case
cp '0'
ret c ; if < '0', we have a problem
sub '0' ; our value now is valid if it's < 10
cp 10 ; on success, C is set, which is the opposite
; of what we want
ccf ; invert C flag
ret
; Also, zero flag set if '0'
; parseDecimalDigit has been replaced with the following code inline:
; add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
; sub 0xff-9 ; maps to 0-9 and carries if not a digit
; Parse string at (HL) as a decimal value and return value in IX under the
; same conditions as parseLiteral.
; Sets Z on success, unset on error.
parseDecimal:
push hl
push de
push hl
ld a, (hl)
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
sub 0xff-9 ; maps to 0-9 and carries if not a digit
exx ; preserve bc, hl, de
ld h, 0
ld l, a ; load first digit in without multiplying
ld b, 3 ; Carries can only occur for decimals >=5 in length
jr c, .end
ld ix, 0
.loop:
ld a, (hl)
or a
jr z, .end ; success!
call parseDecimalDigit
jr c, .error
; Now, let's add A to IX. First, multiply by 10.
push ix \ pop de
add ix, ix ; x2
jr c, .error
add ix, ix ; x4
jr c, .error
add ix, ix ; x8
jr c, .error
add ix, de ; x9
jr c, .error
add ix, de ; x10
jr c, .error
exx
inc hl
ld a, (hl)
exx
; inline parseDecimalDigit
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
sub 0xff-9 ; maps to 0-9 and carries if not a digit
jr c, .end
add hl, hl ; x2
ld d, h
ld e, l ; de is x2
add hl, hl ; x4
add hl, hl ; x8
add hl, de ; x10
ld d, 0
ld e, a
add ix, de
jr c, .error
add hl, de
jr c, .end ; if hl was 0x1999, it may carry here
djnz .loop
inc hl
jr .loop
cp a ; ensure Z
jr .end
.error:
call unsetZ
inc b ; so loop only executes once more
; only numbers >0x1999 can carry when multiplied by 10.
ld de, 0xE666
ex de, hl
add hl, de
ex de, hl
jr nc, .loop ; if it doesn't carry, it's small enough
exx
inc hl
ld a, (hl)
exx
add a, 0xd0 ; the next line expects a null to be mapped to 0xd0
.end:
pop de
; Because of the add and sub in parseDecimalDigit, null is mapped
; to 0x00+(0xff-'9')-(0xff-9)=-0x30=0xd0
sub 0xd0 ; if a is null, set Z
; a is checked for null before any errors
push hl \ pop ix
exx ; restore original de and bc
pop hl
ret

View File

@ -112,14 +112,16 @@ callIY:
jp (iy)
; Ensures that Z is unset (more complicated than it sounds...)
; There are often better inline alternatives, either replacing rets with
; appropriate jmps, or if an 8 bit register is known to not be 0, an inc
; then a dec. If a is nonzero, 'or a' is optimal.
unsetZ:
push bc
ld b, a
inc b
cp b
pop bc
or a ;if a nonzero, Z reset
ret nz
cp 1 ;if a is zero, Z reset
ret
; *** STRINGS ***
; Fill B bytes at (HL) with A
@ -165,15 +167,22 @@ findchar:
pop bc
ret
; Format the lower nibble of A into a hex char and stores the result in A.
fmtHex:
and 0xf
cp 10
jr nc, .alpha ; if >= 10, we have alpha
add a, '0'
ret
.alpha:
add a, 'A'-10
; The idea here is that there's 7 characters between '9' and 'A'
; in the ASCII table, and so we add 7 if the digit is >9.
; daa is designed for using Binary Coded Decimal format, where each
; nibble represents a single base 10 digit. If a nibble has a value >9,
; it adds 6 to that nibble, carrying to the next nibble and bringing the
; value back between 0-9. This gives us 6 of that 7 we needed to add, so
; then we just condtionally set the carry and add that carry, along with
; a number that maps 0 to '0'. We also need the upper nibble to be a
; set value, and have the N, C and H flags clear.
or 0xf0
daa ; now a =0x50 + the original value + 0x06 if >= 0xfa
add a, 0xa0 ; cause a carry for the values that were >=0x0a
adc a, 0x40
ret
; Formats value in A into a string hex pair. Stores it in the memory location
@ -190,7 +199,6 @@ fmtHexPair:
dec hl
pop af
push af
and 0xf0
rra \ rra \ rra \ rra
call fmtHex
ld (hl), a
@ -198,6 +206,7 @@ fmtHexPair:
pop af
ret
; Compares strings pointed to by HL and DE up to A count of characters. If
; equal, Z is set. If not equal, Z is reset.
strncmp:

View File

@ -11,27 +11,19 @@
; On success, the carry flag is reset. On error, it is set.
parseHex:
; First, let's see if we have an easy 0-9 case
cp '0'
jr c, .error ; if < '0', we have a problem
cp '9'+1
jr nc, .alpha ; if >= '9'+1, we might have alpha
; We are in the 0-9 range
sub '0' ; C is clear
add a, 0xc6 ; maps '0'-'9' onto 0xf6-0xff
sub 0xf6 ; maps to 0-9 and carries if not a digit
ret nc
and 0xdf ; converts lowercase to uppercase
add a, 0xe9 ; map 0x11-x017 onto 0xFA - 0xFF
sub 0xfa ; map onto 0-6
ret c
; we have an A-F digit
add a, 10 ; C is clear, map back to 0xA-0xF
ret
.alpha:
call upcase
cp 'A'
jr c, .error ; if < 'A', we have a problem
cp 'F'+1
jr nc, .error ; if >= 'F', we have a problem
; We have alpha.
sub 'A'-10 ; C is clear
ret
.error:
scf
ret
; Parses 2 characters of the string pointed to by HL and returns the numerical
; value in A. If the second character is a "special" character (<0x21) we don't