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”