优化程序性能

优化程序性能主要从两个步骤来考虑,第一步是消除不必要的内容,第二步是利用处理器的指令级并行能力。

消除不必要的内容

  • 消除低效率循环
    通过移动代码,将执行多次但计算结果不改变的计算移动到循环外面。例:

    for(i=0;i<str.length();i++)

    改为

    int lengtn = str.length();
    for(i=0;i<length;i++)
  • 减少过程的调用
    过程调用会带来相当大的开销,并且会妨碍程序的优化。
    然而还有说法说一个方法不应该超过7行代码。出于模块性和抽象性考虑有时我们还是需要将代码放到另一个方法里。
    针对这个问题还是综合考虑吧。

  • 消除不必要的存储器引用
    在之前的文章,程序的机器级表示(一)中,我们了解了如何访问寄存器及存储器。有时通过对代码的优化,可以减少对存储器的引用,从而提高性能。

    *dest = IDENT;
    for (i=0; i<length; i++){
        *dest = *dest OP data [i];  //OP代表操作符,如*
    }
    

    这段代码的汇编代码如下:
    这里写图片描述

    如果我们的代码这样写:

    data_t acc=IDENT;
    for (i=0; i<length; i++){
        acc = acc OP data[i];
    }
    *dest = acc;
    

    汇编代码如下:
    这里写图片描述

    可以看到,通过优化,我们将存储器操作从两次读和一次写减少到只需要一次读,有效提高了性能。

结合CPU特性的优化

  • 现代处理器
    现代高端处理器可以在每个时钟周期执行多个操作,并且是乱序的。整个设计有两个主要部分:

    • 指令控制单元(ICU):负责从存储器中读出指令序列,并根据这些指令序列生成微操作指令。
    • 执行单元(EU):负责执行这些操作。

    这里写图片描述
    上图是一个现代处理器的框图,其中:

    • 指令高速缓存:最近访问的指令。
    • 指令译码器:接受汇编程序指令,将其转换为微操作。
    • 功能单元:专门处理特定类型的微操作。
    • 数据高速缓存:最近访问的数据。
    • 退役单元:控制着寄存器文件的更新。当指令操作完成,并且导致这条指令的分支点也都被确认为预测正确,这条指令就可以退役了 ,对程序寄存器的更新都会被实际执行。如果某个分支点预测错误,这条指令会被清空,丢弃所有计算出来的结果。
    • 操作结果:任何对程序状态的更新都只会在指令退役时才发生。为了加速一条指令到另一条指令的结果传送,需要在执行单元间交换操作结果,执行单元可以直接将结果发送给彼此。控制操作结果在执行单元间传送最常见的机制是寄存器重命名,即通过数据转发的方式,将操作结果从一个操作发送到另一个操作,而不是写到寄存器文件再读出来。

    现代处理器使用了分支预测技术,会猜测是否会选择分支,并预测分支的目标地址。使用投机执行技术在确定分支预测是否正确之前就进行取指译码,对操作求值,但是结果并不会放在寄存器或存储器中,直到确定分支预测正确。

    这里写图片描述
    上图是Core i7中延迟与吞吐量界限的最小值,功能单元的性能由两个周期计数值来刻画:

    • 延迟:完成运算需要的总时间。
    • 吞吐量(发射时间):两个连续相同运算之间需要最小的时钟周期。
  • 循环展开
    循环展开能够从两个方面优化程序的性能,一是减少与结果无直接相关的操作;二是减少整个计算中关键路径上的操作数量。
    首先看下combine4代码中的关键路径:
    这里写图片描述
    其中mulss指令被扩展为一个load操作和一个mul操作。根据上面处理器各操作性能的表格我们可以知道,关键路径就是图中所示的mul操作。

    下面是循环展开优化之后的代码:

    int limit = length - 1;
    data_t acc = IDENT;
    for (i=0; i<limit ; i+=2){
        acc = (acc OP data[i]) OP data[i+1];
    }
    for (; i<length; i++){
        acc = acc OP data[i];
    }
    *dest = acc;

    在这段代码中,每次循环我们进行两次操作运算,后面一个for循环用来处理最后几个(这里是展开2次,所以最多剩一个)元素。通过这样的变换,我们减少了与结果无直接相关的操作,但是关键路径上的操作数量并没有减少。下面我们会了解怎样通过结合CPU的流水线特性提高程序的并行性来提升性能。

  • 提高并行性

    • 多个累积变量
      程序运算受到延迟的限制,但是加法与乘法功能单元是完全流水线化的,即每个时钟周期可以开始一个新操作。代码无法利用这种能力,因为我们将累积值放在一个单独的变量中,在前面的计算完成之前都不能进行新的计算。那么我们如何利用这种能力呢?见如下代码:

      int limit = length - 1;
      data_t acc0 = IDENT;
      data_t acc1 = IDENT;
      for (i=0; i<limit ; i+=2){
          acc0 = acc0 OP data[i]; 
          acc1 = acc1 OP data[i+1];
      }
      for (; i<length; i++){
          acc0 = acc0 OP data[i];
      }
      *dest = acc0 OP acc1;

      在两次循环展开的基础上,使用两路并行。这种方法便利用了功能单元的流水线能力,其关键路径如下图:
      这里写图片描述

    • 重新结合变换
      我们将循环展开那里的代码稍微进行一些变化:

      int limit = length - 1;
      data_t acc = IDENT;
      for (i=0; i<limit ; i+=2){
          acc = acc OP (data[i] OP data[i+1]);
      }
      for (; i<length; i++){
          acc = acc OP data[i];
      }
      *dest = acc;

      可以看到,只是将第一个循环中的操作重新结合了一下。但是这段代码的效率与优化为多个累积变量的代码差不多,让我们看下它的关键路径:
      这里写图片描述
      data[i]*data[i+1]的操作并不依赖acc变量,所以这段代码也提高了并行性。

性能提高技术

优化程序性能的基本策略:

  • 高级设计。选择适当的算法和数据结构。
  • 基本编码原则。
    • 在可能时,将计算移到循环外。
    • 消除连续的函数调用。考虑有选择地妥协程序的模块性以获得更大的效率。
    • 消除不必要的存储器引用。引人临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。
  • 低级优化。
    • 展开循环,降低开销,并且使得进一步的优化成为可能。
    • 通过使用多个累积变量和重新结合等技术,提高指令级并行。
    • 用功能的风格重写条件操作,使得编译采用条件数据传送。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值