优化C++软件(14)

11. 乱序执行

所有现代x86 CPU可以乱序执行指令或者同时做多件事,除了某些小的低功耗CPU(Intel Atom)。下面的例子显示了如何利用这个能力:

// 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,虽然这看起来显而易见。为什么编译器不能在浮点表达式上进行这种优化的原因是,它可能导致精度损失,如第64页所述。你必须手动设置括号。

长依赖链影响更大。这通常是循环里的情形。考虑下面的例子,它计算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仅有一个浮点加法单元,但这个单元是流水线化的,如上所述,因此它可以在前面的加法完成周期,开始一个新的加法。

浮点加法与乘法的最优累加器数是3或4,依赖于CPU。

如果迭代数不能被展开因子整除,展开循环变得有点复杂。例如,如果例子11.2b中list元素数是奇数,我们必须在循环外增加最后的元素,或者向list增加一个额外伪元素并将它置零。

如果没有循环携带依赖链,展开循环与使用多个累加器是不必要的。具有乱序能力的微处理器可以重叠迭代,在前面迭代完成之前,开始一个迭代的计算。例子:

// 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关键字。CodeGear编译器不能制作浮点寄存器变量,而是将temp保存在内存。这阻止了CPU重叠计算。
  • 循环分支应该被预测。如果重复计数很大或是常量,这不是问题。如果循环计数小且改变,那么CPU偶尔预测循环退出,而实际上没有,因此不能开始下一个计数。不过,乱序机制允许CPU预先递增循环计数器,使得它能不至太迟检测到误预测。因此,你不必太担心这个情形。

一般来说,乱序执行机制自动工作。不过,程序员可以做一些事情来最大程度地利用乱序执行。最重要的是避免长依赖链。另一件你可以做的事情是混合不同类型的操作,以在CPU中不同执行单元间平均分配工作。混合整形与浮点计算是有好处的,只要你不需要在整形与浮点值之间转换。混合浮点加法与浮点乘法,混合简单整形与向量整形操作,以及混合数学计算与内存访问也是有好处的。

非常长的依赖链对CPU的乱序资源施加压力,即使它们不带入循环的下一个迭代。现代CPU通常可以处理超过100个挂起的操作(参考手册3《Intel,AMD与VIA CPU的微架构》)。将一个循环分解并保存中间结果,对打破极长的依赖链是有帮助的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
性能优化指南是指在软件或系统开发过程中,通过一系列的优化措施来提升程序的执行效率和系统的性能表现。这样可以使得软件和系统更加高效地运行,提供更好的用户体验。 性能优化的目标在于减少程序的资源消耗、提高响应速度和降低延迟。为了达到这些目标,我们可以从多个方面入手: 1. 代码优化:通过优化算法、减少冗余代码、精简逻辑等方式,改善代码的执行效率。同时,要避免频繁的内存分配和释放操作,使用更高效的数据结构和算法。 2. 并发优化:对于一些多线程或并行计算的场景,可以采用合适的并发模型和数据共享机制,避免竞争条件和死锁,提高并发性能。 3. 数据库优化优化数据库的设计和索引,合理使用查询语句和事务,避免频繁的数据库访问和大量的数据传输,提高数据库的读写效率。 4. 网络优化优化网络通信协议和数据传输方式,减少数据传输量和网络延迟,提高网络通信的效率。 5. 缓存优化:通过合理设置缓存机制,减少重复计算和数据查询的开销,提高系统的响应速度。 6. 资源管理优化:合理管理系统资源,及时释放未使用的资源,减少资源的浪费。 7. 日志优化:精简和优化日志输出,避免不必要的日志记录,减轻系统负担。 8. 前端优化优化前端页面的加载速度和渲染性能,减少页面的请求次数和数据量,提高用户体验。 综上所述,性能优化是一个综合性的工作,要从代码、并发、数据库、网络、缓存、资源管理、日志和前端等多个方面入手。通过适当的优化策略和手段,可以提升软件和系统的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值