开了优化就没这么多破事了。
0x0 问题提出
今天同学突然问我,他在 OJ 上提交同一道题,分别使用了三元运算符和 if-else
语句,前者运行时间 46ms,后者则是 100ms,想知道为什么。
估摸着应该是编译器优化问题,不过还是给同学讲清楚比较好,也方便以后再有初学者问我的时候我可以直接丢链接顺便为博客增加点可怜的访问量。
0x1 关门,放汇编
计算机在运行时只认机器码,所有的高级语言都会被转换成 0 和 1 的组合,然而这个对人却不怎么友好。所以,比机器码稍微友好一点的汇编语言出现了,它可以帮助我们了解高级语言里的每一条语句和机器码的对应关系。
先看两段代码:
#include <bits/stdc++.h> using namespace std; int main() { int a,b; cin >> a >> b; int c; c = ((a > b) ? a : b); cout << c; return 0; }
#include <bits/stdc++.h> using namespace std; int main() { int a,b; cin >> a >> b; int c; if (a > b) c = a; else c = b; cout << c; return 0; }
唯一的区别就是在对变量c
赋值的时候,前者使用了三元运算符,后者使用了 if-else
语句。
在不开启编译器优化的情况下,二者生成的汇编分别为:
;9 : int c; ;10 : c = ((a > b) ? a : b); 0x40138b mov -0x10(%ebp),%edx 0x40138e mov -0x14(%ebp),%eax 0x401391 cmp %eax,%edx 0x401393 jle 0x40139a <main()+74> 0x401395 mov -0x10(%ebp),%eax 0x401398 jmp 0x40139d <main()+77> 0x40139a mov -0x14(%ebp),%eax 0x40139d mov %eax,-0xc(%ebp) ;11 : cout << c; 0x4013a0 mov -0xc(%ebp),%eax 0x4013a3 mov %eax,(%esp) 0x4013a6 mov $0x4c6860,%ecx 0x4013ab call 0x47e0c0 <std::ostream::operator<<(int)> 0x4013b0 sub $0x4,%esp
;9 : int c; ;10 : if (a > b) 0x40138b mov -0x10(%ebp),%edx 0x40138e mov -0x14(%ebp),%eax 0x401391 cmp %eax,%edx 0x401393 jle 0x40139d <main()+77> ;11 : c = a; 0x401395 mov -0x10(%ebp),%eax 0x401398 mov %eax,-0xc(%ebp) 0x40139b jmp 0x4013a3 <main()+83> ;12 : else ;13 : c = b; 0x40139d mov -0x14(%ebp),%eax 0x4013a0 mov %eax,-0xc(%ebp) ;14 : cout << c; 0x4013a3 mov -0xc(%ebp),%eax 0x4013a6 mov %eax,(%esp) 0x4013a9 mov $0x4c6860,%ecx 0x4013ae call 0x47e0c0 <std::ostream::operator<<(int)> 0x4013b3 sub $0x4,%esp
简单粗暴一点,使用三元运算符只有 8 条汇编指令,而使用 if-else
语句则有 9 条,多了一次寄存器赋值的操作 mov %eax,-0xc(%ebp)
。个人认为应该就是这里导致了效率的差异。
然后我们开启 -O2
优化,再次生成的汇编分别是:
;9 : int c; ;10 : c = ((a > b) ? a : b); 0x4b5776 mov -0x10(%ebp),%eax 0x4b5779 mov -0xc(%ebp),%edx ;11 : cout << c; 0x4b577f mov $0x4c6860,%ecx 0x4b5784 cmp %edx,%eax 0x4b5786 cmovl %edx,%eax 0x4b5789 mov %eax,(%esp) 0x4b578c call 0x47e000 <std::ostream::operator<<(int)> 0x4b5794 sub $0x4,%esp
;9 : int c; ;10 : if (a > b) 0x4b5776 mov -0x10(%ebp),%eax 0x4b5779 mov -0xc(%ebp),%edx ;11 : c = a; ;12 : else ;13 : c = b; ;14 : cout << c; 0x4b577f mov $0x4c6860,%ecx 0x4b5784 cmp %edx,%eax 0x4b5786 cmovl %edx,%eax 0x4b5789 mov %eax,(%esp) 0x4b578c call 0x47e000 <std::ostream::operator<<(int)> 0x4b5794 sub $0x4,%esp
可以看到,在编译器的优化帮助下,两种写法在执行上没有任何差异了,也就不存在效率上的孰优孰略问题。
0x2 结论
在现代编译器的优化之下,无论是使用三元运算符,还是 if-else
语句,都不会对效率产生任何影响——也本该如此,否则同功能的二者保留一个效率最高的不就好了。
至于为什么同学在运行的时候有差异,或许是因为他们学校的辣鸡 OJ 没开编译器优化吧。
0x3 延伸阅读
实际上最开始我没考虑到可能是同学使用的编译器没开优化这个问题,以为是有什么玄学的地方,在查阅相关资料的时候发现了 Compiler tricks in x86 assembly: Ternary operator optimization 这篇文章,里面讲述了比较神奇的东西。
这篇文章提到了,在现代 x86 处理器中,存在着 setcc
(此处的cc
是 condition code 的缩写)指令集,包括 setne
、sette
、setna
、seta
、setnb
、setb
等一系列指令。其官方描述为:
Sets the destination operand to 0 or 1 depending on the settings of the status flags (CF, SF, OF, ZF, and PF) in the EFLAGS register. The destination operand points to a byte register or a byte in memory. The condition code suffix (cc) indicates the condition being tested for.
根据 EFLAGS 寄存器中的状态标志(CF、SF、OF、ZF 和 PF)将目标操作数设置为 0 或 1。目标操作数在内存中指向字节寄存器或字节。状态码后缀(cc)指示将要执行测试的条件。
https://www.felixcloutier.com/x86/setcc
因为这次被问到的问题里似乎不太涉及到这个,所以就没仔细看这篇文章(实际上是没太看懂)。大概就是使用这一指令集可以减少条件赋值过程中使用加法的次数。
另外还有一个叫做“分支预测模型”就更高端了,有兴趣可以看这篇《深入理解 CPU 的分支预测(Branch Prediction)模型》。
迷惑问题,O2优化了之后这些玄学全没了。。。。
确实。不过被问到了就随便写写了。另外我觉得我这么整不怎么严谨甚至是错的……