充分利用卡比湖上的管道
Posted
技术标签:
【中文标题】充分利用卡比湖上的管道【英文标题】:Fully utilizing pipelines on kaby lake 【发布时间】:2017-07-16 05:44:00 【问题描述】:(后续代码审查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 代码。
这是(大致)看待事物的正确方式吗?我错过了几件吗?或者这完全是错误的?
【问题讨论】:
“我还没有找到一个好的工具来对 x64 汇编代码进行静态或运行时分析,以消除停顿、改善延迟等。”认识IACA。它支持的最新微架构是 Skylake,但据我所知,SKL 和 KBL 的区别相对较小。 (不幸的是,如果英特尔停止继续更新,该工具在未来的用处将变得不那么大。:-() @CodyGray 感谢您的介绍。发布此问题后,我遇到了 iaca(请参阅下面的“部分答案”)。 “差异相对较小”的问题是我不知道这些差异可能在哪里。从“第 6 代”到“第 7 代”时,对 SSE 的更改似乎是一个合理的改进领域。我对试图定义这些差异可能存在的想法并不感到兴奋。除此之外,iaca 并没有告诉我太多我没有从 Agner 的工作中收集到的信息。它只是为我节省了查找说明以查看他们使用的端口的操作。 相对较小的差异可能是夸大其词。据我所知,Kaby Lake 与 Skylake 的微架构相同,并且时间相同。就像从 Haswell 到 Broadwell 或从 Broadwell 到 Skylake 跳跃一样,差别不大,但 Kaby Lake 确实是“无操作”。 @DavidWohlferd - 进入nottop
循环时ymm3
和ymm0
的典型值是什么?它们在外循环中会发生变化吗?
@BeeOnRope - ymm0 和 ymm3 都是常量,在初始化时加载一次。也就是说,这个问题的目的是测试我对 asm 优化的理解。我已经掌握了一些基础知识,但显然我还有很长的路要走。我希望尽快在 codereview 上发布此代码的可运行版本。
【参考方案1】:
TL:DR:我认为超线程应该让所有矢量 ALU 端口忙于每个核心 2 个线程。
vptest
不写向量寄存器,只写标志。下一次迭代不必等待它,因此它的延迟几乎是无关紧要的。
只有jnz
依赖vptest
,推测执行+分支预测隐藏了控制依赖的延迟。 vptest
延迟与检测到分支错误预测的速度有关,但与正确预测情况下的吞吐量无关。
关于超线程的好点。在单个线程中交错两个独立的 dep 链可能会有所帮助,但要正确有效地做到这一点要困难得多。
让我们看看循环中的说明。 predict-taken jnz
将始终在 p6 上运行,因此我们可以打折。 (展开实际上可能会受到伤害:predicted-not-taken jnz
也可以在 p0 或 p6 上运行)
在一个核心上,您的循环应该在每次迭代中运行 2 个周期,并受到延迟的限制。它是 5 个融合域微指令,因此需要 1.25 个周期才能发出。 (与test
不同,jnz
不能与vptest
进行宏融合)。 使用超线程,前端已经是比延迟更严重的瓶颈。每个线程可以每隔一个周期发出 4 条微指令,小于依赖链瓶颈的每隔一个周期发出 5 条微指令。
(这对于最近的英特尔来说很常见,尤其是 SKL/KBL:许多 uop 有足够的端口可供选择,因此维持每个时钟 4 uop 的吞吐量是现实的,尤其是在 SKL 改进了 uop-cache 和解码器的吞吐量以避免问题泡沫的情况下由于前端限制而不是后端填充。)
每当一个线程停止时(例如,对于一个分支错误预测),前端可以赶上另一个线程,并在乱序核心中获得大量未来的迭代,以便它每次迭代一次2个周期。 (或更少,因为执行端口吞吐量限制,见下文)。
执行端口吞吐量(未融合域):
每 5 个微指令中只有 1 个在 p6(jnz
)上运行。它不能成为瓶颈,因为在运行此循环时,前端发出率限制了我们每个时钟发出的分支少于一个。
每次迭代的其他 4 个向量 ALU 微指令必须在具有向量执行单元的 3 个端口上运行。 p01 和 p015 微控制器具有足够的调度灵活性,没有单个端口会成为瓶颈,所以我们可以只看总 ALU 吞吐量。对于 3 个端口,这是 4 uops / iter,对于每 1.333 个周期一个 iter 的物理核心的最大平均吞吐量。
对于单线程(无 HT),这并不是最严重的瓶颈。但是如果有两个超线程,那就是每 2.6666 个周期 1 个迭代。
超线程应该使您的执行单元饱和,并保留一些前端吞吐量。每个线程应该平均每 2.666c 一个,前端能够以每 2.5c 一个的速度发出。由于延迟仅将您限制为每 2c 一个,因此它可以在由于资源冲突导致关键路径上的任何延迟后赶上。 (vptest
uop 从其他两个 uop 中的一个窃取了一个循环)。
如果您可以更改循环以减少检查频率或使用更少的矢量微指令,那可能是一个胜利。但是我在想的一切都是更多矢量微指令(例如vpand
而不是vptest
然后vpor
在检查之前将这些结果放在一起......或者vpxor
产生vptest
时的全零向量)。也许如果有向量 XNOR 或其他东西,但没有。
要检查实际发生的情况,您可以使用性能计数器来分析您当前的代码,并查看您为整个内核(而不仅仅是每个逻辑线程单独)获得的 uop 吞吐量。或者分析一个逻辑线程,看看它是否在 p015 的一半左右饱和。
【讨论】:
@DavidWohlferd:如果您在 codereview 上发帖,请在此处联系我。很少有 asm/perf 问题,我通常不关注它。回复:通常应该避免依赖链,这显然是不可能的(使用指令输出有点意思:),但避免延迟瓶颈是理想的。有些事情本质上是连续的,找到重叠或交错的方法很酷。尤其是当您还关心没有 HT 的 CPU 时。我在my collatz conjecture optimization answer 中提到了交错 似乎你错过的最大的事情是你的关键路径循环承载的 dep 链只有 2 个循环,而不是 5 个,因为 vptest->jnz 每次迭代都会分叉。所以你已经在一个线程中有相当数量的 ILP。 “更改循环以减少检查频率” - 不能。 “或者使用更少的向量 uops” - 啊……vpand
等人的问题是(与 and
不同)他们没有为 jnz 设置标志。我仍然需要一些方法来从 ymm 到 gp :(。但是 SSE 指令的扫描让我想起了vpmovmskb
。虽然我从未说过,ymm1 只是每个字节的高位。所以vpmovmskb eax, ymm2 ; test eax, eax
有效与vptest
做同样的事情。测试和 jnz 熔断,所以即使有 1 条“更多”指令,这也为我节省了大约 5% 的执行时间。不幸的是,虽然你给出了 2 个有用的答案,但我只能给你 1 个赞.
@BeeOnRope 我的第一反应是“那行不通”。但这只是我对我的工作的防御。我试图想出一些方法来进行直接计算而没有成功(显然)。不代表做不到。虽然内部循环的迭代次数可以在 1 到 ~103 之间变化,但它的权重很大地倾向于 1。即使 N=2,“重新做”也可能不是净赢。但我需要试一试才能确定。如果我可以破解所有 3rd 方库和我正在使用的特定硬件,我会将其发布到 codereview。收到后,我会在此 ping 你和 Peter。
@BeeOnRope “算法更改” - 当我为 CR 创建该示例时,我意识到我对接下来要尝试什么有了一个想法。 AAR,我几乎没有发布这个问题。但我意识到,为了确定我的“下一个想法”是否更好,我需要确保尽可能优化当前代码。所以我想我对两者都持开放态度。【参考方案2】:
部分答案:
英特尔提供了一个名为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 月发布,这意味着我可能需要等待一段时间才能发布下一个版本。
【讨论】:
嗯,奇怪的是他们从 v2.3 中省略了头文件。有点草率。虽然很高兴看到本月发布了一个新版本。我在某处听说过他们将停止维护 IACA 的传言,所以希望那些结果是错误的。至于输出,我从未尝试过使用 Python 脚本。我只是直接在文件中查看文本输出(或将其输出到命令提示符)。在我看来,这很容易阅读。比我先学习 Python 或 graphviz 容易得多。 我不会太担心 Skylake 与 Kaby Lake。 KBL 只是 SKL 的一个次要“优化”步骤,实际上只是工艺缩小和时钟速度提升。 Very little of the microarchitecture changed。我认为输出将非常值得信赖。老实说,即使它并不完美,它肯定会帮助您以 Agner 手册所没有的方式了解代码(除非,就像您说的那样,您已经是专家)。 好的,我会多花点时间陪他们。那里可能还有珍珠。 顺便说一句,python 脚本用于处理–trace
输出。但无论如何,我可能还没有准备好接受那种详细程度的细节。
“微架构几乎没有改变” - 哎呀:卡比湖有-鳍-(大的)! 以上是关于充分利用卡比湖上的管道的主要内容,如果未能解决你的问题,请参考以下文章