SSE XMM 点积说明

Posted

技术标签:

【中文标题】SSE XMM 点积说明【英文标题】:SSE XMM dot product explanation 【发布时间】:2018-05-29 19:33:59 【问题描述】:

我无法理解汇编中的某段代码。任务是使用 SSE 算法和 XMM 寄存器找到 2 个向量的点积。方法是一次读取 4 个浮点数的向量(意味着一个 xmm 寄存器将在一次迭代中保存四个)。最终结果是一个 xmm 寄存器,每个字节保存给定向量的乘积 (x1*y1 +...) 的总和。

我没有得到的是之后的部分。将这些“结束”字节全部相加所需的所有内容,基本上是对构成最终寄存器的 4 个字节进行求和。我试图在这方面找到一些东西,但没有占上风。我得到的超出了我的理解,我什至尝试在纸上写下每一个计算,没有什么意义。在突出显示的部分,计算实际总和并将其存储在xmm0 的最低字节中。欢迎对此提出任何见解。

.intel_syntax noprefix

.data
two: .int 2

.text
.global dot_product

############################################################################
##
## Function:
##
## void dot_product(float *x, float *y, int n, float *r);
##
## calculates the dot product of x and y (n lengths) and stores the result 
## in r
##
## -- float * x -- rdi -- 
## -- float * y -- rsi -- 
## -- int n -- rdx -- 
## -- float * r -- rcx -- 
##
############################################################################
dot_product:

        enter   0, 0


        mov r8, rcx
        mov r9, rdx


        mov     rax, 1
        cpuid
        test    rdx, 0x2000000
        jz not_supported


        mov     rdx, rsp
        and     rsp, 0xfffffffffffffff0
        sub     rsp, 512
        fxsave  [rsp]


        mov rcx, r9

    xorps xmm0, xmm0

next_four:

        cmp     rcx, 4
        jb next_one



        movups  xmm1, [rsi]
        movups  xmm2, [rdi]
    mulps xmm1, xmm2
    addps xmm0, xmm1



        add     rsi, 16
    add     rdi, 16  
        sub     rcx, 4
        jmp next_four

next_one:

        jrcxz finish




    movss  xmm1, [rsi]
        movss  xmm2, [rdi]
    mulss xmm1, xmm2
    addss xmm0, xmm1


        add     rsi, 4
    add     rdi, 4
        dec     rcx

        jmp next_one

finish: 

    #**summing the 4 bytes giving the actual dot product**
        movhlps xmm1, xmm0
        addps   xmm0, xmm1
        movaps  xmm1, xmm0
        shufps  xmm1, xmm1, 0b01010101
        addss   xmm0, xmm1


    movss   [r8], xmm0



        fxrstor [rsp]
        mov     rsp, rdx

done:

    leave
        ret

not_supported:


        mov rax, 1
        mov rbx, 1
        int 0x80

【问题讨论】:

这个循环瓶颈是addps 的延迟(每 3 或 4 个时钟一个迭代器),而不是吞吐量(1 或 0.5 个时钟),因为它只使用一个向量累加器。如果内存带宽不是瓶颈,使用多个累加器展开可以将 Skylake 的性能提高 4 倍。 顺便说一句,这个手写的 asm 优化得很差。顶部的 cmp/jb 以及 3 个 add/sub 和一个 jmp 是 lot 的循环开销。 (仍然不足以比addps 延迟瓶颈慢,但请参阅Why are loops always compiled into "do...while" style (tail jump)? 了解有关循环结构的更多信息。您可能最好在 C 中使用内在函数重写它并让编译器生成代码,或者只让编译器自动矢量化标量循环(使用 OpenMP,或使用 -ffast-math 以允许重新排序 FP 操作)。JRCXZ 比 cmp/jz 慢 【参考方案1】:

这个最终代码仅使用普通的 addps/addss 指令在 xmm0 中添加 4 个压缩浮点数。首先,它将 2 个最高压缩浮点数复制到 xmm1 的低浮点数,因此 xmm0 + xmm1 可以用一条指令进行两次加法。 2个高浮动是“不关心”。重复使用 shufps 将剩余浮点数的最高值复制到最低位置。将 shufps 的直接“选择器”视为每个目标单词的数组索引。唯一重要的是低两位,它等于索引 1,它移动 1->0。其他的都只是占位符。然后,只需添加一次。

xmm0: D | C | B | A
 +
xmm1: X | X | D | C      (movhlps xmm0)
-------------------
 =    X | X | B+D | A+C


xmm0: X | X | B+D | A+C
 +
xmm1: X | X | X   | B+D   (shufps xmm0)
-----------------------
 =    X | X | X   | A + B + C + D

这里,X 的意思是“不关心”。最后,总和位于要被 movss 提取的最低位置。

2 个加法指令,位于 XMM 寄存器中。否则,您需要 3 个,并带有更明确的动作。

更多 shufps 细节:

将 0b01010101 值拆分为 4 个二进制索引:01 | 01 | 01 | 01,十进制为1 | 1 | 1 | 1.每个索引从源中选择源(​​以单词为单位)。正如文档所描述的,对于较高的 2 个单词,这会变得更加复杂,但我们并不关心这些。结果是将 word1 复制到 word0 和 word1,因为两个低位选择器都是 1。

编辑: HADDPS 是另一种可能的实现,添加邻居。两个 HADDPS 依次处理最终的总和。知道哪个更快的唯一方法是在您的目标处理器上进行基准测试,而不是最后一块对函数的整体速度有很大影响。

【讨论】:

您能否详细说明shufps 行?我什至无法理解它的作用,尤其是它的第三个参数0b01010101 The documentation for shufps describes it pretty well. haddps 不值得在任何支持它的 CPU 上使用,除非您在热循环之外针对代码大小或原始速度以外的东西进行优化。或者对 Nehalem 或更早的一些奇怪的解码效果(没有 uop 缓存)。见Fastest way to do horizontal float vector sum on x86。

以上是关于SSE XMM 点积说明的主要内容,如果未能解决你的问题,请参考以下文章

SSE2 直接测试 xmm 位掩码而不使用“pmovmskb”

xmm 寄存器 sse x64 里面的值

SIMD/SSE:短点积和短最大值

C 内在函数、SSE2 点积和 gcc -O3 生成的程序集

内在函数和寄存器(SSE)

如何复制每个双字的最后一个字节?