AVX512BW:使用 bsf/tzcnt 处理 32 位代码中的 64 位掩码?
AVX512BW: handle 64-bit mask in 32-bit code with bsf / tzcnt?
这是我在 AVX512BW
中 'strlen' 函数的代码
vxorps zmm0, zmm0, zmm0 ; ZMM0 = 0
vpcmpeqb k0, zmm0, [ebx] ; ebx is string and it's aligned at 64-byte boundary
kortestq k0, k0 ; 0x00 found ?
jnz .chk_0x00
现在'chk_0x00',在x86_64系统中,没有问题,我们可以这样处理:
chk_0x00:
kmovq rbx, k0
tzcnt rbx, rbx
add rax, rbx
这里我们有一个 64 位寄存器,因此我们可以将掩码存储到其中,但我的问题是关于 x86 系统的,我们没有任何 64 位寄存器,因此我们必须使用 'memory' 保留( 8-byte) 并一一检查掩码的两个DWORD(其实我就是这样,我想知道有没有更好的方法)
chk_0x00:
kmovd ebx, k0 ; move the first dword of the mask to the ebx
test ebx, ebx ; 0x00 found in the first dword ?
jz .check_next_dword
bsf ebx, ebx
add eax, ebx
jmp .done
.check_next_dword:
add eax, 32 ; 0x00 is not found in the first DWORD of the mask so we pass it by adding 32 to the length
sub esp, 8 ; reserve 8-byte from memory
kmovq [esp], k0 ; move the 8-byte MASK from k0 to our reserved memory
mov ebx, [esp+4] ; move the second DWORD of the mask to the ebx
bsf ebx, ebx
add eax, ebx
add esp, 8
在我的 x86 方式中,我使用 'kmovd' 将掩码的第一个 DWORD 移动到 ebx 中,但我不知道我必须为掩码的第二个 DWORD 做什么!!!所以我只是从内存中保留 8 字节并将掩码(8 字节)移入其中然后我将第二个双字移入 ebx 并再次检查它......有没有更好的解决方案? (我认为我的方式不够快)
使用 vxorps
将 zmm
寄存器初始化为零也是正确的吗?
看起来 KSHIFTRQ 可以用作替代方法,将 k0
计数器的高 32 位右移到低 32 位,可以将其复制到常规用途寄存器.喜欢:
.check_next_dword:
add eax, 32
KSHIFTRQ k0, k0, 32 ;shift hi 32 bits to be low 32 bits
kmovd ebx, k0
...
是的,vxorps zmm0, zmm0, zmm0
会将 zmm0
设置为零,根据 vxorps referense it's xor-ing without mask into 3-rd argument (you may check as well this 关于将 zmm 寄存器归零)
首先,如果您的程序在很大程度上依赖于 strlen
大缓冲区的性能,那么您可能做错了。使用像 std::string
这样的显式长度字符串(指针 + 长度),这样您就不必扫描数据来找到结尾。
不过,一些 API 使用隐式长度字符串,所以您不能总是避免它。对于短到中等缓冲区来说,快速通常很重要。允许过度读取其缓冲区的版本使启动更加方便。
如果可以的话,首先要避免使用 32 位模式;你确定手写 32 位 AVX512 asm 值得吗?
此外,您确定要使用 64 字节向量吗?在 Skylake-Xeon 上,这限制了最大涡轮增压(在最后一个 512 位 uop 之后很长一段时间)并且还关闭了矢量 ALU uops 的端口 1(至少在 512 位 uops 正在运行时)。但是,如果您已经在其余代码中使用 512 位向量,那么就使用它,尤其是在您有足够的对齐保证的情况下。但是使用 AVX512 然后根本不展开循环似乎很奇怪,除非您需要在小代码占用空间和良好的大案例处理之间取得平衡。
即使 AVX512BW 可用,您最好只使用 AVX2 strlen
,并展开一些循环。或 AVX512BW + VL 仍然与掩码规则进行比较,但使用 32 位掩码。 也许不是; Skylake-X只能运行 vpcmpeqb k0, ymm, ymm/mem
端口5,不能micro-fuse内存操作数(注意retire_slots:uops.info results中的2.0 ; 即使使用简单的寻址模式,它也会解码为 2 个独立的微指令)。但是 AVX2 vpcmpeqb ymm, ymm, ymm/mem
是 p01 的 1 uop,并且可以微熔断。因此,如果 L1d 能够跟上,它可以在每个时钟周期加载+比较 2x ymm,仅使用 4/时钟前端带宽中的 2 个融合域微指令。 (不过再检查的话费用会比kortest
高)
AVX512 整数比较将比较谓词作为立即数(不是像 SSE/AVX pcmpeq
/pcmpgt
那样的操作码的一部分),所以这可能是阻止它从微熔断负载。但是不,vptestmb k1,zmm0,[ebx]
can't micro-fuse either, otherwise you could use it or vptestnmb
使用全一向量来检查内存中的零。
(请注意,微融合仅适用于具有非索引寻址模式的 Intel Skylake CPU。类似于 vpcmpeqb ymm1, ymm0, [ebx]
,而不是 [ebx+eax]
。参见 Micro fusion and addressing modes。所以在末尾使用指针递增和减去。)
如果要针对大字符串进行优化,可以一次检查两个缓存行。将指针对齐 128 个字节(即通常检查最多 128 个字节的边界)。 kortestq k0,k1
与 2 个单独的掩码寄存器进行比较后,无需额外费用即可正常工作。
您可能想看看 glibc 的 AVX2 strlen 作品:https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/strlen-avx2.S.html。它的主循环(短字符串启动后)使用 vpminub
(无符号字节的最小值)将 4 个 YMM 向量(128 字节 = 2 个缓存行)合并为一个并检查其是否为零。跳出循环后,它会找出第一个零的实际位置。 (它仍然在寄存器中有向量,因为它使用单独的 vmovdqa
负载;重新加载它们会让主循环微熔断负载以更加 HT 友好,但在中断后需要重新加载。)
在 SKX 上,vpminub zmm
运行s 在端口 0 上,但可以微融合内存操作数,而 vpcmpeqb zmm
运行s 仅在 p5 上。如果数据在寄存器中,请使用 vptestmb k0, zmm0,zmm0
这样您就不需要清零的寄存器来进行比较。 结合这些可以用很少的 uops 完成大量检查,允许乱序执行 window 到 "see" 很远,可能有助于记忆级并行。 (跨 4k 页边界的数据预取并不完美。)
但这种优化可能只是使循环对超线程更友好,而没有大大提高其自身的吞吐量,并增加了当您跳出循环时要排序的数据量。特别是如果您使用内存源操作数,那么原始数据不在向量 regs 中。因此,如果您关心中等长度的字符串(数百或数千字节),而不仅仅是数兆字节的大字符串,则将内部循环限制为每次检查仅查看几个缓存行听起来很合理。
但是无论如何,在 32 位代码中,您可以简单地使用 32 字节向量 -> 32 位位图重新检查候选区域。 也许 vextracti64x4
将 ZMM 的高半部分抓取到 AVX2 的 YMM vpcmpeqb
/ vpmovmskb
-> 整数寄存器
但它很小,因此您需要完全展开和优化,这正是您要问的。
问题的实际答案:
kshift
+ kmov
是将 k 寄存器的高半部分放入 32 位 GP 寄存器的明显方法。 Store/reload 是额外的延迟(比如存储转发可能有 5 或 6 个周期)但避免了端口 5 ALU 微指令。或者更糟,比如 <= 10 个周期。 uops.info's dep chain to test that 使存储地址依赖于负载,作为将 store/reload 耦合到循环携带的 dep 链中的一种方式,所以 IDK 如果地址提前准备好会有所不同。
用 256 位向量重做比较也可以作为 kmov
的替代方法,如 AVX2 vpcmpeqb ymm1, ymm0, [ebx+32]
/ vpmovmskb eax, ymm1
。这是任何端口的 2 个融合域 uops,并且对 k0
没有数据依赖性,因此无序执行可以 运行 它与 kmov
并行。 kmov eax, k0
和 vpcmpeqb
都需要端口 0,所以它实际上可能不是很好。 (假设端口 1 上的向量 ALU 由于最近 运行ning 512-bit uops 仍然关闭。)
kmov eax, k0
has 3 cycle latency on SKX. kshiftrq
在不同的端口上有 4 个周期的延迟。因此 kmov + kshift + kmov 可以在 kmov 和 kshift 开始执行(当 k0
准备就绪时,或者在分支错误预测离开环形)。循环分支通常会在离开循环时做出错误预测(绝对是对于大循环行程计数,但可能不适用于类似长度的字符串的重复使用)。为避免数据依赖而进行优化可能没有帮助,例如进行单独的 256 位比较。
不知道无分支清理是否是最好的选择。如果第一个非零字节在低半部分,避免数据依赖于提取高半部分是非常好的。但前提是它预测得好!
;; UNTESTED
; input pointer in ecx, e.g. MS Windows fastcall
strlen_simple_aligned64_avx512_32bit:
vpxor xmm0, xmm0, xmm0 ; ZMM0 = _mm512_setzero_si512()
lea eax, [ecx+64] ; do this now to shorten the loop-exit critical path
.loop:
vpcmpeqb k0, zmm0, [ecx] ; can't micro-fuse anyway, could use an indexed load I guess
add ecx, 64
kortestq k0, k0
jnz .loop ; loop = 5 uops total :(
;;; ecx - 64 is the 64-byte block that contains a zero byte
; to branch: `kortestd k0,k0` to only look at the low 32 bits, or kmovd / test/jnz to be optimistic that it's in the low half
kmovd edx, k0 ; low bitmap
kshiftrq k0, k0, 32
sub ecx, eax ; ecx = end_base+64 - (start+64) = end_base
kmovd eax, k0 ; high bitmap
tzcnt eax, eax ; high half offset
bsf edx, edx ; low half offset, sets ZF if low==0
lea eax, [ecx + eax + 32] ; high half length = base + (32+high_offset)
;; 3-component LEA has 3 cycle latency
;; with more registers we could have just an add on the critical path here
lea ecx, [ecx + edx] ; ecx = low half length not touching flags
; flags still set from BSF(low)
cmovnz eax, ecx ; return low half if its bitmap was non-zero
vzeroupper ; or use ZMM16 to maybe avoid needing this?
ret
注意 bsf
根据其 输入 设置标志,而 tzcnt
根据结果设置标志。它是一个 uop,在 Intel 上有 3 个周期的延迟,与 tzcnt
相同。 AMD 速度较慢 bsf
但不支持任何当前 CPU 上的 AVX512。 我在这里假设 Skylake-avx512 / Cascade Lake 作为要优化的 uarch。(和 Ice Lake)。 KNL / KNM 速度较慢 bsf
但 Xeon Phi 没有 AVX512BW。
使用更多指令可以缩短关键路径,例如与 tzcnt / bsf 并行创建 base+32
,这样我们就可以避免在它和 cmov 之间出现 3 分量 LEA。我想我必须 push/pop 像 EBX 或 EDI 这样的调用保留寄存器来保留所有临时文件。
简单 lea
运行s on p15 on Skylake, complex lea
(3 component) 运行s on p1
.所以它不与任何 kmov
和 kshift
竞争,并且在飞行端口 1 中的 512 位 uops 已为 SIMD 关闭。但是 tzcnt
/bsf
运行s 在端口 1 上,所以那里存在竞争。尽管如此,由于 LEA 依赖于 tzcnt
的输出,资源冲突可能不是问题。 Ice Lake 将 LEA 单元放在每个端口上,可以在一个周期内处理 3 分量 LEA (InstLatx64)。
如果您使用 kortest k0, k1
和 2 个单独的掩码,您可能想使用 kortest k0,k0
来确定第一个掩码中是否有零,然后才用 32 位 GP 整数寄存器选择 k0 或 k1。
bsf
当其输入全为零时,其目的地保持不变。 此 属性 由 AMD 而非 Intel 记录。英特尔 CPU 确实实现了它。您可能想利用它,特别是如果您包含一个单元测试以确保它在您 运行 正在使用的 CPU 上工作。
但也许不是,因为它将依赖链耦合在一起,使得低半部分的bsf
依赖于tzcnt
+[=64= 】 上半场。不过,它看起来确实节省了 uops。 不过,根据用例的不同,延迟可能不是很重要。如果您只是计算一个绑定到其他循环的循环,则不需要立即进行,以后会有工作这与 strlen 结果无关。 OTOH 如果你要再次遍历字符串,你通常可以动态地做 strlen 。
(我也从指针增量更改为索引寻址,以一种多节省 1 uop 的方式,因为它无论如何都不会微熔断。它确实在 add
之前引入了额外的地址延迟首次加载。)
;; untested, uses BSF's zero-input behaviour instead of CMOV
;; BAD FOR LATENCY
strlen_aligned64_throughput:
vpxor xmm0, xmm0, xmm0 ; ZMM0 = _mm512_setzero_si512()
mov edx, -64
.loop:
add edx, 64
vpcmpeqb k0, zmm0, [ecx+edx] ; can't micro-fuse anyway on SKX, might as well use an indexed
kortestq k0, k0
jnz .loop ; loop = 5 uops total :(
;;; edx is the lowest index of the 64-byte block
kshiftrq k1, k0, 32
kmovd eax, k1 ; high bitmap
tzcnt eax, eax ; could also be bsf, it's just as fast on Skylake
add eax, 32 ; high index = tzcnt(high) + 32
kmovd ecx, k0 ; low bitmap
bsf eax, ecx ; index = low if non-zero, else high+32
add eax, edx ; pos = base + offset
vzeroupper
ret
注意使用 kshift
到一个单独的寄存器中,这样我们就可以先得到高半部分(按程序顺序),避免需要 save/restore 任何额外的寄存器。只有 3 个架构寄存器(没有 saving/restoring 更多),我们可以让寄存器重命名 + OoO exec 处理事情。
关键路径延迟不是很大。从k0
准备好,kmovd
可以取出低半位图,但是bsf eax, ecx
不能开始,直到eax
准备好。这取决于 kshift (4) -> kmov (3) -> tzcnt (3),加上 (1) = 11 个周期,然后 bsf
是另外 3 个周期。
如果我们并行执行 bsf
操作,最好的情况是我们可以将 tzcnt(hi) + add
送入 CMOV(1 个额外周期),其中有 2 个整数输入来自两个BSF 链接并标记来自低半部分的输入。 (所以关键路径只来自高半部分,低半部分不涉及kshift,可以更快准备好)。
在之前的版本中,我在上半部 dep 链上使用了一个 3 组件 lea
,这也不是很好。
相关:AVX512CD有SIMD vplzcntq
但是你不能将它用于 tzcnt,因为我们没有有效的位反转。
此外,您需要将 64 位掩码返回到向量元素中,然后 vmovd 到整数 reg。
有将位掩码分解为向量掩码的说明(如 VPMOVM2B
, but there's also VPBROADCASTMW2D xmm1, k1
只是将掩码复制到向量元素。不幸的是,它仅适用于字节或字掩码宽度(不适用于 AVX512BW)。所以这不解决问题。在 64 位模式下,显然你可以 kmovq
到一个整数 reg 和 vmovq
到一个向量,但是你只需要使用标量 lzcnt
或 tzcnt
这是我在 AVX512BW
中 'strlen' 函数的代码vxorps zmm0, zmm0, zmm0 ; ZMM0 = 0
vpcmpeqb k0, zmm0, [ebx] ; ebx is string and it's aligned at 64-byte boundary
kortestq k0, k0 ; 0x00 found ?
jnz .chk_0x00
现在'chk_0x00',在x86_64系统中,没有问题,我们可以这样处理:
chk_0x00:
kmovq rbx, k0
tzcnt rbx, rbx
add rax, rbx
这里我们有一个 64 位寄存器,因此我们可以将掩码存储到其中,但我的问题是关于 x86 系统的,我们没有任何 64 位寄存器,因此我们必须使用 'memory' 保留( 8-byte) 并一一检查掩码的两个DWORD(其实我就是这样,我想知道有没有更好的方法)
chk_0x00:
kmovd ebx, k0 ; move the first dword of the mask to the ebx
test ebx, ebx ; 0x00 found in the first dword ?
jz .check_next_dword
bsf ebx, ebx
add eax, ebx
jmp .done
.check_next_dword:
add eax, 32 ; 0x00 is not found in the first DWORD of the mask so we pass it by adding 32 to the length
sub esp, 8 ; reserve 8-byte from memory
kmovq [esp], k0 ; move the 8-byte MASK from k0 to our reserved memory
mov ebx, [esp+4] ; move the second DWORD of the mask to the ebx
bsf ebx, ebx
add eax, ebx
add esp, 8
在我的 x86 方式中,我使用 'kmovd' 将掩码的第一个 DWORD 移动到 ebx 中,但我不知道我必须为掩码的第二个 DWORD 做什么!!!所以我只是从内存中保留 8 字节并将掩码(8 字节)移入其中然后我将第二个双字移入 ebx 并再次检查它......有没有更好的解决方案? (我认为我的方式不够快)
使用 vxorps
将 zmm
寄存器初始化为零也是正确的吗?
看起来 KSHIFTRQ 可以用作替代方法,将 k0
计数器的高 32 位右移到低 32 位,可以将其复制到常规用途寄存器.喜欢:
.check_next_dword:
add eax, 32
KSHIFTRQ k0, k0, 32 ;shift hi 32 bits to be low 32 bits
kmovd ebx, k0
...
是的,vxorps zmm0, zmm0, zmm0
会将 zmm0
设置为零,根据 vxorps referense it's xor-ing without mask into 3-rd argument (you may check as well this
首先,如果您的程序在很大程度上依赖于 strlen
大缓冲区的性能,那么您可能做错了。使用像 std::string
这样的显式长度字符串(指针 + 长度),这样您就不必扫描数据来找到结尾。
不过,一些 API 使用隐式长度字符串,所以您不能总是避免它。对于短到中等缓冲区来说,快速通常很重要。允许过度读取其缓冲区的版本使启动更加方便。
如果可以的话,首先要避免使用 32 位模式;你确定手写 32 位 AVX512 asm 值得吗?
此外,您确定要使用 64 字节向量吗?在 Skylake-Xeon 上,这限制了最大涡轮增压(在最后一个 512 位 uop 之后很长一段时间)并且还关闭了矢量 ALU uops 的端口 1(至少在 512 位 uops 正在运行时)。但是,如果您已经在其余代码中使用 512 位向量,那么就使用它,尤其是在您有足够的对齐保证的情况下。但是使用 AVX512 然后根本不展开循环似乎很奇怪,除非您需要在小代码占用空间和良好的大案例处理之间取得平衡。
即使 AVX512BW 可用,您最好只使用 AVX2 strlen
,并展开一些循环。或 AVX512BW + VL 仍然与掩码规则进行比较,但使用 32 位掩码。 也许不是; Skylake-X只能运行 vpcmpeqb k0, ymm, ymm/mem
端口5,不能micro-fuse内存操作数(注意retire_slots:uops.info results中的2.0 ; 即使使用简单的寻址模式,它也会解码为 2 个独立的微指令)。但是 AVX2 vpcmpeqb ymm, ymm, ymm/mem
是 p01 的 1 uop,并且可以微熔断。因此,如果 L1d 能够跟上,它可以在每个时钟周期加载+比较 2x ymm,仅使用 4/时钟前端带宽中的 2 个融合域微指令。 (不过再检查的话费用会比kortest
高)
AVX512 整数比较将比较谓词作为立即数(不是像 SSE/AVX pcmpeq
/pcmpgt
那样的操作码的一部分),所以这可能是阻止它从微熔断负载。但是不,vptestmb k1,zmm0,[ebx]
can't micro-fuse either, otherwise you could use it or vptestnmb
使用全一向量来检查内存中的零。
(请注意,微融合仅适用于具有非索引寻址模式的 Intel Skylake CPU。类似于 vpcmpeqb ymm1, ymm0, [ebx]
,而不是 [ebx+eax]
。参见 Micro fusion and addressing modes。所以在末尾使用指针递增和减去。)
如果要针对大字符串进行优化,可以一次检查两个缓存行。将指针对齐 128 个字节(即通常检查最多 128 个字节的边界)。 kortestq k0,k1
与 2 个单独的掩码寄存器进行比较后,无需额外费用即可正常工作。
您可能想看看 glibc 的 AVX2 strlen 作品:https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/strlen-avx2.S.html。它的主循环(短字符串启动后)使用 vpminub
(无符号字节的最小值)将 4 个 YMM 向量(128 字节 = 2 个缓存行)合并为一个并检查其是否为零。跳出循环后,它会找出第一个零的实际位置。 (它仍然在寄存器中有向量,因为它使用单独的 vmovdqa
负载;重新加载它们会让主循环微熔断负载以更加 HT 友好,但在中断后需要重新加载。)
在 SKX 上,vpminub zmm
运行s 在端口 0 上,但可以微融合内存操作数,而 vpcmpeqb zmm
运行s 仅在 p5 上。如果数据在寄存器中,请使用 vptestmb k0, zmm0,zmm0
这样您就不需要清零的寄存器来进行比较。 结合这些可以用很少的 uops 完成大量检查,允许乱序执行 window 到 "see" 很远,可能有助于记忆级并行。 (跨 4k 页边界的数据预取并不完美。)
但这种优化可能只是使循环对超线程更友好,而没有大大提高其自身的吞吐量,并增加了当您跳出循环时要排序的数据量。特别是如果您使用内存源操作数,那么原始数据不在向量 regs 中。因此,如果您关心中等长度的字符串(数百或数千字节),而不仅仅是数兆字节的大字符串,则将内部循环限制为每次检查仅查看几个缓存行听起来很合理。
但是无论如何,在 32 位代码中,您可以简单地使用 32 字节向量 -> 32 位位图重新检查候选区域。 也许 vextracti64x4
将 ZMM 的高半部分抓取到 AVX2 的 YMM vpcmpeqb
/ vpmovmskb
-> 整数寄存器
但它很小,因此您需要完全展开和优化,这正是您要问的。
问题的实际答案:
kshift
+ kmov
是将 k 寄存器的高半部分放入 32 位 GP 寄存器的明显方法。 Store/reload 是额外的延迟(比如存储转发可能有 5 或 6 个周期)但避免了端口 5 ALU 微指令。或者更糟,比如 <= 10 个周期。 uops.info's dep chain to test that 使存储地址依赖于负载,作为将 store/reload 耦合到循环携带的 dep 链中的一种方式,所以 IDK 如果地址提前准备好会有所不同。
用 256 位向量重做比较也可以作为 kmov
的替代方法,如 AVX2 vpcmpeqb ymm1, ymm0, [ebx+32]
/ vpmovmskb eax, ymm1
。这是任何端口的 2 个融合域 uops,并且对 k0
没有数据依赖性,因此无序执行可以 运行 它与 kmov
并行。 kmov eax, k0
和 vpcmpeqb
都需要端口 0,所以它实际上可能不是很好。 (假设端口 1 上的向量 ALU 由于最近 运行ning 512-bit uops 仍然关闭。)
kmov eax, k0
has 3 cycle latency on SKX. kshiftrq
在不同的端口上有 4 个周期的延迟。因此 kmov + kshift + kmov 可以在 kmov 和 kshift 开始执行(当 k0
准备就绪时,或者在分支错误预测离开环形)。循环分支通常会在离开循环时做出错误预测(绝对是对于大循环行程计数,但可能不适用于类似长度的字符串的重复使用)。为避免数据依赖而进行优化可能没有帮助,例如进行单独的 256 位比较。
不知道无分支清理是否是最好的选择。如果第一个非零字节在低半部分,避免数据依赖于提取高半部分是非常好的。但前提是它预测得好!
;; UNTESTED
; input pointer in ecx, e.g. MS Windows fastcall
strlen_simple_aligned64_avx512_32bit:
vpxor xmm0, xmm0, xmm0 ; ZMM0 = _mm512_setzero_si512()
lea eax, [ecx+64] ; do this now to shorten the loop-exit critical path
.loop:
vpcmpeqb k0, zmm0, [ecx] ; can't micro-fuse anyway, could use an indexed load I guess
add ecx, 64
kortestq k0, k0
jnz .loop ; loop = 5 uops total :(
;;; ecx - 64 is the 64-byte block that contains a zero byte
; to branch: `kortestd k0,k0` to only look at the low 32 bits, or kmovd / test/jnz to be optimistic that it's in the low half
kmovd edx, k0 ; low bitmap
kshiftrq k0, k0, 32
sub ecx, eax ; ecx = end_base+64 - (start+64) = end_base
kmovd eax, k0 ; high bitmap
tzcnt eax, eax ; high half offset
bsf edx, edx ; low half offset, sets ZF if low==0
lea eax, [ecx + eax + 32] ; high half length = base + (32+high_offset)
;; 3-component LEA has 3 cycle latency
;; with more registers we could have just an add on the critical path here
lea ecx, [ecx + edx] ; ecx = low half length not touching flags
; flags still set from BSF(low)
cmovnz eax, ecx ; return low half if its bitmap was non-zero
vzeroupper ; or use ZMM16 to maybe avoid needing this?
ret
注意 bsf
根据其 输入 设置标志,而 tzcnt
根据结果设置标志。它是一个 uop,在 Intel 上有 3 个周期的延迟,与 tzcnt
相同。 AMD 速度较慢 bsf
但不支持任何当前 CPU 上的 AVX512。 我在这里假设 Skylake-avx512 / Cascade Lake 作为要优化的 uarch。(和 Ice Lake)。 KNL / KNM 速度较慢 bsf
但 Xeon Phi 没有 AVX512BW。
使用更多指令可以缩短关键路径,例如与 tzcnt / bsf 并行创建 base+32
,这样我们就可以避免在它和 cmov 之间出现 3 分量 LEA。我想我必须 push/pop 像 EBX 或 EDI 这样的调用保留寄存器来保留所有临时文件。
简单 lea
运行s on p15 on Skylake, complex lea
(3 component) 运行s on p1
.所以它不与任何 kmov
和 kshift
竞争,并且在飞行端口 1 中的 512 位 uops 已为 SIMD 关闭。但是 tzcnt
/bsf
运行s 在端口 1 上,所以那里存在竞争。尽管如此,由于 LEA 依赖于 tzcnt
的输出,资源冲突可能不是问题。 Ice Lake 将 LEA 单元放在每个端口上,可以在一个周期内处理 3 分量 LEA (InstLatx64)。
如果您使用 kortest k0, k1
和 2 个单独的掩码,您可能想使用 kortest k0,k0
来确定第一个掩码中是否有零,然后才用 32 位 GP 整数寄存器选择 k0 或 k1。
bsf
当其输入全为零时,其目的地保持不变。 此 属性 由 AMD 而非 Intel 记录。英特尔 CPU 确实实现了它。您可能想利用它,特别是如果您包含一个单元测试以确保它在您 运行 正在使用的 CPU 上工作。
但也许不是,因为它将依赖链耦合在一起,使得低半部分的bsf
依赖于tzcnt
+[=64= 】 上半场。不过,它看起来确实节省了 uops。 不过,根据用例的不同,延迟可能不是很重要。如果您只是计算一个绑定到其他循环的循环,则不需要立即进行,以后会有工作这与 strlen 结果无关。 OTOH 如果你要再次遍历字符串,你通常可以动态地做 strlen 。
(我也从指针增量更改为索引寻址,以一种多节省 1 uop 的方式,因为它无论如何都不会微熔断。它确实在 add
之前引入了额外的地址延迟首次加载。)
;; untested, uses BSF's zero-input behaviour instead of CMOV
;; BAD FOR LATENCY
strlen_aligned64_throughput:
vpxor xmm0, xmm0, xmm0 ; ZMM0 = _mm512_setzero_si512()
mov edx, -64
.loop:
add edx, 64
vpcmpeqb k0, zmm0, [ecx+edx] ; can't micro-fuse anyway on SKX, might as well use an indexed
kortestq k0, k0
jnz .loop ; loop = 5 uops total :(
;;; edx is the lowest index of the 64-byte block
kshiftrq k1, k0, 32
kmovd eax, k1 ; high bitmap
tzcnt eax, eax ; could also be bsf, it's just as fast on Skylake
add eax, 32 ; high index = tzcnt(high) + 32
kmovd ecx, k0 ; low bitmap
bsf eax, ecx ; index = low if non-zero, else high+32
add eax, edx ; pos = base + offset
vzeroupper
ret
注意使用 kshift
到一个单独的寄存器中,这样我们就可以先得到高半部分(按程序顺序),避免需要 save/restore 任何额外的寄存器。只有 3 个架构寄存器(没有 saving/restoring 更多),我们可以让寄存器重命名 + OoO exec 处理事情。
关键路径延迟不是很大。从k0
准备好,kmovd
可以取出低半位图,但是bsf eax, ecx
不能开始,直到eax
准备好。这取决于 kshift (4) -> kmov (3) -> tzcnt (3),加上 (1) = 11 个周期,然后 bsf
是另外 3 个周期。
如果我们并行执行 bsf
操作,最好的情况是我们可以将 tzcnt(hi) + add
送入 CMOV(1 个额外周期),其中有 2 个整数输入来自两个BSF 链接并标记来自低半部分的输入。 (所以关键路径只来自高半部分,低半部分不涉及kshift,可以更快准备好)。
在之前的版本中,我在上半部 dep 链上使用了一个 3 组件 lea
,这也不是很好。
相关:AVX512CD有SIMD vplzcntq
但是你不能将它用于 tzcnt,因为我们没有有效的位反转。
此外,您需要将 64 位掩码返回到向量元素中,然后 vmovd 到整数 reg。
有将位掩码分解为向量掩码的说明(如 VPMOVM2B
, but there's also VPBROADCASTMW2D xmm1, k1
只是将掩码复制到向量元素。不幸的是,它仅适用于字节或字掩码宽度(不适用于 AVX512BW)。所以这不解决问题。在 64 位模式下,显然你可以 kmovq
到一个整数 reg 和 vmovq
到一个向量,但是你只需要使用标量 lzcnt
或 tzcnt