尽可能快地将整数转换为汇编(64 位,NASM)中的字符串
Convert integer to string in assembly (64-bit, NASM) as fast as possible
我用 64 位 NASM 编写了一个 printint 函数,它可以将一个整数打印到 STDOUT。不过它真的很慢,在做了一些基准测试之后,我确定将整数转换为字符串是迄今为止最慢的部分。
我目前将整数转换为字符串的策略是这样的:
- 如果数字是0,跳到特殊情况。
- 如果是负数,打印一个负号,转成正整数,继续
- 创建一个 10 字节的缓冲区(足以容纳所有 32 位整数)和一个指向缓冲区后面的指针
- 检查数字是否为0;如果是,我们就完成了。
- 将数字除以10,将余数转换为ASCII
- 将余数放入缓冲区(从后往前)
- 减少缓冲区指针
- 循环回到第 4 步
我试过谷歌搜索其他人是怎么做的,它或多或少与我所做的相似,除以 10 直到数字为 0。
相关代码如下:
printint: ; num in edi
push rbp ; save base pointer
mov rbp, rsp ; place base pointer on stack
sub rsp, 20 ; align stack to keep 20 bytes for buffering
cmp edi, 0 ; compare num to 0
je _printint_zero ; 0 is special case
cmp edi, 0
jg _printint_pos ; don't print negative sign if positive
; print a negative sign (code not relevant)
xor edi, -1 ; convert into positive integer
add edi, 1
_printint_pos:
mov rbx, rsp ; set rbx to point to the end of the buffer
add rbx, 17
mov qword [rsp+8], 0 ; clear the buffer
mov word [rsp+16], 0 ; 10 bytes from [8,18)
_printint_loop:
cmp edi, 0 ; compare edi to 0
je _printint_done ; if edi == 0 then we are done
xor edx, edx ; prepare eax and edx for division
mov eax, edi
mov ecx, 10
div ecx ; divide and remainder by 10
mov edi, eax ; move quotient back to edi
add dl, 48 ; convert remainder to ascii
mov byte [rbx], dl ; move remainder to buffer
dec rbx ; shift 1 position to the left in buffer
jmp _printint_loop
_printint_done:
; print the buffer (code not relevant)
mov rsp, rbp ; restore stack and base pointers
pop rbp
ret
我如何优化它才能运行更快?或者,是否有更好的方法将整数转换为字符串?
我不想使用 printf 或 C 标准库中的任何其他函数
原来我对瓶颈来源的理解是错误的。我的基准是有缺陷的。尽管魔术数字乘法和更好的循环等微优化确实有所帮助,但最大的瓶颈是系统调用。
通过使用缓冲读写(缓冲区大小为 16 kB),我实现了比 scanf 和 printf 更快地读取和打印整数的目标。
创建一个输出缓冲区使一个特定的基准测试速度提高了 4 倍以上,而微优化将其速度提高了大约 25%。
对于以后遇到此问题的任何人 post,以下是我所做的优化:
- 将所有 sys_write 调用替换为 write_buf 调用,其中 write_buf 将输出写入缓冲区,并且仅在缓冲区已满时打印缓冲区。 write_buf 函数的实现留给 reader.
作为练习。
- 用幻数乘法代替了除法,这在每次循环迭代中节省了几个时钟周期。
- 将 while 循环改为 do...while 循环,每次循环迭代节省一条 jmp 指令(随着时间的推移会增加很多)。
- 优化单个指令(例如使用 neg 代替 xor、add)并删除冗余指令。
我可以(但没有)做的另一个潜在改进是除以更大的基数并使用查找 table,正如 phuclv 在评论中提到的。
我用 64 位 NASM 编写了一个 printint 函数,它可以将一个整数打印到 STDOUT。不过它真的很慢,在做了一些基准测试之后,我确定将整数转换为字符串是迄今为止最慢的部分。
我目前将整数转换为字符串的策略是这样的:
- 如果数字是0,跳到特殊情况。
- 如果是负数,打印一个负号,转成正整数,继续
- 创建一个 10 字节的缓冲区(足以容纳所有 32 位整数)和一个指向缓冲区后面的指针
- 检查数字是否为0;如果是,我们就完成了。
- 将数字除以10,将余数转换为ASCII
- 将余数放入缓冲区(从后往前)
- 减少缓冲区指针
- 循环回到第 4 步
我试过谷歌搜索其他人是怎么做的,它或多或少与我所做的相似,除以 10 直到数字为 0。
相关代码如下:
printint: ; num in edi
push rbp ; save base pointer
mov rbp, rsp ; place base pointer on stack
sub rsp, 20 ; align stack to keep 20 bytes for buffering
cmp edi, 0 ; compare num to 0
je _printint_zero ; 0 is special case
cmp edi, 0
jg _printint_pos ; don't print negative sign if positive
; print a negative sign (code not relevant)
xor edi, -1 ; convert into positive integer
add edi, 1
_printint_pos:
mov rbx, rsp ; set rbx to point to the end of the buffer
add rbx, 17
mov qword [rsp+8], 0 ; clear the buffer
mov word [rsp+16], 0 ; 10 bytes from [8,18)
_printint_loop:
cmp edi, 0 ; compare edi to 0
je _printint_done ; if edi == 0 then we are done
xor edx, edx ; prepare eax and edx for division
mov eax, edi
mov ecx, 10
div ecx ; divide and remainder by 10
mov edi, eax ; move quotient back to edi
add dl, 48 ; convert remainder to ascii
mov byte [rbx], dl ; move remainder to buffer
dec rbx ; shift 1 position to the left in buffer
jmp _printint_loop
_printint_done:
; print the buffer (code not relevant)
mov rsp, rbp ; restore stack and base pointers
pop rbp
ret
我如何优化它才能运行更快?或者,是否有更好的方法将整数转换为字符串?
我不想使用 printf 或 C 标准库中的任何其他函数
原来我对瓶颈来源的理解是错误的。我的基准是有缺陷的。尽管魔术数字乘法和更好的循环等微优化确实有所帮助,但最大的瓶颈是系统调用。
通过使用缓冲读写(缓冲区大小为 16 kB),我实现了比 scanf 和 printf 更快地读取和打印整数的目标。
创建一个输出缓冲区使一个特定的基准测试速度提高了 4 倍以上,而微优化将其速度提高了大约 25%。
对于以后遇到此问题的任何人 post,以下是我所做的优化:
- 将所有 sys_write 调用替换为 write_buf 调用,其中 write_buf 将输出写入缓冲区,并且仅在缓冲区已满时打印缓冲区。 write_buf 函数的实现留给 reader. 作为练习。
- 用幻数乘法代替了除法,这在每次循环迭代中节省了几个时钟周期。
- 将 while 循环改为 do...while 循环,每次循环迭代节省一条 jmp 指令(随着时间的推移会增加很多)。
- 优化单个指令(例如使用 neg 代替 xor、add)并删除冗余指令。
我可以(但没有)做的另一个潜在改进是除以更大的基数并使用查找 table,正如 phuclv 在评论中提到的。