是否有任何方法可以通过将其加载到XMM或YMM寄存器中来获取存储在16字节或32字节缓冲区中的ASCII字符串的长度? 本质上,我正在寻找第一个零字节的索引(以位或字节为单位)。
我的目标是避免循环和分支。我希望AVX或SSE沿BSF(正向位扫描)的界线存在某些问题,但可以对字节而不是位进行操作。
也许类似以下内容?
_my_constant_time_strlen:
vpxor ymm0, ymm0
VPCMPEQB ymm0, ymm0, [rdi]
vpmovmskb eax, ymm0
bsf eax, eax
; string length is in eax?
; and rax, 31 ; editor's note: useless AND
ret
答案 0 :(得分:7)
正是您使用AVX2实现strlen
或memchr
的方式。(对于固定大小的缓冲区,您知道会有缓冲区中的某个匹配项。)
(除了现在,您有一个多余的and
)。
vpmovmskb
将比较向量转换为字节比较结果的位图,您可以使用BSF搜索该位图。您的代码已经完全可以满足您的要求。
如果避免破坏vpxor
,则可以将vpcmpeqb
归零。
在Intel CPU(Haswell / Skylake)上,实际工作只有3个uops。:vpmovmskb
是1个微融合的uop,因为您避免了索引寻址模式。 tzcnt
是1 uop,具有2到3个周期的延迟。 bsf
在Intel或AMD CPU上为1 uop。 (tzcnt
在Intel上也是1 uop)。在Intel bsf
或vpcmpeqb
上有3个周期的延迟。
因此,在主流Intel上,从准备就绪的矢量加载数据到RAX长度的总延迟为1({tzcnt
)+ 2或3(movmsk)+ 3(vpcmpeqb ymm
)= 6或7个周期。这是无分支的,只是数据依赖性,所以这是很合理的。无论地址或数据位于关键路径上,这都不会计算负载使用延迟或存储转发延迟。 而且吞吐量是优秀的,在Intel上,每个时钟1个strlen(端口0和/或端口1上出现瓶颈)。
在AMD Ryzen上,vpmovmskb ymm
为2微秒,延迟为2c。 tzcnt
是2 oups(对于Port2),延迟为3c。 lzcnt
是2微秒,延迟为2c。因此,总延迟= 7个周期,吞吐量是每2个周期1个瓶颈,这是movmsk吞吐量的瓶颈。 (Ryzen tzcnt
是1 uop / 1c延迟;大概lzcnt
是位反转+ pcmpeqb
或类似的东西。)
来自https://agner.org/optimize/的数字。
没有一条指令会扫描XMM或YMM寄存器中的第一个非零字节,唯一有效的方法是pmovmskb
/ vmovd xmm0, eax
。如果您想在向量reg中使用字节位置而不是整数,则可以在末尾vpand
。
但是通常您想要将结果保存在整数寄存器中。
唯一想到的是水平矢量搜索/扫描而没有先 movmsk首先扫描到整数的是phminposuw
,这不是您想要的。
或者1,2,4,8,16, ...
的向量为vpsadbw
的2的幂,然后0xff
对字节进行水平加法。结果的最低设置位告诉该8字节块中前0的位置。
或者您可以执行log2(vector_length)个步骤,分别对下一个元素进行改组和屏蔽,因此您将得到一个矢量,其中仅输入中的前0个具有VPAND
元素。然后0,1,2,3,4,...
的向量为vpsadbw
和vpcmpeqb
到hsum,唯一的非零元素将是字节位置。但是,如果出于某种原因确实希望将结果保存在XMM寄存器中,则与vpmovmskb
/ bsf
/ vmovd
/ tzcnt
返回XMM寄存器相比,这要昂贵得多。
如果不确定是否有零字节,请使用bsf
而不是32
,因为它会为全零输入生成bsf
(操作数大小)。 tzcnt
在AMD上也较慢,因此实际上您应该始终在此处使用bsf
。
vpxor XMM
将使目标保持不变。 (由AMD记录;英特尔说“未定义”,但仍然可以实现。)
和/或在位扫描后检查ZF以检测全零输入情况。 (tzcnt
将CF设置为全零输入,并根据输出是否为零设置ZF。)
此外,将YMM
(而不是map2_chr
)的寄存器清零。在AMD上保存uop。