※天道酬勤※

§水至清则无鱼,人至察则无徒§
posts - 65, comments - 11, trackbacks - 0, articles - 0
  IT博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

i++和++i的效率差别(转)

Posted on 2006-12-05 14:38 五指魅力 阅读(770) 评论(1)  编辑 收藏 引用
首先声明,简单的比较前缀自增运算符和后缀自增运算符的效率是片面的,因为存在很多因素影响这个问题的答案。  
   
   
  首先考虑内建数据类型的情况:  
   
  如果自增运算表达式的结果没有被使用,而仅仅简单的用于增加一员操作数,答案是明确的,前缀法和后缀法没有任何区别,编译器的处理都应该是相同的,很难想象得出有什么编译器实现可以别出心裁在二者之间制造任何差异。  
  测试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上。

Feedback

# re: i++和++i的效率差别(转)  回复  更多评论   

2007-03-24 22:01 by xawi2000
精彩,谢谢楼主.
不过linux的汇编格式怪怪的,看不大懂.
只有注册用户登录后才能发表评论。