我想学习如何推理汇编的速度。我实现了几种不同的方法来反转一个字节的位。其中一种使用了一种算法:
rev_u8_a:
movzx eax, dil
mov ecx, 2149582850
imul rcx, rax
movabs rdx, 36578664720
and rdx, rcx
movabs rax, 4311810305
imul rax, rdx
shr rax, 32
ret
另一个使用查找表:
rev_u8_d:
movzx eax, dil
lea rcx, [rip + .L__unnamed_1]
movzx eax, byte ptr [rax + rcx]
ret
第一个是8条指令,第二个是3条,但是第一个是寄存器,第二个访问内存。我如何去推理哪一个可能更快?在这两段代码中,哪一个更快?
1条答案
按热度按时间uhry853o1#
要说哪种处理器最快并不容易,因为现代处理器非常复杂,尤其是x86-64处理器。
首先,处理器以pipelined的方式执行指令,以便重叠操作,如获取指令、解码指令、执行指令。某些指令可能比其他指令花费更多的周期来完成。例如,
imul rcx, rax
在Skylake上花费3个周期来完成,而and rdx, rcx
花费1个周期。当执行多个指令时,它变得复杂。实际上,现代处理器可以执行multiple instruction in parallel during a given cycle。为此,它们跟踪指令之间的dependencies。一些指令可能有很高的延迟,但每个周期可以发出一条新指令,而另一些指令则不能很好地流水线化。例如,3个imul
在不同寄存器上的操作在Skylake上需要大约5个周期来完成,而1个imul
需要3个周期。关于如何使用代码,更重要的是减少其等待时间或指令数。实际上,由于x86-64指令被分解成微指令(即uop),这就更加复杂了。(宏融合)因此要生成1个微指令,1条指令就可以生成一个或多个微指令。处理器有一个完整的部分专门用于取指令,对它们进行解码,将它们转换成微指令等:另一部分负责指令的执行以及它们在许多执行单元上的调度:后端。在大多数处理器上,执行单元并不完全相同,因此调度指令非常复杂,特别是因为处理器应该集中在critical path上,以便在最少的周期内执行指令。实际上,微操作在许多端口上调度,每个端口可以执行有限的微操作类型列表。还有许多其他事项需要考虑:寄存器X1 E4 F1 X,因此为了避免/打破一些错误依赖性并增加并行性的量,以X1 E5 F1 X方式执行指令,因此为了增加并行性的量并减少停顿,预测存储器存取和分支,(例如,参见speculative execution),微指令被高速缓存,多个线程可以同时在同一内核上运行并共享资源(见SMT),uops can be fused too等,所有这些都只是触及了现代处理器如何工作的表面,现代x86-64处理器的复杂性类似于巨大的人类城市。有LLVM-MCA或uiCA之类的工具(据@PeterCordes说,这显然更好)来分析相对较大的代码,并了解处理器如何在实践中执行指令。它们中没有一个在实践中是好的,因为在运行时有许多事情要考虑。例如,一个代码在该高速缓存热的时候可以很快,在冷的时候可以很慢。代码的大小和内存访问模式一样重要。一些在芯片上实现的算法通常没有公开的文档(例如预取器)。尽管如此,像LLVM-MCA这样的工具可以很好地理解在简单代码中可能发生的情况(不考虑循环、分支未命中预测、缓存未命中等)。
在实践中,当高速缓存是冷的时,第一个代码可以更快,因为从DRAM获取数据通常需要几十纳秒当该高速缓存是热的,即数据在L1高速缓存中时,第二个通常可以更快(更少的微操作和更低的等待时间)。因此,如果LUT很小并且在循环中使用该代码,则第二代码可能更快。如果该代码执行一次,使用前者可能是一个更好的解决方案。2如果多个线程在同一个内核上运行,事情就有点复杂了。3性能与目标处理器紧密相关:一些处理器具有3个加载端口,而其它处理器可能仅具有1个加载端口,L1高速缓存的延迟在一个处理器上可以是3个周期,而在另一个处理器上可以是4个周期,两种不同架构之间的延迟和交互吞吐量可能非常不同(例如,苹果M1 VS ARM Cortex-A53,或者英特尔Skylake VS AMD挖掘机)。最好的做法是编写几个适合您的实际用例的基准测试。在编写基准测试时,您应该非常小心,因为一个糟糕的基准测试可能会导致截然不同的结论。首先,您可以阅读:绩效评估的惯用方式?。