汇编效率优化:打破依赖链

上一篇文章指令处理机制说过现代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, 0xor eax, eaxsub eax, eaxsbb eax, eax 等多种方式。不过常用的是 xorsub,原因如下面表格:

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等指令,也常用于打破依赖链。

原文链接: https://www.cnblogs.com/TaigaCon/p/7471002.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    汇编效率优化:打破依赖链

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/259153

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月14日 下午12:31
下一篇 2023年2月14日 下午12:32

相关推荐