llvm-mos / llvm-mos-sdk

SDK for developing with the llvm-mos compiler
https://www.llvm-mos.org
Other
266 stars 53 forks source link

Optimizing a faster isdigit() #230

Open juj opened 10 months ago

juj commented 10 months ago

The implementation of isdigit() reads

https://github.com/llvm-mos/llvm-mos-sdk/blob/b3c6a274c205c4f28e16fc70f6e4615718d6581e/mos-platform/common/c/ctype.c#L20-L23

which generates the following assembly:

0000  AA                         TAX
0001  A9 00                      LDA #$00
0003  E0 30                      CPX #$30
0005  90 0D                      BCC $0014
0007  E0 3A                      CPX #$3A
0009  90 05                      BCC $0010
000B  A9 00                      LDA #$00
000D  4C 12 00                   JMP $0012
0010  A9 01        LBB0_3        LDA #$01
0012  29 01        LBB0_4        AND #$01
0014  A2 00        LBB0_5        LDX #$00
0016  A8                         TAY
0017  F0 03                      BEQ $001C
0019  A9 01                      LDA #$01
001B  60                         RTS
001C  A9 00        LBB0_7        LDA #$00
001E  60                         RTS

that is 31 bytes.

By resorting to unsigned wraparound (and changing function signature for simplicity of presentation in this ticket), the C code can be written in the following form:

bool is_digit(unsigned char ch)
{
  ch -= '0';
  return ch <= 9;
}

to get

0000  18                         CLC
0001  69 D0                      ADC #$D0
0003  C9 0A                      CMP #$0A
0005  90 03                      BCC $000A
0007  A9 00                      LDA #$00
0009  60                         RTS
000A               LBB0_2        
000A  A9 01                      LDA #$01
000C  60                         RTS

for 13 bytes.

Although, following the observation in https://github.com/llvm-mos/llvm-mos/issues/388, doing > operations is faster than doing < operations, since > operations can avoid a EOR operator. So the above C code could instead be written as

bool is_digit2(unsigned char ch)
{
  ch -= ':';
  return ch >= 245;
}

for the same effect. This can then be turned into

bool is_digit3(unsigned char ch)
{
  bool digit;
  asm(R"(
    CLC
    ADC #198
    CMP #246
    LDA #00
    ROL A
  )":"=a"(digit):"a"(ch):"c");
  return digit;
}

that would ideally be the smallest

0000  18                         CLC
0001  69 C6                      ADC #$C6
0003  C9 F6                      CMP #$F6
0005  A9 00                      LDA #$00
0007  2A                         ROL A
0008  29 01                      AND #$01 ; redundant
000A  60                         RTS

for 11 bytes. Although compiler insists on emitting the AND #$01 here (https://github.com/llvm-mos/llvm-mos/issues/389), so the above could be two bytes shorter still.

TheHans255 commented 10 months ago

I would suggest replacing ':`` with'9' + 1inisdigit_2`, like so:

bool is_digit2(unsigned char ch)
{
  ch -= ('9' + 1);
  return ch >= 245;
}

and adapting isdigit_3 accordingly. While it is a good assumption in most character sets to assume that '0' through '9' are contiguous and in order, ':' coming immediately after '9' is specific to ASCII, which 65XX platforms (especially game consoles) don't always use.