充分利用kaby湖上的管道

时间:2017-07-16 05:44:00

标签: performance assembly x86-64 micro-optimization avx2

(跟进代码审查question here,详细介绍此循环的上下文。)

环境:

  • Windows 7 x64
  • VS 2017社区
  • 在Intel i7700k(kaby lake)上定位x64代码

我没有写很多汇编程序代码,而且当我这么做时,它要么足够短,要么足够简单,以至于我不必担心压缩它的最大数量。我的更复杂的代码通常用C编写,我让编译器的优化器担心延迟,代码对齐等。

然而,在我目前的项目中,MSVC的优化器在关键路径中的代码上做得非常糟糕。所以......

我还没有找到一个好的工具,可以对x64汇编程序代码进行静态或运行时分析,以便消除停顿,改善延迟等等。我所拥有的只是VS剖析器告诉我(粗略)哪些指令花费的时间最多。墙上的时钟告诉我最近的变化是否使事情变得更好或更糟。

作为替代方案,我一直在通过Agner的文档进行操作,希望能从我的代码中挤出一些更多的信息。问题是,在你理解了所有这些工作之前,很难理解他的任何工作。但它的一部分是有意义的,我正在尝试应用我所学到的东西。

考虑到这一点,这里是我最内层循环的核心(毫不奇怪)是VS分析器说我的时间花在了:

nottop:

vpminub ymm2, ymm2, ymm3 ; reset out of range values
vpsubb  ymm2, ymm2, ymm0 ; take a step

top:
vptest  ymm2, ymm1       ; check for out of range values
jnz nottop

; Outer loop that does some math, does a "vpsubb ymm2, ymm2, ymm0",
; and eventually jumps back to top

是的,这几乎是依赖链的教科书示例:这个紧密的小循环中的每条指令都取决于前一操作的结果。这意味着没有并行性,这意味着我没有充分利用处理器。

受到Agner的“优化汇编程序”文档的启发,我想出了一种方法(希望)允许我一次完成2个操作,所以我可以有一个管道更新ymm2而另一个更新(比方说)ymm8。

虽然这是一个非平凡的变化,所以在我开始撕掉所有东西之前,我想知道它是否可能有所帮助。看看Agner的kaby lake(我的目标)的“指令表”,我看到了:

        uops
        each
        port    Latency
pminub  p01     1
psubb   p015    1
ptest   p0 p5   3

鉴于此,看起来当一个管道使用p0 + p5对ymm2进行vptest时,另一个可以利用p1在ymm8上同时执行vpminub和vpsubb。是的,事情仍然会落后于vptest,但它应该会有所帮助。

或者它会吗?

我目前正在从8个线程运行此代码(是的,8个线程确实给我提供了比4,5,6或7更好的总吞吐量)。鉴于我的i7700k有4个超线程内核,那么每个内核上运行2个线程的事实并不意味着我已经已经最大化端口了吗?端口是“每个核心”,而不是“按逻辑cpu”,对吧?

所以

根据我目前对Agner工作的理解,似乎无法以当前形式进一步优化此代码。如果我想要更好的性能,我将需要提出一种不同的方法。

是的,我确定如果我在这里发布了我的整个asm例程,有人可以提出另一种方法。但是这个问题的目的不是让别人为我编写代码。我试图看看我是否开始理解如何考虑优化asm代码。

这(大致)是看待事物的正确方法吗?我错过了几件吗?或者这是完全错误的吗?

2 个答案:

答案 0 :(得分:6)

TL:DR :我认为超线程应该保持所有向量ALU端口忙于每个核心2个线程。

vptest不会写向量寄存器,只能写入标志。下一次迭代不必等待它,因此它的延迟几乎无关紧要。

只有jnz依赖于vptest,而推测执行+分支预测会隐藏控制依赖关系的延迟。 vptest延迟与检测到分支错误预测的速度有关,但与正确预测的情况下的吞吐量无关。

关于超线程的好处。在单个线程中交换两个独立的dep链可能会有所帮助,但要正确有效地执行它会更加困难。

让我们看一下循环中的说明。预测截取jnz将始终在p6上运行,因此我们可以对它进行折扣。 (展开实际上可能会受到伤害:预测 - 未采取jnz也可以在p0或p6上运行)

在核心本身上,你的循环应该每次迭代运行2个周期,在延迟方面存在瓶颈。这是5个融合域uops,因此需要1.25个周期才能发布。 (与test不同,jnz无法与vptest进行宏观融合。 通过超线程,前端已经是一个比延迟更糟糕的瓶颈。每个线程可以每隔一个周期发出4个uop,这比依赖链瓶颈的每隔一个周期少5个uop。

(这对于最近的英特尔来说很常见,尤其是SKL / KBL:许多uops有足够的端口可供选择,每个时钟吞吐量维持4 uops是切合实际的,特别是SKL提高了uop-cache和解码器的吞吐量以避免问题泡沫由于前端限制而不是后端填补。)

每次一个线程停止运转(例如,对于分支误预测),前端可以赶上另一个线程并在未来的核心中获得大量未来的迭代,以便在每个线程中进行一次咀嚼2个周期。 (或者因执行端口吞吐量限制而减少,见下文)。

执行端口吞吐量(未融合域):

每5个uop中只有1个在p6(jnz)上运行。它不能成为瓶颈,因为前端发布速率限制我们在运行此循环时每个时钟发布的分支少于一个。

每次迭代的其他4个向量ALU uop必须在带有向量执行单元的3个端口上运行。 p01和p015 uops具有足够的调度灵活性,没有单个端口会成为瓶颈,因此我们只需查看总ALU吞吐量。对于3个端口,这是4 uops / iter,对于每1.333个周期一个物理核心的最大平均吞吐量。

对于单线程(无HT),这不是最严重的瓶颈。但是有两个超线程,每2.6666个周期就是一个。

超线程应该使您的执行单元饱和,并且需要一些前端吞吐量。每个线程应该平均每2.666c一个,前端能够每2.5c发出一个。由于延迟仅将您限制为每2c一个,因此在资源冲突导致关键路径上的任何延迟后,它可以赶上。 (一个vptest uop从另外两个uops中偷走了一个循环。)

如果您可以更改循环以更少检查或使用更少的向量uop,那么这可能是一个胜利。但我正在考虑的一切是更多向量uops(例如vpand而不是vptest,然后vpor在检查之前将其中的几个结果放在一起...或者vpxorvptest时生成全零向量。也许如果有一个向量XNOR或其他东西,但没有。

要检查实际发生的情况,可以使用perf计数器来分析当前代码,并查看整个内核(不仅仅是每个逻辑线程)的uop吞吐量。或者描述一个逻辑线程,看看它是否在p015的一半饱和。

答案 1 :(得分:1)

部分答案:

英特尔提供了一个名为Intel Architecture Code Analyzer的工具(描述为here),它对代码进行静态分析,显示(在某种程度上)asm代码段中正在使用的端口。

不幸的是:

  • v2.3不包含必要(且唯一)的头文件。您可以在v2.2中找到此文件。
  • v2.2包含标题,但省略了用于分析输出的python脚本(pt.py)。此文件也未包含在v2.3中(尚未找到)。
  • iaca的输出格式之一是.dot文件,由graphviz读取,但英特尔文档未能描述graphviz中的38个可执行文件中的哪一个用于显示输出。

但也许最重要的是(根据我的需要):

  • v2.3(目前是最新版本)支持Skylake,但不支持Kaby Lake。

考虑到处理器之间的实现细节如何变化,这会使所有输出都受到怀疑。 pdf文件中的日期表明v2.3于2017年7月发布,这意味着我可能需要等待下一个版本。