assembly – 最小操作码大小x86-64 strlen实现

我正在调查我的代码高尔夫/二进制可执行文件的最小操作码大小x86-64 strlen实现,它不应该超过某个大小(为简单起见,请考虑使用demoscene).

一般的想法来自
here,尺寸优化的想法从
here
here.

输入字符串地址在rdi中,最大长度不应大于Int32

xor   eax,eax ; 2 bytes
or    ecx,-1  ; 3 bytes
repne scasb   ; 2 bytes
not   ecx     ; 2 bytes
dec   ecx     ; 2 bytes

最终结果是ecx,总共11个字节.

问题是将ecx设置为-1

备选方案1已经说明

or ecx,-1 ; 3 bytes

选项2

lea ecx,[rax-1] ; 3 bytes 

选项3

stc         ; 1 byte
sbb ecx,ecx ; 2 bytes

选项4,可能是最慢的一个

push -1 ; 2 bytes
pop rcx ; 1 byte

我明白那个:
选项1依赖于先前的ecx值
选项2依赖于先前的rax值
选项3我不确定它是否依赖于之前的ecx值?
选项4是最慢的?

这里有明显的赢家吗?
标准是保持操作码的大小尽可能小,并选择最佳性能.
我完全知道有使用现代cpu指令的实现,但这种传统方法似乎是最小的.

最佳答案 对于一个hacky good-enough版本,我们知道rdi有一个有效的地址. edi很可能不是一个小整数,因此2字节mov ecx,edi.使用前请检查所有呼叫站点是否安全!

如果你只是希望rdi指向终止的0字节,而不是实际需要计数,这是很好的.或者,如果您在另一个寄存器中有开始指针,那么您可以执行sub edi,edx或其他方式并获得该方式的长度,而不是处理rcx结果. (如果你知道结果适合32位,你不需要sub rdi,rdx,因为你知道它的高位也是零.而高输入位不会影响add / sub的低输出位;进位从左到右传播.)

对于已知低于255字节的字符串,可以使用mov cl,-1(2字节).这使得rcx至少为0xFF,并且取决于其中剩余的高垃圾量. (这在Nehalem上有一个部分注册失速,在读取RCX时更早,否则只是对旧RCX的依赖).无论如何,然后mov al,-2 / sub al,cl得到长度为8位整数.这可能有用也可能没用.

根据调用者的不同,rcx可能已经持有指针值,在这种情况下,如果可以使用指针减法,则可以保持不变.

你提出的选项中有哪些

lea ecx,[rax-1]是非常好的,因为你只是xor-zeroed eax,它是一个便宜的1 uop指令,具有1个周期延迟,并且可以在所有主流CPU上的多个执行端口上运行.

如果已经有另一个具有已知常量值的寄存器,尤其是具有xor-zeroed的寄存器,则3字节lea几乎总是最有效的3字节方式来创建常量,如果有效的话. (见Set all bits in CPU register to 1 efficiently).

I’m fully aware there are implementations using modern cpu instructions, but this legacy approach seems the smallest one.

是的,repne scasb非常紧凑.它的启动开销可能类似于典型Intel CPU上的15个周期,根据Agner Fog,它发出> = 6n uops,吞吐量> = 2n个周期,其中n是计数(即它比较的每个字节2个周期)对于长期比较,隐藏启动开销),因此它使lea的成本相形见绌.

对ecx有依赖性的东西可能会延迟它的启动,所以你绝对想要lea.

repne scasb可能足够快,无论你做什么,但它比pcmpeqb / pmovmsbk / cmp慢.对于短的固定长度字符串,当长度为4或8个字节(包括终止0)时,整数cmp / jne非常好,假设您可以安全地过度读取字符串,即您不必担心“”在页面的末尾.但是,此方法的开销随字符串长度而变化.例如,对于字符串长度= 7,您可以执行4,2和1个操作数大小,或者您可以执行两个双字节比较重叠1个字节.比如cmp dword [rdi],first_4_bytes / jne; cmp dword [rdi 3],last_4_bytes / jne.

关于LEA的更多细节

在Sandybridge系列CPU上,可以将lea以与它相同的周期发送到执行单元,并将xor-zero发送到无序CPU内核. xor-zeroing在发出/重命名阶段处理,因此uop以“已执行”状态进入ROB.指令不可能等待RAX. (除非在xor和lea之间发生中断,但即便如此,我认为在恢复RAX之后和lea执行之前会有一个序列化指令,所以它不能等待.)

简单的lea可以在SnB上的port0或port1上运行,也可以在Skylake上的port1 / port5上运行(每个时钟吞吐量2个,但有时在不同的SnB系列CPU上有不同的端口).这是1个周期的延迟,因此很难做得更好.

你不太可能看到使用mov ecx,-1(5字节)可以在任何ALU端口上运行的任何加速.

在AMD Ryzen,lea r32,[m]在64位模式下被视为“慢”LEA,只能在2个端口上运行,并且具有2c延迟而不是1.更糟糕的是,Ryzen不会消除xor-zeroing .

您所做的微基准测试仅测量没有错误依赖性而非延迟的版本的吞吐量.这通常是一个有用的衡量标准,你确实得到了正确的答案,即lea是最佳选择.

纯粹的吞吐量是否准确反映了您的真实用例的任何内容是另一回事.实际上,如果字符串比较在关键路径上作为长链路或循环传输数据依赖链的一部分而未被jcc破坏以提供分支预测推测执行,则实际上可能依赖于延迟而不是吞吐量. (但是无分支代码通常更大,所以这不太可能).

stc / sbb ecx,ecx很有意思,但只有AMD CPU将sbb视为依赖性破坏(仅依赖于CF,而不是整数寄存器).在Intel Haswell和更早版本中,sbb是一个2 uop指令(因为它有3个输入:2个GP整数标志).它具有2c延迟,这就是它表现如此糟糕的原因. (延迟是一个循环传输的dep链.)

缩短序列的其他部分

根据你正在做的事情,你也可以使用strlen 2,但是可以抵消另一个常量或其他东西. dec ecx在32位代码中只有1个字节,但x86-64没有短形式的inc / dec指令.所以not / dec在64位代码中并不那么酷.

在repne scas之后,你有ecx = -len – 2(如果你从ecx = -1开始),并且给你-x-1(即len 2 – 1`).

 ; eax = 0
 ; ecx = -1
repne scasb      ; ecx = -len - 2
sub   eax, ecx   ; eax = +len + 2
点赞