x86 32位机器代码(32位整数):17个字节。
(另请参见下面的其他版本,包括16字节的32位或64位,以及DF = 1调用约定。)
调用者在寄存器中传递args,包括指向输出缓冲区末尾的指针(如我的C答案;有关算法的合理性和解释,请参阅它。) glibc内部_itoa
执行此操作,因此,它不仅仅是为代码高尔夫而设计的。传递参数的寄存器与x86-64 System V接近,除了在EAX中有一个arg而不是EDX。
返回时,EDI指向输出缓冲区中0终止的C字符串的第一个字节。通常的返回值寄存器是EAX / RAX,但是在汇编语言中,您可以使用函数方便的任何调用约定。(xchg eax,edi
最后将增加1个字节)。
呼叫者可以根据需要计算出明确的长度buffer_end - edi
。但是我认为我们不能证明省略终止符是有道理的,除非函数实际上返回了两个开始+结束指针或两个指针+长度。在此版本中,这将节省3个字节,但我认为这是没有道理的。
- EAX = n =要解码的数字。(对于
idiv
。其他args不是隐式操作数。)
- EDI =输出缓冲区的末尾(64位版本仍在使用
dec edi
,因此必须在低4GiB中)
- ESI / RSI =查找表,又名LUT。不被破坏
- ECX =表的长度=基数。不被破坏
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(手工编辑以缩小注释,行号很奇怪。)
32-bit: 17 bytes ; 64-bit: 18 bytes
; same source assembles as 32 or 64-bit
3 %ifidn __OUTPUT_FORMAT__, elf32
5 %define rdi edi
6 address %define rsi esi
11 machine %endif
14 code %define DEF(funcname) funcname: global funcname
16 bytes
22 ;;; returns: pointer in RDI to the start of a 0-terminated string
24 ;;; clobbers:; EDX (tmp remainder)
25 DEF(ascii_compress_nostring)
27 00000000 C60700 mov BYTE [rdi], 0
28 .loop: ; do{
29 00000003 99 cdq ; 1 byte shorter than xor edx,edx / div
30 00000004 F7F9 idiv ecx ; edx=n%B eax=n/B
31
32 00000006 8A1416 mov dl, [rsi + rdx] ; dl = LUT[n%B]
33 00000009 4F dec edi ; --output ; 2B in x86-64
34 0000000A 8817 mov [rdi], dl ; *output = dl
35
36 0000000C 85C0 test eax,eax ; div/idiv don't write flags in practice, and the manual says they're undefined.
37 0000000E 75F3 jnz .loop ; }while(n);
38
39 00000010 C3 ret
0x11 bytes = 17
40 00000011 11 .size: db $ - .start
令人惊讶的是,基本没有速度/大小折衷的最简单版本是最小的,但是std
/ cld
花费2个字节以stosb
降序使用并仍然遵循常见的DF = 0调用约定。(并且STOS 在存储后递减,在循环退出时指针指向一个字节太低,这使我们浪费了额外的字节来解决。)
版本:
我提出了4种截然不同的实现技巧(使用简单的mov
加载/存储(上述),使用lea
/ movsb
(整齐但不是最佳选择),使用xchg
/ xlatb
/ stosb
/ xchg
,以及一种通过重叠指令破解进入循环的方法。请参见下面的代码) 。最后一个需要0
在查找表中尾随以复制为输出字符串终止符,因此我将其计为+1字节。取决于32/64位(是否为1字节inc
)以及是否可以假定调用方设置DF = 1(stosb
降序)或其他任何值,不同的版本(并列)最短。
DF = 1以降序存储使它成为xchg / stosb / xchg的胜利,但是调用者通常不希望这样做。感觉就像以一种难以辩解的方式将工作分担给呼叫者。(与自定义arg传递和返回值寄存器不同,自定义arg-passing和返回值寄存器通常不会花费asm调用程序任何额外的工作。)但是在64位代码中,cld
/ scasb
用作inc rdi
,避免将输出指针截断为32位,因此有时不方便在64位清除函数中保留DF = 1。。(在Linux上的x86-64非PIE可执行文件中,静态代码/数据的指针是32位的,并且在Linux x32 ABI中始终如此,因此在某些情况下,可以使用使用32位指针的x86-64版本。)这种交互使得查看需求的不同组合变得很有趣。
- 具有DF = 0的IA32进入/退出调用约定:17B(
nostring
)。
- IA32:16B(使用DF = 1约定:
stosb_edx_arg
或skew
);或使用传入的DF = dontcare,将其设置为:16 + 1Bstosb_decode_overlap
或17Bstosb_edx_arg
- x86-64,带有64位指针,进入/退出调用约定为DF = 0:17 + 1字节(
stosb_decode_overlap
),18B(stosb_edx_arg
或skew
)
带有64位指针的x86-64,其他DF处理:16B(DF = 1 skew
)和17B(nostring
对于DF = 1,使用scasb
代替dec
)。18B(stosb_edx_arg
使用3个字节保留DF = 1 inc rdi
)。
或者,如果我们允许将指针返回到字符串15B之前的1个字节(末尾stosb_edx_arg
不带inc
)。 全部设置为再次调用,然后将另一个字符串扩展到具有不同基数/表的缓冲区中…… 但是如果我们不存储任何终止符0
,那将更有意义,并且您可以将函数体放入循环中,这实际上是单独的问题。
具有32位输出指针,DF = 0调用约定的x86-64:与64位输出指针相比没有任何改进,但nostring
现在绑定了18B()。
- 具有32位输出指针的x86-64:与最佳的64位指针版本相比没有任何改进,因此为16B(DF = 1
skew
)。或设置DF = 1并将其保留为17B skew
,std
但不设置cld
。或17 + 1B,stosb_decode_overlap
以/ inc edi
代替cld
/ scasb
。
使用DF = 1调用约定:16个字节(IA32或x86-64)
在输入上需要DF = 1,保持设置不变。 至少在每项功能的基础上勉强合理。执行与上述版本相同的操作,但使用xchg可以在XLATB(以R / EBX为基础的表查找)和STOSB(*output-- = al
)之前/之后将剩余部分输入AL中。
随着对入口/出口惯例正常DF = 0,所述std
/ cld
/ scasb
版本是32位和64位代码18个字节,是64位的清洁(作品具有64位输出指针)。
请注意,输入args位于不同的寄存器中,包括表的RBX(用于xlatb
)。另请注意,此循环从存储AL开始,以尚未存储的最后一个字符结束(因此mov
在末尾)。因此,该循环相对于其他循环是“偏斜的”,因此得名。
;DF=1 version. Uncomment std/cld for DF=0
;32-bit and 64-bit: 16B
157 DEF(ascii_compress_skew)
158 ;;; inputs
159 ;; O in RDI = end of output buffer
160 ;; I in RBX = lookup table for xlatb
161 ;; n in EDX = number to decode
162 ;; B in ECX = length of table = modulus
163 ;;; returns: pointer in RDI to the start of a 0-terminated string
164 ;;; clobbers:; EDX=0, EAX=last char
165 .start:
166 ; std
167 00000060 31C0 xor eax,eax
168 .loop: ; do{
169 00000062 AA stosb
170 00000063 92 xchg eax, edx
171
172 00000064 99 cdq ; 1 byte shorter than xor edx,edx / div
173 00000065 F7F9 idiv ecx ; edx=n%B eax=n/B
174
175 00000067 92 xchg eax, edx ; eax=n%B edx=n/B
176 00000068 D7 xlatb ; al = byte [rbx + al]
177
178 00000069 85D2 test edx,edx
179 0000006B 75F5 jnz .loop ; }while(n = n/B);
180
181 0000006D 8807 mov [rdi], al ; stosb would move RDI away
182 ; cld
183 0000006F C3 ret
184 00000070 10 .size: db $ - .start
一个类似的不偏斜版本会超出EDI / RDI,然后对其进行修复。
; 32-bit DF=1: 16B 64-bit: 17B (or 18B for DF=0)
70 DEF(ascii_compress_stosb_edx_arg) ; x86-64 SysV arg passing, but returns in RDI
71 ;; O in RDI = end of output buffer
72 ;; I in RBX = lookup table for xlatb
73 ;; n in EDX = number to decode
74 ;; B in ECX = length of table
75 ;;; clobbers EAX,EDX, preserves DF
76 ; 32-bit mode: a DF=1 convention would save 2B (use inc edi instead of cld/scasb)
77 ; 32-bit mode: call-clobbered DF would save 1B (still need STD, but INC EDI saves 1)
79 .start:
80 00000040 31C0 xor eax,eax
81 ; std
82 00000042 AA stosb
83 .loop:
84 00000043 92 xchg eax, edx
85 00000044 99 cdq
86 00000045 F7F9 idiv ecx ; edx=n%B eax=n/B
87
88 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
89 00000048 D7 xlatb ; al = byte [rbx + al]
90 00000049 AA stosb ; *output-- = al
91
92 0000004A 85D2 test edx,edx
93 0000004C 75F5 jnz .loop
94
95 0000004E 47 inc edi
96 ;; cld
97 ;; scasb ; rdi++
98 0000004F C3 ret
99 00000050 10 .size: db $ - .start
16 bytes for the 32-bit DF=1 version
我尝试使用lea esi, [rbx+rdx]
/ movsb
作为内循环主体的替代版本 。(每次迭代都会重置RSI,但RDI会递减)。但是它不能使用xor-zero / stos作为终止符,因此它大了1个字节。(对于LEA上没有REX前缀的查找表,它不是64位清除的。)
具有明确长度和 0终止符的LUT :16 + 1个字节(32位)
此版本设置DF = 1并保持原来的状态。我正在计算作为总字节数一部分所需的额外LUT字节。
这里很酷的技巧是使相同的字节以两种不同的方式解码。我们使用剩下的= base和商=输入数落入循环的中间,然后将0终止符复制到位。
第一次通过该函数时,循环的前3个字节被用作LEA的disp32的高字节。LEA将基数(模数)复制到EDX,idiv
产生其余部分供以后的迭代。
idiv ebp
is 的第二个字节FD
,它是std
此函数需要工作的指令的操作码。(这是一个幸运的发现。我一直在寻找这与div
早些时候,从区别自己idiv
使用/r
的ModRM位。的第2个字节div epb
解码为cmc
,这是无害的,但没有帮助的。但随着idiv ebp
我们能不能取出std
从顶部功能)。
请注意,输入寄存器再次有所不同:EBP为基数。
103 DEF(ascii_compress_stosb_decode_overlap)
104 ;;; inputs
105 ;; n in EAX = number to decode
106 ;; O in RDI = end of output buffer
107 ;; I in RBX = lookup table, 0-terminated. (first iter copies LUT[base] as output terminator)
108 ;; B in EBP = base = length of table
109 ;;; returns: pointer in RDI to the start of a 0-terminated string
110 ;;; clobbers: EDX (=0), EAX, DF
111 ;; Or a DF=1 convention allows idiv ecx (STC). Or we could put xchg after stos and not run IDIV's modRM
112 .start:
117 ;2nd byte of div ebx = repz. edx=repnz.
118 ; div ebp = cmc. ecx=int1 = icebp (hardware-debug trap)
119 ;2nd byte of idiv ebp = std = 0xfd. ecx=stc
125
126 ;lea edx, [dword 0 + ebp]
127 00000040 8D9500 db 0x8d, 0x95, 0 ; opcode, modrm, 0 for lea edx, [rbp+disp32]. low byte = 0 so DL = BPL+0 = base
128 ; skips xchg, cdq, and idiv.
129 ; decode starts with the 2nd byte of idiv ebp, which decodes as the STD we need
130 .loop:
131 00000043 92 xchg eax, edx
132 00000044 99 cdq
133 00000045 F7FD idiv ebp ; edx=n%B eax=n/B;
134 ;; on loop entry, 2nd byte of idiv ebp runs as STD. n in EAX, like after idiv. base in edx (fake remainder)
135
136 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
137 00000048 D7 xlatb ; al = byte [rbx + al]
138 .do_stos:
139 00000049 AA stosb ; *output-- = al
140
141 0000004A 85D2 test edx,edx
142 0000004C 75F5 jnz .loop
143
144 %ifidn __OUTPUT_FORMAT__, elf32
145 0000004E 47 inc edi ; saves a byte in 32-bit. Makes DF call-clobbered instead of normal DF=0
146 %else
147 cld
148 scasb ; rdi++
149 %endif
150
151 0000004F C3 ret
152 00000050 10 .size: db $ - .start
153 00000051 01 db 1 ; +1 because we require an extra LUT byte
# 16+1 bytes for a 32-bit version.
# 17+1 bytes for a 64-bit version that ends with DF=0
这种重叠的解码技巧也可以与 cmp eax, imm32
:仅花费1个字节就可以有效地向前跳4个字节,而只是破坏标志。(这对于在L1i高速缓存BTW中标记指令边界的CPU的性能非常糟糕。)
但是在这里,我们使用3个字节来复制寄存器并跳入循环。这通常需要2 + 2(mov + jmp),并且让我们直接在STOS之前而不是XLATB之前进入循环。但是然后我们需要一个单独的STD,这不会很有趣。
在线尝试!(与_start
调用者一起使用sys_write
结果)
最好通过调试在下运行它strace
,或将输出十六进制转储,这样您可以查看是否\0
在正确的位置有一个终结器,依此类推。但是您可以看到这确实有效,并产生AAAAAACHOO
了
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(实际上xxAAAAAACHOO\0x\0\0...
,因为我们将缓冲区中的2个字节之前的数据转储到固定长度。所以我们可以看到该函数写入了应有的字节,并且没有踩到不应有的字节。)传递给函数的起始指针是倒数第二个x
字符,后跟零。)