背景:
在使用嵌入式汇编语言优化某些Pascal代码时,我注意到一条不必要的MOV指令,并将其删除。
令我惊讶的是,删除不必要的指令导致我的程序速度减慢。
我发现添加任意、无用的MOV
指令会进一步提高性能。
效果是不稳定的,并且基于执行顺序的更改:由单行向上或向下转置的相同垃圾指令会产生减速。
我知道CPU会进行各种优化和精简,但这更像是黑魔法。
数据:
我的代码的一个版本在一个循环的中间有条件地编译了三个垃圾操作,该循环运行了1048576次。(周围的程序只计算SHA-256散列)。
我相当旧的机器(Intel(R)Core(TM)2 CPU 6400@2.13)上的结果
avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without: 1836.44 ms
这些程序循环运行25次,每次运行顺序随机变化。
摘录:
{$asmmode intel}
procedure example_junkop_in_sha256;
var s1, t2 : uint32;
begin
// Here are parts of the SHA-256 algorithm, in Pascal:
// s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
// s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
// Here is how I translated them (side by side to show symmetry):
asm
MOV r8d, a ; MOV r9d, e
ROR r8d, 2 ; ROR r9d, 6
MOV r10d, r8d ; MOV r11d, r9d
ROR r8d, 11 {13 total} ; ROR r9d, 5 {11 total}
XOR r10d, r8d ; XOR r11d, r9d
ROR r8d, 9 {22 total} ; ROR r9d, 14 {25 total}
XOR r10d, r8d ; XOR r11d, r9d
// Here is the extraneous operation that I removed, causing a speedup
// s1 is the uint32 variable declared at the start of the Pascal code.
//
// I had cleaned up the code, so I no longer needed this variable, and
// could just leave the value sitting in the r11d register until I needed
// it again later.
//
// Since copying to RAM seemed like a waste, I removed the instruction,
// only to discover that the code ran slower without it.
{$IFDEF JUNKOPS}
MOV s1, r11d
{$ENDIF}
// The next part of the code just moves on to another part of SHA-256,
// maj { r12d } := (a and b) xor (a and c) xor (b and c)
mov r8d, a
mov r9d, b
mov r13d, r9d // Set aside a copy of b
and r9d, r8d
mov r12d, c
and r8d, r12d { a and c }
xor r9d, r8d
and r12d, r13d { c and b }
xor r12d, r9d
// Copying the calculated value to the same s1 variable is another speedup.
// As far as I can tell, it doesn't actually matter what register is copied,
// but moving this line up or down makes a huge difference.
{$IFDEF JUNKOPS}
MOV s1, r9d // after mov r12d, c
{$ENDIF}
// And here is where the two calculated values above are actually used:
// T2 {r12d} := S0 {r10d} + Maj {r12d};
ADD r12d, r10d
MOV T2, r12d
end
end;
自己试试:
如果你想亲自试用,代码可以在GitHub上在线下载。
我的问题:
速度提高的最可能原因是:
您的Core2不会为每个条件跳转保留单独的历史记录。相反,它保留了所有条件跳转的共享历史。全局分支预测的一个缺点是,如果不同的条件跳跃不相关,则历史会被无关信息稀释。
这个分支预测小教程展示了分支预测缓冲区的工作原理。缓存缓冲区由分支指令地址的较低部分索引。除非两个重要的不相关分支共享相同的低位,否则这种方法效果很好。在这种情况下,最终会出现别名,这会导致许多预测失误的分支(这会暂停指令管道并减慢程序的速度)。
如果您想了解分支预测失误是如何影响性能的,请看下面这个极好的答案:https://stackoverflow.com/a/11227902/1001643
编译器通常没有足够的信息来知道哪些分支将别名以及这些别名是否重要。但是,可以在运行时使用Cachegrind和VTune等工具确定这些信息。
你可能想看看http://research.google.com/pubs/pub37077.html
TL;DR:在程序中随机插入nop指令可以很容易地将性能提高5%或更多,不,编译器不能轻易利用这一点。它通常是分支预测器和缓存行为的组合,但也可以是例如预订站暂停(即使没有中断的依赖链或明显的资源过度订阅)。
我相信在现代CPU中,汇编指令虽然是程序员向CPU提供执行指令的最后可见层,但实际上是CPU实际执行的几层。
现代CPU是RISC/CISC的混合体,它们将CISC x86指令转换为行为更RISC的内部指令。此外,还有无序执行分析器、分支预测器、英特尔的“微操作融合”,它们试图将指令分组为更大批量的同时工作(有点像VLIW/安腾泰坦尼克号)。甚至还有缓存边界,如果代码更大(也许缓存控制器更智能地插入它,或者让它保持更长时间),它可以让代码运行得更快,这是天知道的。
CISC总是有一个汇编到微码的转换层,但问题是,使用现代CPU时,事情要复杂得多。由于现代半导体制造厂中存在着所有额外的晶体管不动产,CPU可能可以并行应用几种优化方法,然后在最后选择一种提供最佳加速比的方法。额外的指令可能会使CPU偏向使用一条优于其他路径的优化路径。
额外指令的效果可能取决于CPU型号/代/制造商,并且不太可能是可预测的。以这种方式优化汇编语言需要针对许多CPU架构代执行,可能使用特定于CPU的执行路径,并且仅适用于非常重要的代码部分,尽管如果您正在进行汇编,您可能已经知道这一点。