Dav*_*erd 8 performance assembly x86-64 micro-optimization avx2
环境:
我没有写很多汇编程序代码,当我这么做时,它要么足够短,要么足够简单,以至于我不必担心压缩它的最大数量.我的更复杂的代码通常用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
Run Code Online (Sandbox Code Playgroud)
是的,这几乎是一个依赖链的教科书示例:这个紧密的小循环中的每个指令都取决于前一个操作的结果.这意味着没有并行性,这意味着我没有充分利用处理器.
受Agner的"优化汇编程序"文档的启发,我想出了一种方法(希望)允许我一次做2个操作,所以我可以有一个管道更新ymm2和另一个更新(比如说)ymm8.
虽然这是一个非平凡的变化,所以在我开始撕掉所有东西之前,我想知道它是否可能有所帮助.看看Agner的kaby lake(我的目标)的"指令表",我看到:
uops
each
port Latency
pminub p01 1
psubb p015 1
ptest p0 p5 3
Run Code Online (Sandbox Code Playgroud)
鉴于此,看起来当一个管道使用p0 + p5对ymm2进行vptest时,另一个可以利用p1在ymm8上同时执行vpminub和vpsubb.是的,事情仍然会落后于vptest,但它应该会有所帮助.
或者是吗?
我目前正在从8个线程运行此代码(是的,8个线程确实给我提供了比4,5,6或7更好的总吞吐量).鉴于我的i7700k有4个超线程内核,每个内核上运行2个线程的事实是不是意味着我已经最大化了端口?端口是"每个核心",而不是"每个逻辑CPU",对吧?
所以.
根据我目前对Agner工作的理解,似乎无法以当前形式进一步优化此代码.如果我想要更好的性能,我将需要提出一个不同的方法.
是的,我确定如果我在这里发布我的整个asm例程,有人可能会提出另一种方法.但是这个问题的目的不是让别人为我编写代码.我试图看看我是否开始理解如何考虑优化asm代码.
这(大致)是看待事物的正确方法吗?我错过了几件吗?或者这是完全错误的吗?
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(the jnz)上运行.它不能成为瓶颈,因为前端发布速率限制我们在运行此循环时每个时钟发出的分支少于一个.
每次迭代的其他4个向量ALU uop必须在具有向量执行单元的3个端口上运行.p01和p015 uops具有足够的调度灵活性,没有单个端口会成为瓶颈,因此我们只需查看总ALU吞吐量.对于3个端口,这是4 uops/iter,对于每1.333个周期一个物理核心的最大平均吞吐量.
对于单线程(无HT),这不是最严重的瓶颈.但是有两个超线程,每2.6666个周期就是一个.
超线程应该使执行单元饱和,并且需要一些前端吞吐量.每个线程应该平均每2.666c一个,前端能够每2.5c发出一个.由于延迟仅将您限制为每2c一个,因此在资源冲突导致关键路径上的任何延迟后,它可以赶上.(vptest从另外两个uops中偷走一个循环).
如果您可以更改循环以检查任何频率,或使用更少的向量uops,那可能是一个胜利.但是我想到的一切都是更多的向量uops(例如vpand,在检查之前代替vptest并且然后vpor将其中的几个结果放在一起......或者vpxor当它vptest会产生全零向量时).也许如果有一个向量XNOR或其他东西,但没有.
要检查实际发生的情况,您可以使用perf计数器来分析当前代码,并查看您为整个核心获取的uop吞吐量(不仅仅是每个逻辑线程单独).或者描述一个逻辑线程,看看它是否在p015的一半饱和.