SSE2 8x8 字节矩阵转置代码在 Haswell+ 上比在 ivy bridge 上慢两倍

SSE2 8x8 byte-matrix transpose code twice as slow on Haswell+ then on ivy bridge

我编写了很多 punpckl、pextrd 和 pinsrd 代码,它们旋转 8x8 字节矩阵作为更大例程的一部分,该例程使用 looptiling 旋转 B/W 图像。

我用 IACA 对它进行了分析,看看它是否值得为它做一个 AVX2 例程,令人惊讶的是,代码在 Haswell/Skylake 上的速度几乎是 IVB 上的两倍(IVB:19.8,HSW,SKL: 36 个周期)。 (IVB+HSW 使用iaca 2.1,skl 使用3.0,但hsw 给出与3.0 相同的数字)

根据 IACA 的输出,我猜不同之处在于 IVB 使用端口 1 和 5 执行上述指令,而 haswell 仅使用端口 5。

我用谷歌搜索了一下,但找不到解释。 haswell 使用旧版 SSE 真的很慢,还是我只是遇到了一些极端的极端情况?任何避免此子弹的建议(AVX2 除外,这是一个已知选项,但由于暂时推迟将工具链更新到新版本)

也欢迎一般性评论或建议的改进。

   // r8 and r9 are #bytes to go to the next line in resp. src and dest 
   // r12=3*r8 r13=3*r9  
  // load 8x8 bytes into 4 registers, bytes interleaved.
  movq xmm1,[rcx]
  movq xmm4,[rcx+2*r8]
  PUNPCKLBW xmm1,xmm4   // 0 2 0 2 0 2
  movq xmm7,[rcx+r8]
  movq xmm6,[rcx+r12]
  PUNPCKLBW xmm7,xmm6   // 1 3 1 3 1 3

  movdqa xmm2,xmm1
  punpcklbw xmm1,xmm7   // 0 1 2 3 0 1 2 3 in xmm1:xmm2
  punpckhbw xmm2,xmm7   
  lea rcx,[rcx+4*r8]

  // same for 4..7

  movq xmm3,[rcx]
  movq xmm5,[rcx+2*r8]
  PUNPCKLBW xmm3,xmm5
  movq xmm7,[rcx+r8]
  movq xmm8,[rcx+r12]
  PUNPCKLBW xmm7,xmm8

  movdqa xmm4,xmm3
  punpcklbw xmm3,xmm7
  punpckhbw xmm4,xmm7

  // now we join one "low" dword from XMM1:xmm2 with one "high" dword
  // from XMM3:xmm4

  movdqa  xmm5,xmm1
  pextrd  eax,xmm3,0
  pinsrd  xmm5,eax,1
  movq    [rdx],xmm5

  movdqa  xmm5,xmm3
  pextrd  eax,xmm1,1
  pinsrd  xmm5,eax,0
  movq    [rdx+r9],xmm5

  movdqa  xmm5,xmm1
  pextrd  eax,xmm3,2
  pinsrd  xmm5,eax,3
  MOVHLPS  xmm6,xmm5
  movq    [rdx+2*r9],xmm6

  movdqa  xmm5,xmm3
  pextrd  eax,xmm1,3
  pinsrd  xmm5,eax,2
  MOVHLPS  xmm6,xmm5
  movq    [rdx+r13],xmm6

  lea     rdx,[rdx+4*r9]

  movdqa  xmm5,xmm2
  pextrd  eax,xmm4,0
  pinsrd  xmm5,eax,1
  movq    [rdx],xmm5

  movdqa  xmm5,xmm4
  pextrd  eax,xmm2,1
  pinsrd  xmm5,eax,0
  movq    [rdx+r9],xmm5

  movdqa  xmm5,xmm2
  pextrd  eax,xmm4,2
  pinsrd  xmm5,eax,3
  MOVHLPS  xmm6,xmm5
  movq    [rdx+2*r9],xmm6

  movdqa  xmm5,xmm4
  pextrd  eax,xmm2,3
  pinsrd  xmm5,eax,2
  MOVHLPS  xmm6,xmm5
  movq    [rdx+r13],xmm6

  lea     rdx,[rdx+4*r9]

目的: 它实际上是为了图像视觉目的而旋转来自相机的图像。在一些(较重的)应用程序中,旋转被推迟并仅显示(opengl),在一些应用程序中,旋转输入然后适应算法更容易。

更新代码:我发布了一些最终代码here加速非常依赖于输入的大小。在小图像上大,但与使用 32x32 块的循环 HLL 代码相比,在大图像上仍然是两倍。 (与链接的 asm 代码相同的算法)

结合使用 pshufd 和立即混合,即使不是最有效地插入双字,也更有效。

 pshufd xmm5, xmm3, 0x55 * slot
 pblendw xmm1, xmm5, 3 << dst_slot

pblendw is SSE4.1,但当然可以在 haswell 上使用。不幸的是,它仅在 Haswell/Skylake 上的端口 5 上运行,因此它仍然与 shuffle 竞争。

AVX2 vpblendd 在 Haswell/Skylake 上的任何矢量 ALU 端口 (p0/p1/p5) 上运行,因此比字粒度 pblendw / [=13] 更有效=].

如果您需要避免使用 AVX2,请考虑使用 SSE4.1 blendps 将 32 位元素与直接控件混合。它在 Haswell 上的任何端口上运行(或 Sandybridge 上的 p0/p5 与随机播放的 p1/p5 相比),并且在整数数据上使用它的延迟惩罚应该与您的情况无关。

TL:DR: 使用punpckl/hdq在dword-rearranging步骤中保存大量shuffle,与中的转置代码完全一样

您的内存布局需要分别存储每个向量结果的低/高 8 字节,您可以使用 movq [rdx], xmm / movhps [rdx+r9], xmm.

高效地做到这一点

The code is almost twice times as slow on Haswell/Skylake than on IVB

您的代码在随机播放吞吐量方面存在严重瓶颈。

Haswell 只有一个洗牌执行单元,在端口 5 上。SnB/IvB 有 2 个整数洗牌单元(但仍然只有一个 FP 洗牌单元)。参见 Agner Fog's instruction tables and optimization guide / microarch guide

我看你已经发现了大卫坎特的优秀Haswell microarch write-up

像这样的代码很容易在 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,不需要 shuffle uop。

pextrd eax,xmm3,0总是比movd eax, xmm3差;切勿将 pextrd 与立即数 0 一起使用。(此外,pextrd 直接存储到内存中可能是一个胜利。您可以执行 64 位 movq,然后用 32- bit pextrd。然后你可能会在存储吞吐量上遇到瓶颈。另外,在 Sandybridge 上,indexed addressing modes don't stay micro-fused,所以更多的存储会损害你的总 uop 吞吐量。但是 Haswell 对存储没有这个问题,只有一些索引加载取决于指令。)如果你在一些地方使用更多的存储而在其他地方使用更多的随机播放,你可以为单寄存器寻址模式使用更多的存储。


Source and destination format is not a degree of freedom in image manipulation.

取决于你在做什么。 x264(开源 h.264 视频编码器)将 8x8 块复制到 连续 缓冲区中,然后重复使用它们,因此行之间的跨度是一个 assemble 时间常数。

这节省了在寄存器中传递一个步幅并像使用 [rcx+2*r8] / [rcx+r8] 那样做一些事情。它还允许您用一个 movdqa 加载两行。它为您提供了访问 8x8 块的良好内存位置。

当然,如果此旋转是 all,那么花时间复制此格式的 in/out 可能不是一件好事,您正在使用 8x8 像素块。 FFmpeg 的 h.264 解码器(它使用许多与 x264 相同的 asm 原语)不使用这个,但是 IDK 如果那是因为没有人费心去移植更新的 x264 asm 或者它不值得。


  // now we join one "low" dword from XMM1:xmm2 with one "high" dword
  // from XMM3:xmm4

extract/insert to/from整数不是很有效; pinsrdpextrd 各为 2 微指令,其中一个微指令是随机播放。您甚至可能仅使用 pextrd 以 32 位块的形式存储在当前代码之前。

还可以考虑使用 SSSE3 pshufb,它可以将您的数据放在任何需要的地方,并将其他元素归零。这可以让你与 por 合并。 (您可以使用 pshufb 而不是 punpcklbw)。


另一种选择是使用 shufps 合并来自两个来源的数据。之后您可能需要再次洗牌。 或使用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

所以我们用 1 个替换了 4 个 shuffle 微指令,并将总微指令数从 12 个融合域微指令 (Haswell) 减少到 4 个。(或者在 Sandybridge 上,从 13 个减少到 5 个,因为索引存储没有保持微融合)。

punpckhdq 用于 [ 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,因为您可以解压到新的目标寄存器中,而不是复制 + 销毁。