ELH*_*ERS 2 x86 assembly 32-bit micro-optimization avx512
这是我在 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
Run Code Online (Sandbox Code Playgroud)
现在对于'chk_0x00',在x86_64系统中,没有问题,我们可以这样处理:
chk_0x00:
kmovq rbx, k0
tzcnt rbx, rbx
add rax, rbx
Run Code Online (Sandbox Code Playgroud)
这里我们有一个 64 位寄存器,因此我们可以将掩码存储到其中,但我的问题是关于 x86 系统,我们没有任何 64 位寄存器,因此我们必须使用“内存”保留(8 字节)并检查两者掩码的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
Run Code Online (Sandbox Code Playgroud)
以我的 x86 方式,我使用“kmovd”将掩码的第一个 DWORD 移动到 ebx 中,但我不知道必须对掩码的第二个 DWORD 做什么!所以我只是从内存中保留了 8 字节并将掩码(8 字节)移入其中,然后我将第二个双字移入 ebx 并再次检查......有没有更好的解决方案?(我认为我的方法不够快)用零vxorps来初始化寄存器是否正确?zmm
首先,如果您的程序很大程度上依赖于strlen大缓冲区的性能,那么您可能做错了。使用显式长度字符串(指针+长度),这样std::string您就不必扫描数据来找到结尾。
尽管如此,一些 API 使用隐式长度字符串,因此您不能总是避免它。对于短到中等的缓冲区来说,速度快通常很重要。允许过度读取缓冲区的版本使启动更加方便。
如果可以的话,首先避免使用 32 位模式;您确定手写 32 位 AVX512 asm 值得吗?
另外,您确定要使用 64 字节向量吗?在 Skylake-Xeon 上,这会限制最大睿频(在最后一个 512 位微指令之后的很长一段时间内),并且还会关闭矢量 ALU 微指令的端口 1(至少在 512 位微指令运行时)。但是,如果您已经在代码的其余部分中使用 512 位向量,那么就继续使用它,特别是如果您有足够的对齐保证。但是使用 AVX512 然后根本不展开循环似乎很奇怪,除非您需要小代码占用和良好的大情况处理之间的平衡。
即使 AVX512BW 可用,您也可能最好只使用 AVX2 strlen,并展开一些循环。或者 AVX512BW + VL 仍与掩码寄存器进行比较,但使用 32 位掩码。 或者可能不是; Skylake-X 只能vpcmpeqb k0, ymm, ymm/mem 在端口 5 上运行,并且无法微熔合内存操作数(请注意uops.info 结果中的retire_slots: 2.0 ;即使使用简单的寻址模式,它也会解码为 2 个单独的 uops)。但AVX2vpcmpeqb ymm, ymm, ymm/mem对于p01来说是1 uop,并且可以微熔断。因此,如果 L1d 能够跟上,它可以在每个时钟周期加载+比较 2x ymm,仅使用 4/时钟前端带宽中的 2 个融合域微指令。(但是检查起来会花费更多kortest)
pcmpeqAVX512 整数比较将比较谓词作为立即数(不是像 SSE/AVX /这样的操作码的一部分pcmpgt),因此这可能是阻止它微融合负载的原因。但是不,也vptestmb k1,zmm0,[ebx]不能微熔丝,否则您可以使用它或vptestnmb使用全一向量来检查内存中的零。
(请注意,微融合仅适用于具有非索引寻址模式的 Intel Skylake CPU。例如vpcmpeqb ymm1, ymm0, [ebx],而不是[ebx+eax]。请参阅微融合和寻址模式。因此请在最后使用指针递增和减法。)
如果要针对大字符串进行优化,可以一次检查两个缓存行。将指针对齐 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 个缓存行)组合为 1,并检查其是否为零。跳出循环后,它会找出第一个零的实际位置。(它的寄存器中仍然有向量,因为它使用单独的vmovdqa加载;重新加载它们将使主循环微熔断加载,使其对 HT 更加友好,但在中断后需要重新加载。)
在 SKX 上,vpminub zmm在端口 0 上运行,但可以微融合内存操作数,而vpcmpeqb zmm仅在 p5 上运行。如果数据在寄存器中,则使用vptestmb k0, zmm0,zmm0这样您就不需要一个归零的寄存器来进行比较。 将这些结合起来可以用很少的 uops 完成大量检查,从而允许无序执行窗口“看到”很远的距离,并且可能有助于内存级并行性。(跨 4k 页边界的数据预取并不完美。)
但这种优化可能只会使循环对超线程更加友好,而不会大幅提高其自身的吞吐量,并且会增加跳出循环时要排序的数据量。特别是如果您使用内存源操作数,因此原始数据不在向量寄存器中。因此,如果您关心中等长度的字符串(数百或数千字节),而不仅仅是大型的数兆字节字符串,则将内部循环限制为每次检查仅查看几个缓存行听起来很合理。
但无论如何,在 32 位代码中,您可以简单地使用 32 字节向量 -> 32 位位图重新检查候选区域。 也许vextracti64x4将 ZMM 的高半部分抓取到 YMM 中以用于 AVX2 vpcmpeqb/ vpmovmskb-> 整数寄存器
但它很小,所以您想要完全展开和优化,这就是您所要求的。
kshift+kmov是将 ak 寄存器的高半部分放入 32 位 GP 寄存器的明显方法。存储/重新加载会产生额外的延迟(例如存储转发可能需要 5 或 6 个周期),但避免了端口 5 ALU 微指令。或者可能更糟,比如 <= 10 个周期。 uops.info 的 dep 链进行测试,使存储地址依赖于加载,作为将存储/重新加载耦合到循环携带的 dep 链中的一种方式,所以我不知道这是否与提前准备好的地址不同。
与 256 位向量重新进行比较也可以作为 的替代方案kmov,例如 AVX2 vpcmpeqb ymm1, ymm0, [ebx+32]/ vpmovmskb eax, ymm1。对于任何端口来说,这都是 2 个融合域微指令,并且没有数据依赖性,k0因此乱序 exec 可以与kmov. 和kmov eax, k0都vpcmpeqb需要端口 0,所以实际上可能不是很好。(假设端口 1 上的矢量 ALU 由于最近运行 512 位微指令而仍然关闭。)
kmov eax, k0SKX 上有 3 个周期延迟。 kshiftrq在不同的端口上有 4 个周期延迟。k0因此,kmov + kshift + kmov 可以在从 kmov 和 kshift 开始执行(准备好时,或者在离开循环时分支错误预测后发出它们之后)起的7 个周期内在整数寄存器中准备好高半部分。循环分支通常在离开循环时会做出错误预测(肯定是对于大循环行程计数,但可能不会在相似长度的字符串上重复使用)。为避免数据依赖性而进行的优化可能没有帮助,例如进行单独的 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
Run Code Online (Sandbox Code Playgroud)
请注意,bsf根据其输入设置标志,而tzcnt根据结果设置标志。它是 Intel 上具有 3 个周期延迟的单个微指令,与tzcnt. AMD 速度很慢bsf,但在任何当前的 CPU 上都不支持 AVX512。 我假设 Skylake-avx512 / Cascade Lake 作为要优化的 uarch。 (和冰湖)。KNL / KNM 速度慢bsf,但 Xeon Phi 没有 AVX512BW。
使用更多指令可以缩短关键路径,例如base+32与 tzcnt / bsf 并行创建,这样我们就可以避免在 tzcnt / bsf 和 cmov 之间出现 3 组件 LEA。我想我必须推送/弹出调用保留的寄存器(如 EBX 或 EDI)才能保留所有临时寄存器。
简单lea运行在 Skylake 的 p15 上,复杂lea(3 个组件)运行在p1. 因此,它不会与任何kmov和kshift内容竞争,并且飞行端口 1 中的 512 位微指令会针对 SIMD 关闭。但tzcnt/bsf在端口 1 上运行,因此那里存在竞争。尽管如此,由于 LEA 依赖于 的输出tzcnt,资源冲突可能不是问题。Ice Lake 在每个端口上放置了 LEA 单元,可以在单个周期内处理 3 组件 LEA ( InstLatx64 )。
如果您正在使用kortest k0, k12 个单独的掩码,您可能想要用来kortest k0,k0确定第一个掩码中是否有零,然后才使用 32 位 GP 整数寄存器来区分 k0 或 k1。
bsf当其输入全为零时,不修改其目的地。 此属性由 AMD 记录,但英特尔未记录。Intel CPU 确实实现了它。您可能想要利用它,特别是如果您包含单元测试以确保它可以在您正在运行的 CPU 上运行。
但也许不是,因为它将依赖链耦合在一起,使得bsf低半部分的 依赖于tzcnt+ 。add高半部分的不过,看起来确实可以节省 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
Run Code Online (Sandbox Code Playgroud)
请注意,使用kshift到单独的寄存器中,以便我们可以首先获取高半部分(按程序顺序),从而避免需要保存/恢复任何额外的寄存器。只有 3 个架构寄存器(无需保存/恢复更多),我们可以让寄存器重命名 + OoO exec 来处理事情。
关键路径延迟并不大。从k0准备好开始,kmovd可以取出下半位图,但bsf eax, ecx不能开始,直到eax只有准备好这取决于 kshift (4) -> kmov (3) -> tzcnt (3),加上 (1) = 11 个周期,然后bsf是另外 3 个周期。
如果我们并行bsf执行操作,最好的情况是我们可以得到 tzcnt(hi) +add馈入 CMOV(1 个额外周期),该 CMOV 有来自两个 BSF 链的 2 个整数输入,以及来自下半部分的标记输入。(因此关键路径仅来自高半部分,低半部分不涉及 kshift 并且可以更快准备好)。
lea在之前的版本中,我在高半深度链上使用了 3 组件,但这也不是很好。
vplzcntq要将其用于tzcnt,您可以使用-v & vbithack 来隔离最低设置位,例如 BMI1 blsi。然后63-lzcnt = tzcnt。如果您已经有一个归零向量,则bithack 需要两条指令vpsubq+ 。vpandq(请参阅尝试编写 Gerd Isenberg 的 Bit Scan Forward 的矢量化实现作为练习)
但问题是您首先需要将 64 位掩码放回到向量元素中。以及vmovd整数 reg 的结果63-n。
有将位掩码分解为向量掩码的说明(例如vpmovm2b,但也vpbroadcastmw2d xmm1, k1可以将掩码复制到向量元素。不幸的是,它仅适用于字节或字掩码宽度(不适用于 AVX512BW),大概是用于收集或分散和vplzcnt.所以这并不能解决问题。在 64 位模式下,显然你可以kmovq使用整数 reg 和vmovq向量,但随后你只需使用标量lzcnt或。所以如果你想尝试这个,tzcnt你可能会被kmov [mem], k0/困住vmovq xmm0, [mem]; 可能最好不要这样做。
| 归档时间: |
|
| 查看次数: |
606 次 |
| 最近记录: |