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 点积说明的主要内容,如果未能解决你的问题,请参考以下文章