string->digit 的基本算法是:total = total*10 + digit
,从 MSD 开始。(例如,digit = *p++ - '0'
用于 ASCII 数字字符串)。因此,最左边/最重要/第一个数字(在内存中,按阅读顺序)乘以 10 N 次,其中 N 是它之后的总位数。
这样做通常比在添加之前将每个数字乘以 10 的右幂更有效。那将需要2次乘法;一个增长 10 的幂,另一个将其应用于数字。(或 10 次幂的查表)。
当然,为了提高效率,您可以使用 SSSE3pmaddubsw
和 SSE2pmaddwd
并行地将数字乘以它们的位置值:请参阅如何使用 SIMD 实现 atoi?. 不过,当数字通常很短时,这可能不是一场胜利。当大多数数字只有几位数时,标量循环是有效的。
添加到@Michael 的答案,让 int->string 函数停在第一个 non-digit而不是固定长度可能会很有用。这将捕获诸如您的字符串之类的问题,包括用户按下回车时的换行符,以及不会12xy34
变成一个非常大的数字。(把它当作12
,就像 C 的atoi
函数)。停止字符也可以是0
C 隐式长度字符串中的终止符。
我也做了一些改进:
除非您针对代码大小进行优化,否则不要使用慢速loop
指令。忘记它的存在并使用dec
/jnz
以防倒数到零仍然是您想要做的,而不是比较指针或其他东西。
2 LEA 指令明显优于imul
+ add
:延迟更低。
将结果累积到我们想要返回的 EAX 中。(如果您将其内联而不是调用它,请使用您想要结果的任何寄存器。)
我更改了寄存器,使其遵循 x86-64 System V ABI(RDI 中的第一个参数,EAX 中的返回)。
移植到 32 位: 这完全不依赖于 64 位;只需使用 32 位寄存器即可将其移植到 32 位。(即替换rdi
为edi
、和)。注意 32 位和 64 位之间的 C 调用约定差异,例如,EDI 是保留调用的,而参数通常在堆栈上传递。但是如果你的调用者是 asm,你可以在 EDI 中传递一个 arg。rax
ecx
rax
eax
; args: pointer in RDI to ASCII decimal digits, terminated by a non-digit
; clobbers: ECX
; returns: EAX = atoi(RDI) (base 10 unsigned)
; RDI = pointer to first non-digit
global base10string_to_int
base10string_to_int:
movzx eax, byte [rdi] ; start with the first digit
sub eax, '0' ; convert from ASCII to number
cmp al, 9 ; check that it's a decimal digit [0..9]
jbe .loop_entry ; too low -> wraps to high value, fails unsigned compare check
; else: bad first digit: return 0
xor eax,eax
ret
; rotate the loop so we can put the JCC at the bottom where it belongs
; but still check the digit before messing up our total
.next_digit: ; do {
lea eax, [rax*4 + rax] ; total *= 5
lea eax, [rax*2 + rcx] ; total = (total*5)*2 + digit
; imul eax, 10 / add eax, ecx
.loop_entry:
inc rdi
movzx ecx, byte [rdi]
sub ecx, '0'
cmp ecx, 9
jbe .next_digit ; } while( digit <= 9 )
ret ; return with total in eax
这将停止转换第一个非数字字符。 通常这将是终止隐式长度字符串的 0 字节。如果你想检测尾随垃圾,你可以在循环之后检查它是一个字符串结尾,而不是其他一些非数字字符,方法是检查ecx == -'0'
(它仍然保存超出范围的整数“数字”值)。str[i] - '0'
如果您的输入是显式长度的字符串,则需要使用循环计数器而不是检查终止符(如@Michael 的答案),因为内存中的下一个字节可能是另一个数字。或者它可能位于未映射的页面中。
使第一次迭代特别并在跳入循环的主要部分之前对其进行处理称为循环剥离。剥离第一次迭代允许我们特别优化它,因为我们知道 total=0 所以不需要将任何东西乘以 10。这就像从而sum = array[0]; i=1
不是sum=0, i=0;
.
为了获得良好的循环结构(底部有条件分支),我使用了跳到循环中间进行第一次迭代的技巧。这甚至不需要额外的jmp
,因为我已经在剥离的第一次迭代中进行了分支。将循环重新排序以使if()break
中间的一个循环分支成为底部的循环分支称为循环旋转,并且可能涉及剥离第一次迭代的第一部分和最后一次迭代的第二部分。
解决在非数字上退出循环问题的简单方法是jcc
在循环体中添加 a ,就像if() break;
C 中的 . 之前的语句一样total = total*10 + digit
。但是我需要一个jmp
并且在循环中有两个分支指令,这意味着更多的开销。
如果我不需要sub ecx, '0'
循环条件的结果,我也可以lea eax, [rax*2 + rcx - '0']
将其作为 LEA 的一部分。但这会使Sandybridge 系列 CPU的 LEA 延迟 3 个周期而不是 1 个周期。eax
(3 组件 LEA 与 2 或更少。)两个 LEA 在( )上形成一个循环携带的依赖链total
,因此(尤其是对于大量数据)在 Intel 上不值得。在base + scaled-index
不比base + scaled-index + disp8
(Bulldozer-family / Ryzen)快的 CPU 上,如果您有明确的长度作为循环条件并且根本不想检查数字,那么可以肯定。
我首先使用 movzx 加载零扩展名,而不是在将数字从 ASCII 转换为整数之后这样做。(必须在某个时候添加到 32 位 EAX 中)。处理 ASCII 数字的代码通常使用字节操作数大小,例如mov cl, [rdi]
. 但这会在大多数 CPU 上创建对 RCX 旧值的错误依赖。
sub al,'0'
节省 1 个字节sub eax,'0'
,但会导致 Nehalem/Core2 上的部分寄存器停顿,在 PIII 上更糟。在所有其他 CPU 系列上都很好,甚至是 Sandybridge:它是 AL 的 RMW,因此它不会将部分 reg 与 EAX 分开重命名。但cmp al, 9
不会造成问题,因为读取字节寄存器总是没问题的。它保存了一个字节(没有 ModRM 字节的特殊编码),所以我在函数的顶部使用了它。
有关更多优化内容,请参阅http://agner.org/optimize以及x86标签 wiki中的其他链接。
标签 wiki 也有初学者链接,包括一个 FAQ 部分,其中包含指向 integer->string 函数的链接,以及其他常见的初学者问题。
有关的: