From 38333e9e0764c10097aec93b85ad6a1c9e9393df Mon Sep 17 00:00:00 2001 From: Clanmaster21 Date: Thu, 24 Oct 2019 12:58:32 +0100 Subject: [PATCH] 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. --- apps/ed/cmd.asm | 18 ++++++++-- apps/lib/parse.asm | 97 +++++++++++++++++++++++++++++++----------------------- kernel/core.asm | 35 ++++++++++++-------- kernel/parse.asm | 30 +++++++---------- 4 files changed, 103 insertions(+), 77 deletions(-) diff --git a/apps/ed/cmd.asm b/apps/ed/cmd.asm index e7b8f23..7d4c7da 100644 --- a/apps/ed/cmd.asm +++ b/apps/ed/cmd.asm @@ -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. diff --git a/apps/lib/parse.asm b/apps/lib/parse.asm index 6ab6e16..c51aad2 100644 --- a/apps/lib/parse.asm +++ b/apps/lib/parse.asm @@ -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 - - diff --git a/kernel/core.asm b/kernel/core.asm index 1289351..be41e97 100644 --- a/kernel/core.asm +++ b/kernel/core.asm @@ -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: diff --git a/kernel/parse.asm b/kernel/parse.asm index 1bd81b2..8f7397b 100644 --- a/kernel/parse.asm +++ b/kernel/parse.asm @@ -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