对字节求和的正常方法是psadbw
针对一个归零的寄存器,将 8 个组相加成两个 64 位半部分。(如Sum reduction of unsigned bytes without overflow, using SSE2 on Intel中所述,Fastest way to do Horizontal SSE vector sum (or other reduction))
这适用于无符号字节。(或有符号字节,如果您只关心低 8 位,即将总和截断为元素宽度。任何给出正确截断无符号字节总和的方法也必须适用于截断有符号字节,因为有符号/无符号加法是在 2 的补码机器上进行相同的二进制运算。)
要扩大有符号字节的总和,首先将范围转移到无符号,然后在最后减去偏差。通过添加 0x80 将范围从 -128..127 转移到 0..255,这与翻转高位相同,因此我们可以使用pxor
它在某些 CPU 上比paddb
) 具有更好的吞吐量。pmaddubsw
这需要一个掩码向量常数,但它仍然比 3 个 shuffle/add 或/ pmaddwd
/ pshufd
/链更有效paddd
。
您可以使用向量字节移位丢弃任何组装时间常数的字节数。保留 9 是一种特殊情况,见下文。-1, ..., -1, 0, ...
(所以是 8 或 4,只是 movq 或 movd。)如果您需要运行时变量掩码,可能会从字节加载滑动窗口,如使用未对齐缓冲区的矢量化:使用 VMASKMOVPS:从未对齐计数生成掩码?或者根本不使用那个insn
您可能会考虑将指针 arg 传递给此函数,以便可以在任何 9 字节数据上使用它(只要它不在页面末尾附近,因此可以安全地读取 16)。
;; General case, for any number of bytes from 9 .. 16
;; using SIMD for the low 8 and high 1..8
GetSumOfMasks proc
movdqu xmm1, xmmword ptr [Masks]
pslldq xmm1, 7 ; discard 7 bytes, keep the low 9
pxor xmm1, [mask_80h] ; range shift to unsigned. hoist this constant load out of a loop if inlining
pxor xmm0, xmm0 ; _mm_setzero_si128
psadbw xmm0, xmm1 ; hsum bytes into two 64-bit halves
movd eax, xmm0 ; low part
pextrw edx, xmm0, 4 ; the significant part of the high qword. 2 uops, same as punpckhqdq / movd
lea eax, [rax + rdx - 16 * 80h] ; 1 uop but worse latency than separate sub/add
; or into RAX if you want the result sign-extended to int64_t RAX
; instead of int32_t EAX
ret
endp GetSumOfMasks
.section .rdata ; or however MASM spells this directive
align 16
mask_80h db 16 dup(80h)
水平和的其他可能性包括在提取到标量之前进行,例如movhlps xmm1, xmm0
(or pshufd
) // paddd xmm0, xmm1
/ 。使用另一个向量常量,您甚至可以使用与 high->low shuffle 并行的常量,创建更多 ILP,但如果常量必须来自内存,则可能不值得。movd eax, xmm0
sub rax, 16 * 80h
paddq
-16 * 80h
使用单个lea
对吞吐量有好处,但对延迟不利;请参阅为什么用于测试 Collatz 猜想的 C++ 代码比手写汇编运行得更快?(以及https://agner.org/optimize/和https://uops.info/)了解有关慢 LEA 的详细信息(3 个组件,+
寻址模式下的两个标志,使其在 Intel 和 AMD 上运行缓慢。)Ice Lake仍然可以在端口 1 或 5 而不是任何端口上以 1 个周期延迟运行“慢 LEA”,但 SKL 和更早的版本以 3 个周期延迟运行它,因此只能在端口 1 上运行。
如果您可以将掩码生成提升到循环之外,则可以动态生成它,例如pcmpeqd xmm1,xmm1
/SSSE3 pabsb xmm1,xmm1
/psllw xmm1, 7
我能够使用 justmovd
和 SSE2pextrw
而不是movq
因为我们 8 字节的无符号总和绝对适合 16 位。这节省了代码大小(REX.W 前缀)。
9 个字节是一个有趣的特例
使用movq
向量加载获取前 8 个,使用标量movsx
获取剩余字节。这样您就不必屏蔽高半部分不需要的字节,也不需要提取 psadbw 结果的高 64 位一半。(除非您可能想要完整[Masks]
的东西在寄存器中?)
; optimized for exactly 9 bytes; SIMD low half, scalar high byte.
GetSumOfMasks proc
movq xmm1, qword ptr [Masks] ; first 8 bytes
movsx eax, byte ptr [Masks+8] ; 9th byte
pxor xmm1, [mask_80h] ; range shift to unsigned. hoist this constant load out of a loop if inlining
; note this is still a 16-byte vector load
pxor xmm0, xmm0 ; _mm_setzero_si128
psadbw xmm0, xmm1 ; hsum bytes into two 64-bit halves
movd edx, xmm0 ; low part
sub rax, 8 * 80h ; add the bias off the critical path. Only 8x biased bytes made it into the final sum
add rax, rdx
;lea eax, [rax + rdx - 8 * 80h] ; save an instruction but costs latency.
ret
endp GetSumOfMasks
要将向量常量缩小到 8 个字节,您需要使用movq
. (或者仍然对齐它,但在高 8 个字节中放置一些其他常量;这些字节完全不关心这个。)
sub
此版本通过与向量 dep 链并行执行偏置,针对 Intel pre-Ice Lake 上的延迟进行了优化。如果您的用例涉及到该 Masks 数组中的标量存储,那么您可能会在矢量加载时遇到存储转发停顿。在这种情况下,您可能应该只优化吞吐量并使其远离关键路径。但是,如果在调用它之前没有写入数据,则可能不会发生存储转发停顿。不过,如果您在向量寄存器中有数据,最好以这种方式将其传递给函数,而不是通过静态存储反弹它。
首选低 8 个 XMM 寄存器;您可以在没有 REX 前缀的情况下使用它们。此外,XMM0..5 在 Windows x64 中是完全调用破坏的,但 XMM6..15 是保留调用的。这意味着您必须保存/恢复您使用的任何内容。
(我想我记得曾经读过只有低半部分被调用保留,在这种情况下,您调用的任何函数都可能只恢复低半部分,而不是全部。但是https://docs.microsoft.com/en-us /cpp/build/x64-calling-convention?view=msvc-170说 XMM6-15(不是 XMM6L-15L)是“非易失性的”)