(后续代码审查question here,带有此循环上下文的更多详细信息。)

环境:

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

  • 我不需要编写很多汇编程序代码,当我这样做的时候,它要么足够简短,要么足够简单,因此我不必担心从中挤出最大性能。我更复杂的代码通常是用C编写的,我让编译器的优化器担心延迟,代码对齐等问题。

    但是,在我当前的项目中,MSVC的优化器在关键路径中的代码上做得非常差。所以...

    我还没有找到一个好的工具来对x64汇编程序代码进行静态或运行时分析,以期消除停顿,改善延迟等。我所拥有的只是VS Profiler,它(大概)告诉我哪些指令。花费最多的时间。墙上的时钟告诉我最新的变化是使情况好转还是恶化。

    作为替代方案,我一直在遍历Agner的文档,以期从代码中榨取更多性能。问题在于,除非您了解所有工作,否则很难理解他的任何工作。但是它的某些部分是有道理的,我正在尝试应用我所学到的东西。

    请记住,这是我最内层循环的核心(毫不奇怪)是VS profiler说我正在花费时间的地方:
    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)的“指令表”,我看到:
            uops
            each
            port    Latency
    pminub  p01     1
    psubb   p015    1
    ptest   p0 p5   3
    

    这样看来,当一个管道使用p0 + p5对ymm2进行vptest时,另一个管道可以利用p1对ymm8进行vpminub和vpsubb。是的,在vptest之后,事情仍然会堆积如山,但这应该有所帮助。

    还是会吗?

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

    所以。

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

    是的,我敢肯定,如果我在这里发布了整个汇编程序,那么有人可以建议另一种方法。但是,此问题的目的不是要有人为我编写我的代码。我正在尝试看看我是否开始了解如何考虑优化asm代码。

    这(大致)是正确看待事物的方式吗?我错过了几块吗?还是这种完全错误的做法?

    最佳答案

    TL:DR :我认为超线程应该使所有矢量ALU端口保持繁忙,每个内核有2个线程。
    vptest不写任何向量寄存器,只写标志。下一次迭代不必等待它,因此它的延迟几乎是无关紧要的。

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

    关于超线程的好处。在单个线程中插入两个独立的dep链可能会有所帮助,但是要正确有效地执行则要困难得多。

    让我们看一下循环中的指令。预测采用的jnz将始终在p6上运行,因此我们可以对其进行打折。 (展开实际上可能会造成伤害:预测未采用的jnz也可以在p0或p6上运行)

    在内核本身上,您的循环应在每次迭代中以2个周期运行,这是延迟的瓶颈。它是5个融合域的对象,因此需要1.25个周期来发出。 (与test不同,jnz不能与vptest宏融合)。 使用超线程,前端已经比等待时间严重了。每个线程每隔一个周期可以发出4个微指令,这比依赖关系链瓶颈的每隔一个周期发出5个微指令要少。

    (这对于最近的Intel来说很常见,尤其是SKL/KBL:许多uops都有足够的端口可供选择,维持每时钟4 uops的吞吐量是现实的,特别是SKL改进了uop缓存和解码器的吞吐量,以避免前端出现气泡-限制,而不是后端填充。)

    每当一个线程停止运行时(例如,分支错误预测),前端可以追上另一个线程,并在无序内核中获得大量 future 迭代,使其每2个周期迭代一次。 (或更少,因为执行端口吞吐量限制,请参见下文)。

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

    每5个微码中只有1个在p6(jnz)上运行。这不是瓶颈,因为在运行此循环时,前端发出率将我们限制为每个时钟少于一个分支发出。

    每次迭代的其他4个矢量ALU运算必须在具有矢量执行单元的3个端口上运行。 p01和p015 uo拥有足够的调度灵活性,没有哪个端口会成为瓶颈,因此我们只需要看一下ALU的总吞吐量即可。 3个端口为4 uops/迭代,每个1.333周期一个物理核心的最大平均吞吐量为1迭代。

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

    超线程应该使您的执行单元饱和,并具有一些前端吞吐量来节省。每个线程应平均每2.666c分配一个线程,而前端则可以每2.5c分配一个线程。由于延迟仅使您每2c限制一次,因此由于资源冲突而在关键路径上出现任何延迟之后,延迟可能会 catch 。 (一个vptest uop从其他两个uops中的一个窃取了一个循环)。

    如果您可以更改循环以减少检查频率,或者减少向量矢量,那将是一个成功。但是我想到的一切都是更多矢量(例如vpand而不是vptest,然后vpor在检查之前将这些结果合并在一起...或者vpxorvptest会生成全零矢量的情况下)。也许有一个向量XNOR之类的东西,但是没有。

    要检查实际发生的情况,您可以使用性能计数器来分析当前代码,并查看整个内核(而不仅仅是每个逻辑线程)所获得的uop吞吐量。或分析一个逻辑线程,然后查看它是否已达到p015的一半。

    09-27 19:14