++i,i++的效率探讨

   关于i++,++i效率的问题!!下一篇blog打算写一下自定义类的自增效率问题。我事先在vc上运行过。其实必然是与编译器有着千丝万缕的联系,这里我们只针对vc。答案是效率一样,以下是我的反汇编结果:
之后偶然的机会在北邮人论坛上找到了讨论类似问题的一些回复,对方比我考虑的周到,所以这里就转载一篇人家的结论。对方可以说应该考虑的都考虑了!
首先声明,简单的比较前缀自增运算符和后缀自增运算符的效率是片面的,因为存在很多因素影响这个问题的答案。 
 
首先考虑内建数据类型的情况:
 
如果自增运算表达式的结果没有被使用,而仅仅简单的用于增加一员操作数,答案是明确的,前缀法和后缀法没有任何区别,编译器的处理都应该是相同的,很难想象得出有什么编译器实现可以别出心裁在二者之间制造任何差异。
测试C++源代码如下:
//test1.cpp
void test()
{
int i=0;
i++;
++i;
}
Gnu C/C++ 2编译的汇编中间代码如下: 
.file   "test1.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
        .align 4
.globl _test__Fv
        .def    _test__Fv;      .scl    2;      .type   32;     .endef
_test__Fv:
        pushl %ebp
        movl %esp,%ebp
        subl $24,%esp
        movl $0,-4(%ebp)    ;i=0
        incl -4(%ebp)        ;i++
        incl -4(%ebp)        ;++i
        jmp L3
        jmp L2
        .p2align 4,,7
L3:
L2:
        leave
        ret
很显然,不管是i++还是++i都仅仅是一条incl指令而已。
 
如果表达式的结果被使用,那么情况要稍微复杂一些。
测试C++源代码如下:
//test2.cpp
void test()
{
int i=0,a,b;
a=i++;
b=++i;
}
Gnu C/C++ 2编译的汇编中间代码如下: 
.file    "test2.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
    .align 4
.globl _test__Fv
    .def    _test__Fv;    .scl    2;    .type    32;    .endef
_test__Fv:
    pushl %ebp
    movl %esp,%ebp
    subl $24,%esp
    movl $0,-4(%ebp)        ;i=0
    movl -4(%ebp),%eax        ;i --> ax
    movl %eax,-8(%ebp)        ;ax --> a(a=i)
    incl -4(%ebp)            ;i++
    incl -4(%ebp)            ;++i
    movl -4(%ebp),%eax        ;i --> ax
    movl %eax,-12(%ebp)        ;ax --> b(b=i)
    jmp L3
    jmp L2
    .p2align 4,,7
L3:
L2:
    leave
    ret
有差别吗?显然也没有,同样是一条incl指令,再加上两条movl指令借用eax寄存器复制调用栈内容。
 
让我们再加上编译器优化,重新编译后的汇编代码如下: 
.file    "test2.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
    .align 4
.globl _test__Fv
    .def    _test__Fv;    .scl    2;    .type    32;    .endef
_test__Fv:
    pushl %ebp
    movl %esp,%ebp
    leave
    ret
好了,优化的过火了,由于i,a,b三个变量没有被使用,所以干脆全都被优化了,结果成了一个什么都不做的空函数体。
 
那么,让我们再加上一点代码使用a和b的结果吧,这样i的结果也不能够忽略了,C++源代码如下:
//test3.cpp
int test()
{
int i=0,a,b;
a=i++;
b=++i;
return a+b;
}
此时汇编代码如下: 
 .file    "test3.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
    .align 4
.globl _test__Fv
    .def    _test__Fv;    .scl    2;    .type    32;    .endef
_test__Fv:
    pushl %ebp
    movl %esp,%ebp
    movl $2,%eax
    leave
    ret
你还是没有想到吧,答案仅仅是编译器计算了返回值,常量展开(constant-unwinding)启动,变成了直接返回常量结果。
 
怎么办?我们把i变成参数,避免这种预期以外的结果,C++源代码如下:
//test4.cpp
int test1(int i)
{
int a=i++;
return a;
}
 
int test2(int i)
{
int a=++i;
return a;
}
好了,很辛苦,终于得到了不一样的汇编代码: 
 .file    "test4.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
    .align 4
.globl _test1__Fi
    .def    _test1__Fi;    .scl    2;    .type    32;    .endef
_test1__Fi:
    pushl %ebp
    movl %esp,%ebp
    movl 8(%ebp),%eax
    leave
    ret
    .align 4
.globl _test2__Fi
    .def    _test2__Fi;    .scl    2;    .type    32;    .endef
_test2__Fi:
    pushl %ebp
    movl %esp,%ebp
    movl 8(%ebp),%eax
    incl %eax
    leave
    ret
和你接触到的教条正相反吧,(这个结果还是让我比较吃惊的!)++i反而增加了一条汇编指令incl,而i++却没有,这就是编译器优化的魅力。
因为不管i有没有增加,都不影响a的值,而函数仅仅返回i的值,所以i的自增运算就根本不必进行了。
所以,为了更客观一些,我们将i参数改为按照引用传递,C++源代码如下;
//test5.cpp
int test1(int &i)
{
int a=i++;
return a;
}

 
int test2(int &i)
{
int a=++i;
return a;
}
这一次的结果加入了指针的运算,稍微复杂一些: 
.file    "test5.cpp"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.text
    .align 4
.globl _test1__FRi
    .def    _test1__FRi;    .scl    2;    .type    32;    .endef
_test1__FRi:
    pushl %ebp
    movl %esp,%ebp
    movl 8(%ebp),%eax
    movl (%eax),%edx
    incl (%eax)
    movl %edx,%eax
    leave
    ret
    .align 4
.globl _test2__FRi
    .def    _test2__FRi;    .scl    2;    .type    32;    .endef
_test2__FRi:
    pushl %ebp
    movl %esp,%ebp
    movl 8(%ebp),%eax
    movl (%eax),%edx
    leal 1(%edx),%ecx
    movl %ecx,(%eax)
    movl %ecx,%eax
    leave
    ret
惊讶吗?还是a=i++的代码更高效一些,不知道这会让你有什么想法。反正,我得出的结论,对于内建数据类型来说,i++和++i孰优孰劣,是编译器实现相关的,实在不必太可以关心这个问题。 
 
最后让我们再回到起点,对于自定义数据类型(主要是指类)说,不需要再做很多汇编代码的分析了,我很清楚的知道为什么会有人循循善诱。
因为前缀式可以返回对象的引用,而后缀式必须返回对象的值,所以导致了在大对象的时候产生了较大的复制开销,引起效率降低,因此会有劝告尽量使用前缀式,尽可能避免后缀式,除非从行为上真的需要后缀式。
这也就是More Effective C++/Term 7中的原文提到的,处理使用者自定义类型(注意不是指内建类型)的时候,应该尽可能的使用前缀式地增/递减,因为他天生体质较佳。
同时,为了保证前缀和后缀对递增/递减的语义的实现保持一致,设计上的一般原则是后缀式的实现以前缀式为基础,这样,后缀式往往多了一次函数调用,这也许也是一个需要考虑的效率因素,不过相比之下,就有点微乎其微了。
重申一点关于这个问题的进一步叙述,可以在Scott Mayer的<<More Effective C++>>一书的条款7中获得,大约在原书的P31-34上。
1、对于内置数据类型,以现在的编译器的优化水平,前++和后++没区别的,这个可以通过看汇编代码证明

2、对于自定义数据类型,像STL,前++的效率要高于后++,所以STL中关于iterator都是前++的

因为前置操作需要做的工作更少,只需要加1后返回加1后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加1之前的值作为操作的结果。对于int对象和指针,编译器可优化掉这项额外工作。但是对于更多的复杂迭代器类型,这种额外工作可能会花费更大代价。因此,养成使用前置操作这个好习惯,就不必担心性能差异的问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值