SSE2 8x8字节矩阵转换代码在Haswell +上慢两倍,然后在常春藤桥上

时间:2017-11-24 17:42:26

标签: performance assembly x86 sse simd

我的代码中有大量的punpckl,pextrd和pinsrd,它们旋转8x8字节矩阵,作为旋转带有循环的黑白图像的较大例程的一部分。

我用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。

我用Google搜索了一下,但无法找到解释。使用传统的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代码相关的算法相同)

2 个答案:

答案 0 :(得分:3)

如果不是通过pshufd和立即混合的组合最有效地完成插入dword,则更多。

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

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

AVX2 vpblendd在Haswell / Skylake上的任何vector-ALU端口(p0 / p1 / p5)上运行,因此比word-granularity pblendw / vpblendw更有效。< / p>

如果您需要避免AVX2,请考虑使用SSE4.1 blendps将32位元素与立即控制混合。它可以在Haswell上的任何端口上运行(或者在Sandybridge上使用p0 / p5,对于shuffle使用p1 / p5),并且在整数数据上使用它的延迟惩罚应该与您的情况无关。

答案 1 :(得分:3)

TL:DR:在dword-rearranging步骤中使用punpckl/hdq来保存大量的随机播放,就像A better 8x8 bytes matrix transpose with SSE?中的转置代码一样

您的内存布局需要单独存储每个矢量结果的低/高8字节,您可以使用movq [rdx], xmm / movhps [rdx+r9], xmm高效地执行此操作。

  

Haswell / Skylake的代码几乎是IVB的两倍

您的代码严重影响了随机播放吞吐量。

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

我看到你已经找到David Kanter出色的Haswell microarch write-up

对于像这样的代码,很容易出现乱码(或一般来说是port5)吞吐量的瓶颈,并且AVX / AVX2经常会变得更糟,因为很多shuffle只是在通道内。用于128位操作的AVX可能有所帮助,但我认为你不会从改组到256b向量中获得任何东西,然后将它们再次分成64位块。如果你可以加载或存储连续的256b块,那么值得尝试。

在我们考虑重大变化之前,你有一些简单的遗漏优化:

  MOVHLPS  xmm6,xmm5
  movq    [rdx+r13],xmm6

应为movhps [rdx+r13],xmm6。在Sandybridge和Haswell,movhps是一个纯粹的商店uop,不需要随机播放。

pextrd eax,xmm3,0总是比movd eax, xmm3更糟糕;永远不要使用pextrd立即使用0.(另外,pextrd直接对内存可能是一个胜利。你可以做一个64位movq,然后用32-覆盖其中一半位pextrd。然后你可能会对商店吞吐量产生瓶颈。另外,在Sandybridge,indexed addressing modes don't stay micro-fused,所以更多的商店会损害你的总uop吞吐量。但Haswell没有商店的问题,只有一些索引加载取决于指令。)如果你在某些地方使用更多的存储,而在其他地方使用更多的存储,你可以使用更多的存储来进行单寄存器寻址模式。

  

源和目标格式不是图像处理的自由度。

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

这样可以节省通过寄存器的步伐,并执行与[rcx+2*r8] / [rcx+r8]相关的操作。它还允许您使用一个movdqa加载两行。它为您提供了访问8x8块的良好内存位置。

当然,如果这个旋转是所有你正在使用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

从整数中提取/插入效率不高; pinsrdpextrd各有2个uop,其中一个uops是shuffle。您甚至可以使用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 uop,并将总uop数量从12个融合域uops(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

然后解压缩lo / hi为xmm2和xmm4。

使用AVX或AVX2可以跳过movdqa,因为您可以解压缩到新的目标寄存器而不是复制+销毁。