论程序优化一般考虑的几个方向

写程序最主要的目标是使它在任何情况下都能正确工作,但在很多情况下,让程序运行的快也是一个重要的因素。编写高效程序要做到一下几点:

  • 必须选择一组适当的数据结合和算法;
  • 编写出编译器能有效优化的源代码;
  • 编写适合不同处理器架构和性能的代码。

当然,有人可能会说,优化代码可以依靠编译工具(如GCC)的优化选项,为什么还需要我们自己亲自干呢?有这种疑问的请继续阅读,没有疑问的可以跳过第0节。

0. 为什么不能完全依靠编译器的自动优化?

介绍一下常用GCC编译器的优化选项:

  1. 基本优化命令行选项:-Og 一般用于小型程序的优化,用于学习理解
  2. 一般优化命令行选项:-O1 一般用于小型软件项目的优化
  3. 标准优化命令行选项:-O2 一般用于大中型软件项目的优化,较多使用
  4. 高级优化命令行选项:-O3 一般用于超大型软件项目的优化,较少使用

编译器必须很小心的对程序只使用安全的优化,即对于程序可能遇到的所有情况,在优化前后必须得到相同的结果。为了解释一种程序转换是否安全,我们先看一个例子:

void twiddle_1(long *xp, long *yp)
{
    *xp += *yp;
    *xp += *yp;
}

void twiddle_2(long *xp, long *yp)
{
    *xp += 2 * (*yp);
}

咋一看,这两个程序似乎表达同一个意思,而且第2个会更高效一点。因为第一个程序总共要进行6次内存引用,而第二个只需要3次内存引用。

先介绍一个术语——内存别名使用:两个指针指向同一个内存位置的情况。

由于在优化之前,编译器会考虑所有可能的情况,其中一种情况就是“内存别名使用”。此时,第一个函数会执行以下的计算:

     *xp += *xp;     /*double value at xp*/
     *xp += *xp;     /*double value at xp*/

结果是xp的值增加4倍。另一方面,第二个函数twiddle_2会执行以下的计算:

     *xp += 2 *(*xp);     /*triple value at xp*/

结果是xp的值增加3倍。但编译器并不知道你会如何使用第一个函数twiddle_1,保险起见它不能将函数1优化成函数2的样子。所以我们不能完全依靠编译器的自动优化工具来对自己的代码进行优化,而是要结合代码实际使用情况,有针对性的结合几种优化原则,来帮助编译器对代码进行无差错优化。

1. 减少函数调用次数

循环语句中的代码往往要执行多次,它的高效与否往往决定了整个程序的性能瓶颈。所以在循环语句中,我们要尽可能的降低其运行消耗(时间消耗内存消耗)。先看一段代码:

void lower_1(char *s)
{
  int i;
  for(i=0; i<strlen(s); i++)
    if(s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a'); 
}

void lower_2(char *s)
{
  int i;
  int len = strlen(s); 
  for(i=0; i<len; i++)
    if(s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a'); 
}

第一个函数中,每次循环都要调用函数strlen一次,而函数调用所花费的性能代价是很大的。第二个函数在循环展开之前就把字符串的长度存在了一个标量中,以后循环中就使用该标量值来代替调用函数strlen,而从寄存器中读取一个值相比于调用一个函数来获得值无论在时间上还是存储开销上都大大降低。实验可知,当字符串长度为100万字符时,函数lower_1需要花费1000秒的时间,而函数lower_2仅需要2毫秒,性能提升近50万倍!

2. 减少内存引用次数

加载或读取内存地址是非常耗时耗力的,虽然现在绝大多数处理器都使用了缓冲技术来减少程序运行中的内存引用的时间开销,但相比于直接读取寄存器它的速度还是相当相当慢的。看下面一个例子:

void func1(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  for(i=0; i < len; i++){
    *dest += a[i];
  }
}

void func2(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int val=0;
  for(i=0; i < len; i++){
    val += a[i];
  }
  *dest = val;
}

第一个函数中,每次循环都要对内存地址dest进行一次读写;而第二个函数巧妙的设定了一个局部变量用于循环内的计算,当循环结束在将结果放到指定的内存地址dest处。

3. 循环展开

循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。

  1. 它减少了操作的数量,例如循环索引语句和条件分支语句的计算次数;
  2. 它提供了一些方法进一步改进代码,减少关键路径上的操作次数。

3.1 减少循环次数

下面一段程序将上节中的函数func2中的for语句进行了“2x1”循环展开:

void func3(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int limit = length-1;
  int val=0;
  for(i=0; i < limit; i+=2){
    val =  val + a[i] + a[i+1];
  }
  for( ; i < len; i++){
    val =  val + a[i];
  }
  *dest = val;
}

当然,我们也可以进行“kx1”循环展开,

void func3(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int limit = length -(k -1);
  int val=0;
  for(i=0; i < limit; i+=2){
    val = val + a[i] + ... +a[i+k-1];
  }
  for( ; i < len; i++){
    val =  val + a[i];
  }
  *dest = val;
}

3.2 提高并行性

随着集成度的提高,现代处理器一般都具有多核结构,或者支持超标量技术(SIMD),这就给我们程序员编码提供了另一种选择,利用编写并行性较高的程序 来达到提高运行速度的目的。

对于一个可结合和可交换的合并运算来说(整数加或乘),我们可以通过将一组合并运算分割成两个或更多的部分,并在最后合并结果来提高性能。

我们将func3函数重新编写成“2x2”循环展开形式:

void func4(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int limit = length-1;
  int val_1=0;
  int val_2=0;
  for(i=0; i < limit; i+=2){
    val_1 +=  a[i];
    val_2 += a[i+1];
  }
  for( ; i < len; i++){
    val_1 +=  a[i];
  }
  *dest = val_1 + val_2;
}

相应的“kxk”模式,相比大家应该知道怎么变了吧?此处k的最大取值取决于你程序运行所在处理器中寄存器的个数和流水线级数。一般情况下k取10以内问题都不大。

3.3 重新结合变换

这是一种利用改变结合变换达到提高并行性目的的方法。

废话不多说,看码:

void func3(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int limit = length-1;
  int val=0;
  for(i=0; i < limit; i+=2){
    val =  val + a[i] + a[i+1];
  }
  for( ; i < len; i++){
    val =  val + a[i];
  }
  *dest = val;
}

void func5(int *a[], int length, int *dest)
{
  int i;
  int len = length;
  int limit = length-1;
  int val=0;
  for(i=0; i < limit; i+=2){
    val =  val + (a[i] + a[i+1]);  /*此处添加了一对圆括号*/
  }
  for( ; i < len; i++){
    val =  val + a[i];
  }
  *dest = val;
}

正如以上代码所示,func5仅比func3在循环语句中添加了一对括号,从而改变了该语句执行时的顺序,产生了我们称为“2x1a”的循环展开形式。
对于刚接触C的来说,这两个函数看上去本质是一样的,但是当我们运用性能分析工具gprof来比较这两个函数的时候,会得到令人吃惊的结果:2x1a的性能几乎等同于2x2的性能

考虑v = v * x * y * z;如何添加括号,以达到最优性能呢?

答案:v = v * ((x * y) * z)或v = v * (x * (y * z))

4. 用条件送传指令代码代替分支语句

总所周知,现代处理器普遍具有分支预测功能,它为我们程序的快速运行提供了不小的功劳。例如在预测循环测试条件时,我们处理器总是判定条件成立(当然,程序运行的绝大部分时间也确实如此),只有在最后一次时,我们才会预测错误。而这时付出的代价和前面所带来的收益可以忽略不记。

因为绝大部分循环条件测试的结果都是显而易见的,我们可以忽略分支预测错误的惩罚。但在一些场合(比较大小),他的测试结果是随机的,此时我们就不得不考虑分支预测错误时所带来的惩罚了。为此有没有一种方法来避免使用条件分支呢?答案看码!

  /*给定两个数组,比较每个相对应的位置的值,
  将小的放在a数组中,大的放在b数组中。*/
void minmax_1(int a[], int b[], int len)
{
  int i;
  for(i=0; i<len; i++){
    if(a[i] > b[i]){
      int t = a[i];
      a[i] = b[i];
      b[i] = t;
    }
}

void minmax_2(int a[], int b[], int len)
{
  int i;
  for(i=0; i<len; i++){
      int min = a[i] < b[i] ? a[i] : b[i];
      int max = a[i] < b[i] ? b[i] : a[i];    
      a[i] = min;
      b[i] = max;
    }
}
}

5.总结

上述总总描述了许多优化程序性能的基本策略:
1. 高层设计
为遇到的问题选择适当的算法和数据结构。

2.基本编码原则

  • 消除连续的函数调用,有可能时将计算或访存移到循环外。
  • 消除不必要的内存引用,引入临时变量来保存中间结果,在最后的值计算出来时,才存放到数组或全局变量中。

3.底层优化

  • 展开循环,降低开销,并且使得进一步的优化成为可能。
  • 通过多个累计变量和重新结合等技术,找到提高指令级并行。
  • 用条件传送指令代替条件语句,使得减少分支预测错误的惩罚。

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 注:这是第三遍读《C语言深度解剖》,想想好像自从大学开始就没读完过几本书,其中谭浩强的那本《C语言程序设计(第四版...
    HavenXie阅读 1,717评论 1 6
  • 生命始于一枚简单的细胞 而它可以成为各种各样的我们 缘分来源于未知的世界 却让我们在这里相遇 也许未来的我们会成为...
    第九片叶子阅读 421评论 6 8
  • 再怎么过 生活都是自己的 经历着 过的好的 过不好的 开心的 悲伤的 想要倾诉 与之陪伴的只有那些文字 文字背后 ...
    灰白印记阅读 312评论 2 0
  • 不识君 相识不相知, 相守却相忘。 吾与君别离, ...
    七彩姑娘会微笑阅读 227评论 2 3
  • 这里是黄石镇,古越国西南边陲的小镇,南临蛮荒森林,西邻万剑山余脉,东边是江水郡,北边就是大名鼎鼎博陵郡。此地出...
    觉解X阅读 315评论 0 0