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 jr z, .dot
cp '$' cp '$'
jr z, .eof 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 jr c, .notHandled
; straight number ; straight number
ld a, ABSOLUTE ld a, ABSOLUTE
@ -121,7 +125,11 @@ cmdParse:
inc hl ; advance cmd cursor inc hl ; advance cmd cursor
ld a, (hl) ld a, (hl)
ld de, 1 ; if .pmNoSuffix 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 jr c, .pmNoSuffix
call .parseDecimalM ; --> DE call .parseDecimalM ; --> DE
.pmNoSuffix: .pmNoSuffix:
@ -149,7 +157,11 @@ cmdParse:
.loop: .loop:
inc hl inc hl
ld a, (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 jr nc, .loop
; We're at the first non-digit char. Let's save it because we're going ; We're at the first non-digit char. Let's save it because we're going
; to temporarily replace it with a null. ; to temporarily replace it with a null.

View File

@ -1,5 +1,5 @@
; *** Requirements *** ; *** Requirements ***
; unsetZ ; None
; ;
; *** Code *** ; *** Code ***
@ -7,58 +7,71 @@
; result in A. ; result in A.
; ;
; On success, the carry flag is reset. On error, it is set. ; On success, the carry flag is reset. On error, it is set.
parseDecimalDigit: ; Also, zero flag set if '0'
; First, let's see if we have an easy 0-9 case ; parseDecimalDigit has been replaced with the following code inline:
cp '0' ; add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
ret c ; if < '0', we have a problem ; sub 0xff-9 ; maps to 0-9 and carries if not a digit
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
; Parse string at (HL) as a decimal value and return value in IX under the ; Parse string at (HL) as a decimal value and return value in IX under the
; same conditions as parseLiteral. ; same conditions as parseLiteral.
; Sets Z on success, unset on error. ; Sets Z on success, unset on error.
parseDecimal: parseDecimal:
push hl push hl
push de
ld ix, 0
.loop:
ld a, (hl) ld a, (hl)
or a add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
jr z, .end ; success! sub 0xff-9 ; maps to 0-9 and carries if not a digit
call parseDecimalDigit exx ; preserve bc, hl, de
jr c, .error 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
; Now, let's add A to IX. First, multiply by 10. .loop:
push ix \ pop de exx
add ix, ix ; x2 inc hl
jr c, .error ld a, (hl)
add ix, ix ; x4 exx
jr c, .error
add ix, ix ; x8 ; inline parseDecimalDigit
jr c, .error add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
add ix, de ; x9 sub 0xff-9 ; maps to 0-9 and carries if not a digit
jr c, .error
add ix, de ; x10 jr c, .end
jr c, .error
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 d, 0
ld e, a ld e, a
add ix, de add hl, de
jr c, .error jr c, .end ; if hl was 0x1999, it may carry here
djnz .loop
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 inc hl
jr .loop ld a, (hl)
exx
cp a ; ensure Z add a, 0xd0 ; the next line expects a null to be mapped to 0xd0
jr .end
.error:
call unsetZ
.end: .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 pop hl
ret ret

View File

@ -112,14 +112,16 @@ callIY:
jp (iy) jp (iy)
; Ensures that Z is unset (more complicated than it sounds...) ; 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: unsetZ:
push bc or a ;if a nonzero, Z reset
ld b, a ret nz
inc b cp 1 ;if a is zero, Z reset
cp b
pop bc
ret ret
; *** STRINGS *** ; *** STRINGS ***
; Fill B bytes at (HL) with A ; Fill B bytes at (HL) with A
@ -165,15 +167,22 @@ findchar:
pop bc pop bc
ret ret
; Format the lower nibble of A into a hex char and stores the result in A. ; Format the lower nibble of A into a hex char and stores the result in A.
fmtHex: fmtHex:
and 0xf ; The idea here is that there's 7 characters between '9' and 'A'
cp 10 ; in the ASCII table, and so we add 7 if the digit is >9.
jr nc, .alpha ; if >= 10, we have alpha ; daa is designed for using Binary Coded Decimal format, where each
add a, '0' ; nibble represents a single base 10 digit. If a nibble has a value >9,
ret ; it adds 6 to that nibble, carrying to the next nibble and bringing the
.alpha: ; value back between 0-9. This gives us 6 of that 7 we needed to add, so
add a, 'A'-10 ; 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 ret
; Formats value in A into a string hex pair. Stores it in the memory location ; Formats value in A into a string hex pair. Stores it in the memory location
@ -190,7 +199,6 @@ fmtHexPair:
dec hl dec hl
pop af pop af
push af push af
and 0xf0
rra \ rra \ rra \ rra rra \ rra \ rra \ rra
call fmtHex call fmtHex
ld (hl), a ld (hl), a
@ -198,6 +206,7 @@ fmtHexPair:
pop af pop af
ret ret
; Compares strings pointed to by HL and DE up to A count of characters. If ; 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. ; equal, Z is set. If not equal, Z is reset.
strncmp: strncmp:

View File

@ -11,27 +11,19 @@
; On success, the carry flag is reset. On error, it is set. ; On success, the carry flag is reset. On error, it is set.
parseHex: parseHex:
; First, let's see if we have an easy 0-9 case ; First, let's see if we have an easy 0-9 case
cp '0'
jr c, .error ; if < '0', we have a problem add a, 0xc6 ; maps '0'-'9' onto 0xf6-0xff
cp '9'+1 sub 0xf6 ; maps to 0-9 and carries if not a digit
jr nc, .alpha ; if >= '9'+1, we might have alpha ret nc
; We are in the 0-9 range
sub '0' ; C is clear 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 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 ; 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 ; value in A. If the second character is a "special" character (<0x21) we don't