充分利用Kaby Lake的管道技术

8

(关于该循环的更多上下文信息,请参见此处的后续代码审查问题。)


环境:

  • 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 的“优化汇编”文档启发,我想出了一种方法(希望可以)同时执行两个操作,这样我就可以有一个流水线更新 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代码。
这(粗略地)是正确的看待事物的方式吗?我漏掉了一些细节吗?还是完全错误的?

1
我还没有找到一款好工具,可以静态或动态分析x64汇编代码,以期消除停顿,提高延迟等。介绍 IACA。它支持的最新微架构是Skylake,但据我所知,SKL和KBL之间的差异相对较小。(不幸的是,如果英特尔停止更新这个工具,它将在未来变得不那么有用。:() - Cody Gray
@CodyGray 谢谢介绍。在我发出这个问题后(请参见我的“部分答案”),我偶然遇到了iaca。 “相对很少的差异”的问题在于我不知道这些差异可能存在哪里。 从“第6代”到“第7代”时,SSE的更改似乎是一个可以改进的领域。 我对尝试定义这些差异可能存在的位置并不感到兴奋。 除此之外,iaca并没有告诉我太多我没有从Agner的工作中获得的信息。 它只是省去了我查找指令以查看它们使用的端口的步骤。 - David Wohlferd
相对来说差异很小或许过于夸张了。据我所知,Kaby Lake与Skylake是相同的微架构,并且时序也是相同的。如果说Haswell转到Broadwell或Broadwell转到Skylake,那么差异就会很小,但Kaby Lake真的是“无操作”。 - BeeOnRope
@DavidWohlferd - nottop循环进入时ymm3ymm0的典型值是什么?它们在外部循环中会改变吗? - BeeOnRope
1
@BeeOnRope - ymm0和ymm3都是常量,在初始化时加载一次。 也就是说,这个问题的目的是为了测试我对汇编优化的理解。 我已经掌握了一些基础知识,但显然还有很长的路要走。 我希望很快在codereview上发布一个可运行版本的代码。 - David Wohlferd
我猜一旦了解了外层循环的行为,整个过程的速度就可以大大加快。 - BeeOnRope
2个回答

6
TL:DR: 我认为Hyperthreading应该保持每个核心的所有向量ALU端口都忙于2个线程。
vptest不会写入任何向量寄存器,只写标志位。下一次迭代不必等待它,因此其延迟大多是无关紧要的。
仅jnz依赖于vptest,并且推测执行+分支预测隐藏了控制依赖项的延迟。vptest的延迟对于快速检测分支错误预测很重要,但在正确预测的情况下,对吞吐量没有影响。
关于超线程的观点很好。在单个线程内交错两个独立的dep链可能有所帮助,但正确和高效地执行起来更加困难。
让我们看看循环中的指令。预测取将始终在p6上运行,因此我们可以忽略它。(展开实际上可能会有害:预测未取也可以在p0或p6上运行)
在单独的核心上,您的循环应以每次迭代2个周期运行,受到延迟的限制。它是5个融合域uop,因此需要1.25个周期才能发出。(与test不同,jnz无法与vptest宏融合)。超线程的情况下,前端已经成为比延迟更糟糕的瓶颈。每个线程可以每隔一次周期发出4个uop,这少于依赖链瓶颈每隔一次周期的5个uop。
(这在最近的Intel中很常见,特别是SKL/KBL:许多uop具有足够的端口可供选择,以实现每时钟周期4个uop的吞吐量是现实的,特别是由于SKL通过提高uop缓存和解码器的吞吐量来避免由于前端限制而导致的发行气泡,而不是后端填充。)
每当一个线程停顿(例如因分支错误预测),前端就可以在另一个线程上赶上并将许多未来的迭代放入乱序核心中,以便它以每2个周期处理一个迭代。(或更少,因为执行端口吞吐量限制,见下文)。
执行端口吞吐量(未融合域):
每5个uop中只有1个在p6上运行()。它不能成为瓶颈,因为前端发射速率限制我们在运行此循环时每个时钟周期都少于一个分支发射。
每个迭代中的另外4个向量ALU uop必须在具有向量执行单元的3个端口上运行。 p01和p015 uop具有足够的调度灵活性,以至于没有单个端口会成为瓶颈,因此我们只需查看总ALU吞吐量。对于3个端口,每次迭代的最大平均吞吐量为4个uop/iter,为物理核心。
对于单线程(没有HT),这不是最严重的瓶颈。但是对于两个超线程,这就是每2.6666个周期中的一个迭代。
超线程应该饱和执行单元,并有一些前端吞吐量。每个线程的平均值应为每2.666c一个,前端能够以每2.5c的速度发出指令。由于延迟只在每2c限制时才会出现,因此它可以在任何由于资源冲突而导致关键路径上的延迟之后进行赶上(一个vptest uop从另外两个uops中窃取一个周期)。
如果您可以更改循环以较少的频率或使用较少的向量uop进行检查,那将是一个胜利。但是我所想到的一切都是更多的向量uop(例如vpand而不是vptest,然后将其中几个结果vpor在一起再检查...或者vpxor在vptest时产生全零向量)。也许如果有一个向量XNOR之类的东西,但确实没有。
要检查实际发生的情况,您可以使用perf计数器来分析您当前的代码,并查看整个核心(而不仅仅是每个逻辑线程)的uop吞吐量。或者分析一个逻辑线程并查看它是否使p015的大约一半饱和。

1
@DavidWohlferd:如果你在codereview上发布,请在这里提醒我。汇编/性能问题很少,所以我通常不会关注它。关于应该尽量避免依赖链,这显然是不可能的(使用指令的输出是一种目标:),但避免延迟瓶颈是理想的。有些事情本质上是串行的,找到重叠或交错的方法是很酷的。特别是当你还关心没有超线程的CPU时。我在我的Collatz猜想优化答案中提到了交错。 - Peter Cordes
1
看起来你最大的遗漏是你的关键路径循环依赖链只有2个周期,而不是5个周期,因为vptest->jnz在每次迭代时都会分叉。因此,在单个线程中,你已经具备了相当多的ILP。 - Peter Cordes
2
"将循环更改为较少的检查次数" - 不行。 "或者使用更少的向量uops" - 啊... vpand等的问题是(与and不同),它们不会为jnz设置标志。我仍然需要一些方法从ymm到gp :(. 但是,对SSE指令的扫描提醒了我关于vpmovmskb。虽然我从未说过,但ymm1只是每个字节的最高位。因此,vpmovmskb eax,ymm2; test eax,eax实际上与vptest执行相同的操作。测试和jnz融合,因此即使有1个“更多”的指令,这也可以节省约5%的执行时间。不幸的是,虽然您给出了两个有用的答案,但我只能给您1个赞。 - David Wohlferd
1
@BeeOnRope 我的第一反应是“那不可能行得通”。但这只是我对自己工作的防御。我试图想出直接计算的方法,但没有成功(显然)。这并不意味着不能做到。虽然内部循环的迭代次数可以在1和~103之间变化,但它倾向于1。即使N=2,“重新做”也可能不是净赢。但我需要尝试才能确定。如果我能够削减所有第三方库和特定硬件的使用,我将把它发布到codereview上。当我完成时,我会在这里提醒你和Peter。 - David Wohlferd
2
@BeeOnRope "算法变更" - 当我为CR创建样本时,我意识到我有一个下一步尝试的想法。AAR,我几乎没有发布问题。但我意识到,为了决定我的“下一个想法”是否更好,我需要确保我已经尽可能地优化了当前代码。所以我想我对任何一种选择都持开放态度。 - David Wohlferd
显示剩余10条评论

1

部分答案:

英特尔提供了一个名为Intel Architecture Code Analyzer(在这里描述)的工具,对代码进行静态分析,显示了一段汇编代码中使用了哪些端口。

不幸的是:

  • 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月发布,这意味着我可能需要等待一段时间才能获得下一个版本。

嗯,他们在v2.3中省略了头文件,这有点马虎。不过很高兴看到本月发布了新版本。我听说过他们要停止维护IACA的传言,希望那些传言是假的。至于输出方面,我从未尝试使用Python脚本。我直接查看文件中的文本输出(或将其输出到命令提示符)。在我看来,这非常容易阅读。比我先学习Python或graphviz要容易得多。 - Cody Gray
1
我不会太担心 Skylake 和 Kaby Lake 的区别。KBL 实际上只是 SKL 的一个小“优化”步骤,仅仅是工艺缩小和时钟速度提升。微体系结构的改变很少。我认为输出结果应该是相当可靠的。而且说实话,即使它不完美,它肯定会帮助你以一种 Agner 的手册所做不到的方式理解代码(除非像你说的那样,你已经是专家)。 - Cody Gray
好的,我会再花一些时间与它们相处。或许还有一些珍珠需要挖掘。 - David Wohlferd
顺便说一下,这个Python脚本是用来处理“-trace”输出的。但是我可能还没有准备好处理那么详细的内容。 - David Wohlferd
我使用 xdot FILENAME 像在 Linux 上查看 .dot 文件,对于 IACA 文件效果很好。 - BeeOnRope
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接