如何在XMM中对有符号字节进行水平求和 [英] How to horizontally sum signed bytes in XMM

查看:0
本文介绍了如何在XMM中对有符号字节进行水平求和的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在用x64汇编和SIMD编写一些代码。

xmm15寄存器中打包了9个字节。为简单起见,让我们看一下以下代码:

.data
Masks BYTE 0, -1, 0, -1, 5, -1, 0, -1, 0

.code
GetSumOfMasks proc

movdqu xmm15, xmmword ptr [Masks]
; xmm15 now contains { 0,-1,0,-1,5,-1,0,-1,0,0,0,0,0,0,0,0 }

; sum the elements horizontally - the result should be 1
; convert the sum to a QWORD and put it in RAX

GetSumOfMasks endp

如何获取xmm15中元素的水平总和?

我已尝试haddps,但它似乎只适用于无符号的DWORD,并且我找不到替代的字节操作。

在这种情况下,我可以使用什么SSE指令对有符号字节求和?

推荐答案

对字节求和的正常方法是对置零寄存器进行求和,将每组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具有更好的吞吐量)。这需要一个掩码向量常量,但它仍然比3个随机加/加或pmaddubsw/pmaddwd/pshufd/paddd的链更有效。

您可以使用向量字节移位丢弃任何汇编时间恒定数量的字节。Keep 9是一个特例,见下文。(8或4也是一样的,只是移动或移动。)如果您需要运行时变量屏蔽,可以从-1, ..., -1, 0, ...字节加载一个滑动窗口,如Vectorizing with unaligned buffers: using VMASKMOVPS: generating a mask from a misalignment count? Or not using that insn at all

所示

您可以考虑将指针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(或pshufd)/paddd xmm0, xmm1/movd eax, xmm0/sub rax, 16 * 80h。使用另一个向量常量,您甚至可以paddq将一个-16 * 80h常量与高低置乱并行,从而创建更多ILP,但如果该常量必须来自内存,则可能不值得这样做。

使用单个lea有利于提高吞吐量,但不利于延迟;有关Slow-LEA的详细信息,请参阅Why does C++ code for testing the Collatz conjecture run faster than hand-written assembly?(和https://agner.org/optimize/https://uops.info/)(寻址模式下使用3个组件、两个+符号,会使Intel和AMD的速度变慢。)Ice Lake仍然可以在端口1或5而不是任何端口上以1个周期延迟运行LEA和Slow LEA,但SKL和更早版本以3个周期延迟运行它,因此仅在端口1上运行。

如果您可以将掩码生成提升到循环之外,则could generate it on the fly,例如pcmpeqd xmm1,xmm1/SSSE3pabsb xmm1,xmm1/psllw xmm1, 7

我只能使用movd和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个字节中放入其他常量;这些字节完全不需要考虑这一点。)

此版本针对Intel Pre-Ice Lake上的延迟进行了优化,方法是在向量解析链的同时执行sub偏置。如果您用例涉及到掩码数组中的标量存储,那么无论如何,使用向量加载都可能遇到存储转发停滞。在这种情况下,您可能应该只针对吞吐量进行优化,并使其远离关键路径。但是,如果在调用此函数之前没有正确写入数据,则可能不会发生存储转发停滞。不过,如果数据位于向量寄存器中,最好将其以这种方式传递给函数,而不是通过静态存储跳过它。


首选较低的8个XMM寄存器;可以使用不带REX前缀的寄存器。此外,在Windows x64中,XMM0..5被完全取消调用,但XMM6..15被保留调用。这意味着您必须保存/恢复您使用的所有内容。

(我记得我曾经读到过,只有低半部分是调用保留的,在这种情况下,您调用的任何函数可能只恢复低半部分,而不是全部。但https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170表示XMM6-15(不是XMM6L-15L)是非易失性的

这篇关于如何在XMM中对有符号字节进行水平求和的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆