【yebangyu博客】性能优化的那些传说和迷思

本文通过实例分析了编译器如何对代码进行优化,包括循环展开、消除重复计算以及移位代替除法。通过查看反汇编代码,展示了GCC在开启-O2优化选项后,如何自动完成这些常见的代码优化技巧。同时,文章提醒读者要对教科书中的建议保持批判性思维,理解编译器的优化策略并进行实践验证。
摘要由CSDN通过智能技术生成

转自:http://www.yebangyu.org/blog/2016/12/03/gccandperfopt/

相信你在很多书籍中见到很多代码调优(tuning code)的建议和方法。这些书籍可能包括《编程珠玑》、《深入理解计算机系统》、《程序设计实践》、《Optimized C++》等等。坦白说,这些书我都看过,它们确实提供了不少有意思的性能调优的方法,那么我们的问题是,这些建议和方法有效吗?所谓有效,一种衡量途径是,假如我们不那么做,是否编译器已经会自动优化了呢?

本文我们举几个例子,然后开启编译器优化选项后,看看发生了什么。

本文环境为:Ubuntu 14.04 32bit + Intel I7 CPU + GCC 4.8

生成汇编代码的语句是:g++ -S -O2 code.cpp

循环展开

对于下面的函数

 

1
2
3
4
5
6
void f1(int *a)
{
  for (int i = 0; i < 3;i++) {
    a[i] = a[i] + 2;
  }
}

它们建议可以将循环展开,变成这样:

 

1
2
3
4
5
6
void f2(int *a)
{
  a[0] = a[0] + 2;
  a[1] = a[1] + 2;
  a[2] = a[2] + 2;
}

OK,我们看看f1函数的反汇编代码:

 

1
2
3
4
5
    movl    4(%esp), %eax
    addl    $2, (%eax)
    addl    $2, 4(%eax)
    addl    $2, 8(%eax)
    ret

嗯,编译器已经自动帮你循环展开了。

练习:那么对于以下这种情况呢?是否有优化效果?

 

1
2
3
4
5
6
7
8
void f1(int *a)
{
  for (int i = 0; i < 3n; i += 3) {
    a[i] = a[i] + 2;
    a[i + 1] = a[i + 1] + 2;
    a[i + 2] = a[i + 2] + 2;
  }
}

循环条件去重

对于以下函数

 

1
2
3
4
5
6
7
8
int g1(char *p)
{
  int xx = 0;
  for (int i = 0 ; i < strlen(p); i++) {
    xx += p[i];
  }
  return xx;
}

它们建议你可以将第4行中的strlen(p)调用抽离出来放在循环开始前:

 

1
2
3
4
5
6
7
8
9
int g2(char *p)
{
  int xx = 0;
  int len = strlen(p);
  for (int i = 0 ; i < len; i++) {
    xx += p[i];
  }
  return xx;
}

OK,我们看看g1函数的反汇编代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.LFB26:
        pushl   %ebx
        subl    $24, %esp
        movl    32(%esp), %ebx
        movl    %ebx, (%esp)
        call    strlen //只有一次调用,不在循环里
        movl    %ebx, %edx
        leal    (%ebx,%eax), %ecx
        xorl    %eax, %eax
        jmp     .L2
.L3:
        movsbl  (%edx), %eax
        addl    $1, %edx
        addl    $9977, %eax
.L2:
        cmpl    %ecx, %edx
        jne     .L3
        addl    $24, %esp
        popl    %ebx
        ret

编译器已经帮你做了优化了。Why?本质原因是什么?

这是因为,首先,strlen函数的原型是:

size_t strlen(const char *p)

看到没,const char,也就是说这个函数不会改变输入参数,并且该函数不会对全局状态做一些设置和改变。而在函数g1内部,也只有对p的读,没有写,因此编译器可以放心地、大胆的做优化,把它当固定量。如果该函数可能有副作用,编译器是不会做这样的优化的。

因此,如果这里不是strlen,而是memcpy这样的函数,显然编译器无法做优化。如果这里不是strlen,而是你的一个辅助函数,比如说help,它的函数签名除了名字之外,和strlen一模一样。为了帮助gcc施展优化,你可以给gcc提供更多更好的提示和信息,比如说:

 

1
int attribute ((pure)) help(const char *p);

根据gcc文档,pure属性是用来修饰这样的函数:该函数除了返回一些值之外,不会产生其他作用和影响;并且它的返回值只依赖于它的输入参数和一些全局变量,比如说strlen和memcmp。

练习:实现一个简单的help函数,参数分别是const char *pvolatile char *p,查看gcc策略的差异。这个练习有助于让你发现,volatile是如何阻止编译器优化的。

移位代替简单除法

对于除以2,它们建议用移位来代替。比如说,对于下面的函数:

 

1
2
3
4
unsigned int h1(unsigned int a)
{
  return a / 2;
}

它们建议改为:

 

1
2
3
4
unsigned int h2(unsigned int a)
{
  return a >> 1;
}

查看h1函数的反汇编代码:

 

1
2
3
    movl    4(%esp), %eax
    shrl    %eax //移位指令
    ret

也就是说,编译器已经帮你做了优化。

练习:对于以下函数和“优化”,查看是否必要,是否有效

 

1
2
3
4
unsigned int h1(unsigned int a)
{
  return a * 9;
}

对应的“优化”版本:

 

1
2
3
4
unsigned int h2(unsigned int a)
{
  return (a << 3) + a; //括号不能丢哦。
}

那么,你可能会说了,什么是有效的呢?什么是编译器可能无法自动主动做的呢?你可以参考我的这篇博客。

写在最后

1,本文的演示仅仅是抛砖引玉,更重要的目的则有二:一是授人以鱼不如授人以渔,用一种方法和大家一起分析。二是,尽信书不如无书,必须报着怀疑的态度去读书,去验证,去思考。

2,教科书自然是强调原理的,强调思想的,它和实际有脱节这是无法避免的,也是必须这样的。无可厚非。我并不是说教科书说的这些优化不好,相反,如你所见,非常好。本文的目的是,很可能编译器已经帮你做了。

3,不同的编译器策略可能不同;同一个编译器不同版本策略可能也不同;同一个编译器的同一个版本在不同的上下文对于同一个函数的策略也可能不同。读者诸君务必不要过于相信本文,如我第一点所说:质疑!!!实践!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值