Hex 是二进制的 ASCII 序列化格式。您首先要在寄存器中从 ASCII 十进制转换为二进制整数。 然后将该二进制转换为十六进制。 十六进制!= 二进制。
二进制 -> 十六进制很容易;每个二进制字节分别转换为两个 ASCII 十六进制数字。 (或每个 dword 到 8 个十六进制数字)。请参阅How to convert a binary integer number to a hex string? 了解简单循环,以及使用 SSE2、SSSE3、AVX2、AVX512F 或 AVX512VBMI 一次将 64 位输入转换为 16 字节十六进制的有效方法,或者使用 AVX2 甚至可以执行整个 128 位 /一步输入 16 字节,生成所有 32 字节的十六进制数字。
这就留下了 decimal-ASCII -> unsigned __int128 输入 问题。使用shld/.../shl(从高位 dword 开始)和添加 add/adc/adc/adc(从低位 dword 开始)的 128 位移位很简单,因此您可以实现通常的 total = total * 10 + digit (@ 987654322@) 但具有扩展精度的 128 位整数数学。保存一个 128 位整数需要 4 个 32 位寄存器。
将t*10 实现为t*2 + t*8 = (t*2) + (t*2)*4,首先使用3x shld 和add eax,eax 或add eax,eax + 3x adc same,same 加倍。然后复制并移动另一个 2,然后将两个 128 位数字相加。
但是只有 7 个 GP 整数寄存器(不包括堆栈指针),您必须将某些内容溢出到内存中。而且你还希望你的字符串输入指针在一个寄存器中。
因此,您可能希望在 4x 寄存器中左移 1,然后将它们溢出到内存并在寄存器中再移动 2。然后 add/3xadc 从您溢出它们的堆栈缓冲区中。 这使您可以将 4 个 reg 中的 128 位整数乘以 10,而无需使用任何额外的寄存器。
; input: total = 128-bit integer in EBX:ECX:EDX:EAX
; 16-byte tmp buffer at [esp]
; result: total *= 10 in-place
; clobbers: none
; it's traditional to keep a 64-bit integer in EDX:EAX, e.g. for div or from mul
; I chose EBX:ECX for the high half so it makes an easy-to-remember pattern.
;;; total *= 2 and copy to tmp buf
add eax, eax ; start from the low element for carry propagation
mov [esp + 0], eax
adc edx, edx
mov [esp + 4], edx
adc ecx, ecx
mov [esp + 8], ecx
adc ebx, ebx
mov [esp + 12], ebx
;;; shift that result another 2 to get total * 8
shld ebx, ecx, 2 ; start from the high element to pull in unmodified lower bits
shld ecx, edx, 2
shld edx, eax, 2
shl eax, 2
;;; add total*2 from memory to total*8 in regs to get total*10
add eax, [esp + 0]
adc edx, [esp + 4]
adc ecx, [esp + 8]
adc ebx, [esp + 12]
乱序执行在这里非常很有帮助。请注意,在shld 块中,指令不实际上依赖于之前的shld。他们从未修改的较低元素中提取位。只要第一个add eax,eax 运行,shl eax,2 就可以运行(如果前端已经发出)。
寄存器重命名使运行 SHL 成为可能,而不会因 WAR(读后写)危险而停滞。 shld edx, eax, 2 也需要 EAX 作为输入,但寄存器重命名的重点是让 CPU 跟踪 EAX 的版本与 shl eax,2 的输出分开。
这使我们可以编写不使用很多架构寄存器(仅这 4 个)的代码,但仍然可以利用更多物理寄存器来让 shld/shl 块以与程序顺序相反的顺序执行,因为输入已准备好来自 add/adc 块。
这很棒,因为这意味着最终的 add/adc 块(从内存中添加)按其需要的顺序准备好输入,而无需序列化任一指令链的延迟。这很好,因为shld 在当前的 Intel CPU(如 Haswell/Skylake)上具有 3 个周期延迟,而在 Sandybridge/IvyBridge 上是 1 个。 (在 Nehalem 和更早的版本上,它是一个 2-uop 指令,具有 2c 延迟)。但是在 Haswell/Skylake 上,它仍然是 1 uop,每时钟 1 个吞吐量。 (仅限端口 1)
Ryzen 的 shld 较慢:6 微指令,3 个周期延迟,每 3 个周期一个吞吐量。 (https://agner.org/optimize/)
我们可以有效地同时运行 3 个 add 或 shift 链,即使按照程序顺序,每个块都是单独完成的。一旦我们在第 4 个区块中添加新数字,它也可以在飞行中。
示例循环。输入 EBX:ECX:EDX = 0 和 EAX = 第一个数字,准备检查第二个字符是否为数字,然后执行total = t*10 + digit。
.digit_loop:
... earlier block ; total *= 10
add eax, ebp ; total += digit
adc edx, 0
adc ecx, 0
adc ebx, 0
.loop_entry_point:
inc esi
movzx ebp, byte ptr [esi] ; load a new input digit
sub ebp, '0' ; ASCII digit -> 0..9 integer
cmp ebp, 9 ; unless it was out of range
jbe .digit_loop
;else fall through on a non-digit.
; ESI points at the first non-digit
; EBX:ECX:EDX:EAX holds the 128-bit binary integer.
您可以将 total += digit 移动到重新加载 total*2 之前,以更好地隐藏存储转发延迟。
另一个可能的选项是 4x mul 和部分产品的必需 add/adc。如果您可以假设mulx 的 BMI2 在不影响标志的情况下相乘,那可能会很好,这样您就可以将 mulx 与 adc 交错。但是你需要10 在一个寄存器中。
另一种选择是将 XMM 寄存器用于 SSE2 64 位整数数学。或 MMX 用于 64 位 MMX regs。但是,处理 64 位元素边界并不方便,因为只有标量整数才有加法进位。但可能仍然值得,因为您只有一半的操作。
最好将 9 位整数组转换为 32 位十进制,然后进行扩展精度乘以 1e9 进行组合。 (如最后 9 位数字、之前的 9 位数字等)因此您没有为 每个 数字执行所有这些 adc / store+reload 工作。这意味着最后需要进行大量的乘法运算以组合最多四(?)组数字。
或者也许只是用一个寄存器处理前 9 位数字(正常方式),然后用第二个循环扩大到两个寄存器,然后在第 18 位之后扩大到四个数字。这对于结果小于 9 位的数字非常有用,并且只使用快速 1 寄存器累加器。