一个无数人讨论过的问题,今天终于看到一个人讲得全面而清楚。首先声明,简单的比较前缀自增运算符和后缀自增运算符的效率是片面的,因为存在很多因素影响这个问题的答案。
首先考虑内建数据类型的情况: 如果自增运算表达式的结果没有被使用,而仅仅简单的用于增加一员操作数,答案是明确的,前缀法和后缀法没有任何区别,编译器的处理都应该是相同的,很难想象得出有什么编译器实现可以别出心裁在二者之间制造任何差异。 测试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 ëp movl %esp,ëp subl $24,%esp movl $0,-4(ëp) i=0 incl -4(ëp) i++ incl -4(ëp) ++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 ëp movl %esp,ëp subl $24,%esp movl $0,-4(ëp) i=0 movl -4(ëp),êx i --> ax movl êx,-8(ëp) ax --> a(a=i) incl -4(ëp) i++ incl -4(ëp) ++i movl -4(ëp),êx i --> ax movl êx,-12(ëp) 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 ëp movl %esp,ëp 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 ëp movl %esp,ëp movl $2,êx 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 ëp movl %esp,ëp movl 8(ëp),êx leave ret .align 4 .globl _test2__Fi .def _test2__Fi; .scl 2; .type 32; .endef _test2__Fi: pushl ëp movl %esp,ëp movl 8(ëp),êx incl êx 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 ëp movl %esp,ëp movl 8(ëp),êx movl (êx),íx incl (êx) movl íx,êx leave ret .align 4 .globl _test2__FRi .def _test2__FRi; .scl 2; .type 32; .endef _test2__FRi: pushl ëp movl %esp,ëp movl 8(ëp),êx movl (êx),íx leal 1(íx),ìx movl ìx,(êx) movl ìx,êx leave ret 惊讶吗?还是a=i++的代码更高效一些,不知道这会让你有什么想法。反正,我得出的结论,对于内建数据类型来说,i++和++i孰优孰劣,是编译器实现相关的,实在不必太可以关心这个问题。
最后让我们再回到起点,对于自定义数据类型(主要是指类)说,不需要再做很多汇编代码的分析了,我很清楚的知道为什么会有人循循善诱。 因为前缀式可以返回对象的引用,而后缀式必须返回对象的值,所以导致了在大对象的时候产生了较大的复制开销,引起效率降低,因此会有劝告尽量使用前缀式,尽可能避免后缀式,除非从行为上真的需要后缀式。 这也就是More Effective C++/Term 7中的原文提到的,处理使用者自定义类型(注意不是指内建类型)的时候,应该尽可能的使用前缀式地增/递减,因为他天生体质较佳。 同时,为了保证前缀和后缀对递增/递减的语义的实现保持一致,设计上的一般原则是后缀式的实现以前缀式为基础,这样,后缀式往往多了一次函数调用,这也许也是一个需要考虑的效率因素,不过相比之下,就有点微乎其微了。
| | |