汇编效率优化:打破依赖链
Posted TaigaComplex求职中
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了汇编效率优化:打破依赖链相关的知识,希望对你有一定的参考价值。
上一篇文章指令处理机制说过现代CPU处理指令的方式大多数都是out-of-order,那么为了更好地利用out-of-order这种处理机制,我们在编写程序的时候有必要规避过长的依赖链。
循环
如下面的一个C++例子,目的是计算长度为100的数组的总和:
// Example 9.3a, Loop-carried dependency chain double list[100], sum = 0.; for (int i = 0; i < 100; i++) sum += list[i];
上述代码中有100次加法运算,并且每次加法运算都依赖于上一次加法运算的结果。这是一个循环依赖链,一旦循环依赖链非常长,就有可能长时间使得out-of-order处理机制处于无法有效运转的状态。在上述例子中,在执行浮点运算时,只有加法运算i ++ 可以并行执行。
现假设浮点加法的latency为4,throughput为1,那么最优的实现方式就是并行执行4个浮点加法,如此一来就能充分利用浮点加法器的流水线机制。我们可以把代码修改如下:
// Example 9.3b, Multiple accumulators double list[100], sum1 = 0., sum2 = 0., sum3 = 0., sum4 = 0.; for (int i = 0; i < 100; i += 4) { sum1 += list[i]; sum2 += list[i+1]; sum3 += list[i+2]; sum4 += list[i+3]; } sum1 = (sum1 + sum2) + (sum3 + sum4);
这下循环里面有了四条依赖链,并且每条依赖链的长度为原来的四分之一,由于四条依赖链相互独立,因此可以并行执行,优化后的代码理论上的处理速度为原来的四倍。不过依赖链越多,CPU进行指令调度的难度也就越大,因此不一定能达到理论值。
现在的微处理器运行速度越来越快,可能在一个时钟周期可以同时处理4到5条指令,支持macro-op fusion的处理器甚至能处理更多,对于并行度越高的处理器,就更应该避免过长的依赖链。
普通线性运算
普通线性运算可以打破依赖链:
y = a + b + c + d;
上述式子可以写成以下形式:
y = (a + b) + (c + d);
如此一来a+b与c+d就能并行执行。不过可能有些编译器已经对这种形式的运算做了优化。
寄存器清零
前一篇文章我们讨论过:对整个寄存器进行写入时会使得寄存器重命名、打破依赖链。因此对寄存器清零是一种常用的打破依赖链的方式。
寄存器清零可以用 mov eax, 0 、 xor eax, eax 、 sub eax, eax、 sbb eax, eax 等多种方式。不过常用的是 xor 与 sub,原因如下面表格:
Instruction | length | side-effect |
mov eax, 0 | 5 bytes | - |
xor eax, eax | 2 bytes | - |
sub eax, eax | 2 bytes | - |
sbb eax, eax | 2 bytes | depend on carry flag |
我们之前也说过,对partial register写入是会导致falce dependence的,因此对partial register的清零是不能打破依赖链的。另外,现代处理器对清零操作的处理不用经过处理阶段,可以直接在寄存器重命名阶段对物理寄存器清零并对接上逻辑寄存器。
SIMD的相关寄存器也是有相应的xor,sub等指令,也常用于打破依赖链。
以上是关于汇编效率优化:打破依赖链的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向使用 Python 解析 ELF 文件 ( Capstone 反汇编 ELF 文件中的机器码数据 | 创建反汇编解析器实例对象 | 设置汇编解析器显示细节 )(代码片段