每个字节是整个双精度的掩码,所以PMOVSXBQ 完全符合我们的需要:从m16 指针加载两个字节,并将它们符号扩展为两个 64 位 ( qword) 一个 xmm 寄存器的一半。
# UNTESTED CODE
# (loop setup stuff)
# RSI: double pointer
# RDI: mask pointer
# RCX: loop conter = mask byte-count
add rdi, rcx
lea rsi, [rsi + rcx*8] ; sizeof(double) = 8
neg rcx ; point to the end and count up
XORPS xmm0, xmm0 ; clear accumulator
; for real use: use multiple accumulators
; to hide ADDPD latency
ALIGN 16
.loop:
PMOVSXBQ XMM1, [RDI + RCX]
ANDPD XMM1, [RSI + RCX * 8]
ADDPD XMM0, XMM1
add RCX, 2 ; 2 bytes / doubles per iter
jl .loop
MOVHLPS XMM1, XMM0 ; combine the two parallel sums
ADDPD XMM0, XMM1
ret
实际使用时,请使用多个累加器。另请参阅Micro fusion and addressing modes re:索引寻址模式。
用内在函数编写这个应该很容易。正如其他人指出的那样,只需使用取消引用的指针作为内部函数的参数。
回答您问题的另一部分,关于如何移动数据以将其排列为PMOVSX:
在 Sandybridge 及更高版本上,使用 RAM 中的 PMOVSXBQ 可能很好。在每个周期无法处理两次加载的早期 CPU 上,一次加载 16B 掩码数据,并使用 PSRLDQ xmm1, 2 一次将其移动 2 个字节,会将 2 个字节的掩码数据放入寄存器的低 2 个字节.或者 PUNPCKHQDQ 或 PSHUFD 通过将另一个 reg 的高 64 移动到低 64 来获得两个依赖链。您必须检查哪个端口被哪个指令使用(移位与随机/提取),并查看哪些与PMOVSX 和ADDPD 的冲突较少。
punpck 和 pshufd 都在 SnB 上使用 p1/p5,pmovsx 也是如此。 addpd 只能在 p1 上运行。 andpd 只能在 p5 上运行。嗯,也许PAND 会更好,因为它可以在 p0(和 p1/p5)上运行。否则,循环中的任何内容都不会使用执行端口 0。如果将数据从整数域移动到 fp 域存在延迟损失,那么使用PMOVSX 是不可避免的,因为这将获得 int 域中的掩码数据。最好使用更多的累加器来使循环比最长的依赖链更长。但将其保持在 28 微欧左右以适合循环缓冲区,以确保每个周期可以发出 4 微欧。
还有更多关于优化整个事情的信息:
实际上并不需要对齐循环,因为在 nehalem 及以后它会适合循环缓冲区。
您应该将循环展开 2 或 4,因为 Haswell 之前的 Intel CPU 没有足够的执行单元来处理单个周期内的所有 4 个(融合的)微指令。 (3 个向量和一个融合 add/jl。这两个负载与它们所属的向量微指令融合。) Sandybridge 和更高版本可以在每个周期执行两个加载,因此每个周期进行一次迭代是可行的,除了循环开销.
哦,ADDPD 有 3 个周期的延迟。所以你需要展开并使用多个累加器来避免循环携带的依赖链成为瓶颈。可能展开 4,然后在最后总结 4 个累加器。即使使用内在函数,您也必须在源代码中这样做,因为这会改变 FP 数学的运算顺序,因此编译器可能不愿意在展开时这样做。
因此,每个展开的 4 循环将需要 4 个时钟周期,加上 1 uop 用于循环开销。在 Nehalem 上,您有一个很小的循环缓存但没有 uop 缓存,展开可能意味着您必须开始关心解码器的吞吐量。但是,在 pre-sandybridge 上,每个时钟一个负载可能无论如何都会成为瓶颈。
对于解码器吞吐量,您可能可以使用ANDPS 而不是ANDPD,后者需要少一个字节来编码。如果有帮助,请 IDK。
将此扩展到 256b ymm 寄存器将需要 AVX2 才能实现最直接的实现(对于 VPMOVSXBQ ymm)。通过执行两个 VPMOVSXBQ xmm 并将它们与 VINSERTF128 或其他东西结合起来,您可能会加速 AVX。