11 Out of order execution

文章讲述了如何通过利用现代x86CPU的无序执行能力,通过修改代码结构(如循环展开和避免依赖链)来优化浮点计算性能。强调了乱序执行、寄存器重命名和循环条件对性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

除了一些小型低功耗的CPU(如英特尔Atom),所有现代x86 CPU都能够无序执行指令或同时执行多个任务。以下示例展示了如何利用这种能力:

// Example 11.1a
float a, b, c, d, y;
y = a + b + c + d;

这个表达式计算为((a+b)+c)+d。这是一个依赖链,在每个加法操作中都必须等待前一个加法的结果。你可以通过改写来改善这个情况:// Example 11.1b

float a, b, c, d, y;
y = (a + b) + (c + d);

现在这两个括号可以独立计算了。CPU会在完成(a+b)的计算之前开始计算(c+d)。这可以节省几个时钟周期。你不能假设优化编译器会自动将示例11.1a中的代码改写为11.1b,尽管看起来这是一个明显的事情。编译器不对浮点表达式进行此类优化的原因是它可能导致精度损失,如第75页所述。你必须手动设置括号。
当依赖链很长时,其效果更为明显。这通常发生在循环中。考虑以下示例,它计算100个数字的总和:

// Example 11.2a
const int size = 100;
float list[size], sum = 0; int i;
for (i = 0; i < size; i++) sum += list[i];

这有一个很长的依赖链。如果一个浮点加法需要5个时钟周期,那么这个循环将花费大约500个时钟周期。你可以通过展开循环并将依赖链拆分为两个来显着改善性能:

// Example 11.2b
const int size = 100;
float list[size], sum1 = 0, sum2 = 0; int i;
for (i = 0; i < size; i += 2) {
sum1 += list[i];
sum2 += list[i+1];}
sum1 += sum2;

如果一个只有一个加法器的微处理器在时间T到T+5进行一次加法操作将结果累加到sum1,那么它可以在时间T+1到T+6进行另一次加法操作将结果累加到sum2,整个循环将仅花费256个时钟周期。
在每次迭代都需要前一次迭代结果的计算循环被称为循环传递的依赖链。这种依赖链可能非常长且耗时。如果能够打破这种依赖链,将会带来很大的收益。两个求和变量sum1和sum2被称为累加器。当前的CPU通常拥有一个或两个浮点加法单元,并且这些单元是流水线处理的,就像之前解释的那样,因此它可以在前一个加法完成之前启动新的加法操作。
对于浮点加法和乘法,最佳的累加器数量可能是三个或四个,具体取决于CPU的设计。
如果迭代次数不能被展开因子整除,展开循环就会变得稍微复杂一些。例如,在示例11.2b中,如果列表中的元素数目是奇数,那么我们需要在循环外部添加最后一个元素,或者在列表中添加一个额外的虚拟元素,并将该额外元素置零。
如果没有循环传递的依赖链,就没有必要展开循环并使用多个累加器。具有乱序执行能力的微处理器可以重叠迭代,并在前一次迭代完成之前开始下一次迭代的计算。例如:

// Example 11.3
const int size = 100; int i;
float a[size], b[size], c[size];
float register temp;
for (i = 0; i < size; i++) {
temp = a[i] + b[i];
c[i] = temp * temp;
}

具有乱序执行能力的微处理器非常智能。它们可以检测到在示例11.3的循环的一个迭代中,寄存器temp的值与前一次迭代中的值无关。这使得它可以在完成使用上一个值之前开始计算新的temp值。它通过为temp分配一个新的物理寄存器来实现这一点,即使在机器码中出现的逻辑寄存器相同。这被称为寄存器重命名。CPU可以保存许多重命名的相同逻辑寄存器的实例。
这种优势是自动发生的。没有理由展开循环并使用temp1和temp2。如果满足某些条件,现代CPU可以进行寄存器重命名和并行多个计算。使CPU能够重叠循环迭代的计算的条件有:
• 没有循环传递的依赖链。一个迭代的计算中,除了循环计数器之外,不应该依赖于上一次迭代的结果(如果是整数,则计算速度很快)。
• 所有中间结果应保存在寄存器中,而不是内存中。重命名机制只对寄存器有效,不对内存或缓存中的变量有效。大多数编译器会将示例11.3中的temp设置为寄存器变量,即使没有使用register关键字。
• 循环分支应该被预测。如果重复计数是大的或者是常数,这不是问题。如果循环计数较小且会变化,那么CPU偶尔可能预测循环退出,而实际上并未退出,因此无法启动下一次计算。然而,乱序机制允许CPU提前增加循环计数器,以便在太迟之前检测到错误的预测。因此,您应该对此条件不过分担心。
总的来说,乱序执行机制是自动工作的。然而,程序员可以做一些事情以充分利用乱序执行。最重要的是避免长的依赖链。您可以做的另一件事是混合不同类型的操作,以便在CPU的不同执行单元之间均匀分配工作。只要不需要整数和浮点数之间的转换,将整数和浮点数计算混合在一起可能是有利的。将浮点数加法与浮点数乘法混合、将简单整数与向量整数操作混合,以及将数学计算与内存访问混合也可能是有利的。
非常长的依赖链会给CPU的乱序资源带来压力,即使它们没有传递到循环的下一次迭代中。现代CPU通常可以处理超过一百个待处理操作(请参阅手册3:"Intel、AMD和VIA CPU的微体系结构")。为了打破一个极长的依赖链,将循环分成两部分并存储中间结果可能是有用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值