生之为萌,乐享创造

三元运算符和 if-else 的效率比较

其实就是优化与否的问题。

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 的缩写)指令集,包括setnesettesetnasetasetnbsetb等一系列指令。其官方描述为:

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)模型》。

  1. icebound说道:

    迷惑问题,O2优化了之后这些玄学全没了。。。。

    1. Robotxm说道:

      确实。不过被问到了就随便写写了。另外我觉得我这么整不怎么严谨甚至是错的……

发表评论

电子邮件地址不会被公开。 必填项已用*标注