TL:DR: use punpckl/hdq
节省大量的洗牌次数在双字重新排列步骤中,与中的转置代码完全相同更好的 8x8 字节矩阵转置 SSE? https://stackoverflow.com/questions/42162270/a-better-8x8-bytes-matrix-transpose-with-sse/42222490#42222490
您的内存布局需要分别存储每个向量结果的低/高 8 字节,您可以使用以下命令高效地做到这一点movq [rdx], xmm
/ movhps [rdx+r9], xmm
.
Haswell/Skylake 上的代码几乎是 IVB 上的两倍
您的代码在洗牌吞吐量上存在严重瓶颈。
Haswell 只有 1 个 shuffle 执行单元,位于端口 5。SnB/IvB 有 2 个整数 shuffle 单元(但仍然只有一个 FP 洗牌单元)。看Agner Fog 的指令表和优化指南/微架构指南 http://agner.org/optimize/.
我看到你已经发现了大卫·坎特的出色表现Haswell 微架构撰写 https://www.realworldtech.com/haswell-cpu/4/.
对于这样的代码,shuffle(或一般为 port5)吞吐量很容易出现瓶颈,并且使用 AVX / AVX2 时情况通常会变得更糟,因为许多 shuffle 仅在通道内。用于 128 位操作的 AVX 可能会有所帮助,但我认为您不会从改组为 256b 向量然后将它们再次改组为 64 位块中获得任何好处。如果您可以加载或存储连续的 256b 块,那么值得尝试。
即使在我们考虑重大更改之前,您也有一些简单的错过优化:
MOVHLPS xmm6,xmm5
movq [rdx+r13],xmm6
应该movhps [rdx+r13],xmm6
。在 Sandybridge 和 Haswell 上,movhps
是一个纯存储 uop,不需要洗牌 uop。
pextrd eax,xmm3,0
总是比movd eax, xmm3
;从不使用pextrd
立即数为 0。(此外,pextrd
直接记忆可能是一个胜利。你可以做一个64位的movq
然后用 32 位覆盖一半pextrd
。然后,您可能会成为商店吞吐量的瓶颈。另外,在桑迪布里奇,索引寻址模式不会保持微融合 http://stackoverflow.com/questions/26046634/micro-fusion-and-addressing-modes,因此更多的存储会损害您的总 uop 吞吐量。但 Haswell 对于存储没有这个问题,仅对于某些索引加载(取决于指令)。)如果您在某些地方使用更多的存储,而在其他地方使用更多的洗牌,则可以为单寄存器寻址模式使用更多的存储。
源和目标格式不是图像处理的自由度。
取决于你在做什么。 x264(开源 h.264 视频编码器)将 8x8 块复制到连续的在重复使用它们之前先进行缓冲,因此行之间的步幅是一个汇编时间常数。
这可以节省在寄存器中传递一步并做像您正在做的事情[rcx+2*r8]
/ [rcx+r8]
。它还允许您用一行加载两行movdqa
。它为您提供了良好的内存局部性来访问 8x8 块。
当然,如果这种轮换是这样的话,花时间复制进/出这种格式可能不是一个胜利。all您正在使用 8x8 像素块进行操作。 FFmpeg 的 h.264 解码器(使用许多与 x264 相同的 asm 原语)不使用此功能,但我不知道这是因为没有人费心移植更新的 x264 asm 或者只是不值得。
// now we join one "low" dword from XMM1:xmm2 with one "high" dword
// from XMM3:xmm4
从整数中提取/插入的效率不是很高;pinsrd
and pextrd
每个微指令有 2 个微指令,其中一个微指令是随机指令。您甚至可能会领先于当前的代码,只需使用pextrd
以 32 位块的形式存储到内存中。
还可以考虑使用 SSSE3pshufb
它可以将您的数据放在任何需要的地方,并将其他元素归零。这可以让您为合并做好准备por
。 (你可能会使用pshufb
代替punpcklbw
).
另一种选择是使用shufps
合并两个来源的数据。之后您可能需要再次洗牌。Or use punpckldq
.
// "low" dwords from XMM1:xmm2
// high dwords from XMM3:xmm4
; xmm1: [ a b c d ] xmm2: [ e f g h ]
; xmm3: [ i j k l ] xmm4: [ m n o p ]
; want: [ a i b j ] / [ c k d l ] / ... I think.
;; original: replace these with
; movdqa xmm5,xmm1 ; xmm5 = [ a b c d ]
; pextrd eax,xmm3,0 ; eax = i
; pinsrd xmm5,eax,1 ; xmm5 = [ a i ... ]
; movq [rdx],xmm5
; movdqa xmm5,xmm3 ; xmm5 = [ i j k l ]
; pextrd eax,xmm1,1 ; eax = b
; pinsrd xmm5,eax,0 ; xmm5 = [ b j ... ]
; movq [rdx+r9],xmm5
替换为:
movdqa xmm5, xmm1
punpckldq xmm5, xmm3 ; xmm5 = [ a i b j ]
movq [rdx], xmm5
movhps [rdx+r9], xmm5 ; still a pure store, doesn't cost a shuffle
因此,我们将 4 个 shuffle uops 替换为 1 个,并将总 uop 计数从 12 个融合域 uops (Haswell) 减少到 4 个。(或者在 Sandybridge 上,从 13 个减少到 5 个,因为索引存储不会保持微融合) 。
Use punpckhdq
for [ c k d l ]
,它甚至更好,因为我们正在替换movhlps
以及。
; movdqa xmm5,xmm1 ; xmm5 = [ a b c d ]
; pextrd eax,xmm3,2 ; eax = k
; pinsrd xmm5,eax,3 ; xmm5 = [ a b c k ]
; MOVHLPS xmm6,xmm5 ; xmm6 = [ c k ? ? ] (false dependency on old xmm6)
; movq [rdx+2*r9],xmm6
然后解压 xmm2 和 xmm4 的 lo/hi。
使用 AVX 或 AVX2 可以让您跳过movdqa
,因为您可以解压到新的目标寄存器中,而不是复制+销毁。