(关于该循环的更多上下文信息,请参见此处的后续代码审查问题。)
环境:
- 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代码。
这(粗略地)是正确的看待事物的方式吗?我漏掉了一些细节吗?还是完全错误的?
nottop
循环进入时ymm3
和ymm0
的典型值是什么?它们在外部循环中会改变吗? - BeeOnRope