为什么在c / c ++中,开关的优化方式与链接方式不同?


39

Square的以下实现产生了一系列cmp / je语句,就像我期望的链式if语句一样:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

并生成以下数据表以供返回:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

为什么gcc无法将最上层的优化为最下层的?

拆卸参考:https ://godbolt.org/z/UP_igi

编辑:有趣的是,MSVC生成一个跳转表,而不是针对切换情况的数据表。令人惊讶的是,clang将它们优化为相同的结果。


3
您是什么意思“不确定行为”?只要可观察到的行为相同,编译器就可以生成所需的任何汇编/机器代码
bolov

2
@ user207421忽略returns;案例没有breaks,因此开关也有特定的执行顺序。if / else链在每个分支中都有return,在这种情况下,语义是等效的。优化不是不可能的。作为反例,icc并未优化任何功能。
user1810087

9
也许是最简单的答案... gcc尚无法看到此结构并对其进行优化(尚未)。
user1810087

3
我同意@ user1810087。您只是找到了编译器优化过程的当前边界。当前未被子级(某些编译器认为)可优化的子子级。实际上,不是所有其他if链都可以通过这种方式进行优化,而只能对其中的SAME变量针对常数进行测试。
罗伯托·卡波尼

1
if-else从上到下具有不同的执行顺序。尽管如此,用if语句代替代码并不能改善机器代码。另一方面,该开关没有预定义的执行顺序,实际上只是一个美化的goto跳转表。话虽这么说,但允许编译器对此处的可观察行为进行推理,因此if-else版本的较差的优化令人非常失望。
伦丁

Answers:


29

所生成的代码switch-case通常使用跳转表。在这种情况下,通过查找表直接返回似乎是一种优化,它利用了这里的每种情况都涉及返回的事实。尽管该标准不能保证这种效果,但是如果编译器生成一系列比较而不是常规开关盒的跳转表,我会感到惊讶。

现在到if-else,情况正好相反。尽管switch-case以固定时间执行,而不管分支的数量如何,但if-else针对较小数量的分支进行了优化。在这里,您希望编译器基本上按照编写顺序生成一系列比较。

因此,如果我if-else因为期望大多数调用square()用于01很少为其他值而使用过,那么将其“优化”为表查找实际上可能会使我的代码运行得比我预期的慢,从而违反了使用if替代方法的目的的switch。因此,尽管值得商bat,但我认为GCC所做的是正确的事情,而clang在其优化方面过于激进。

有人在评论中共享了一个链接,其中clang进行了此优化,并为此生成了基于查找表的代码if-else。当我们用clang将案例数量减少到只有两个(默认)时,会发生一些值得注意的事情。它再次为if和switch生成相同的代码,但是这次,这两者都 切换到比较和移动,而不是查找表方法。这意味着,即使选择开关的叮当声也很容易理解,当案例数较少时,“ if”模式会更加理想!

总而言之,比较器if-else和跳转表的顺序switch-case是编译器倾向于遵循的标准模式,而开发人员在编写代码时往往期望这种标准模式。但是,在某些特殊情况下,某些编译器可能会选择中断这种模式,因为他们认为它提供了更好的优化。其他编译器可能仍然选择遵循该模式,即使显然不是最佳选择,也可以信任开发人员知道他想要什么。两者都是有效的方法,各有其优缺点。


2
是的,优化是一把多刃剑:他们写什么,他们想要什么,他们得到什么以及我们为此骂谁。
重复数据删除器

1
“ ...然后将其“优化”到表查找中实际上会使我的代码运行得比我预期的慢...”您能为此提供理由吗?为什么跳转表会比两个可能的条件分支(要根据0和检查输入1)慢?
科迪·格雷

@CodyGray我不得不承认我还没有达到计数周期的水平-我只是直觉,通过指针从内存中加载可能比比较和跳转花费更多的周期,但是我可能是错的。但是,我希望您同意我的看法,即使在这种情况下,至少对于“ 0”,if速度显然更快?现在,这里是一个平台示例,其中使用0和1 if都比使用switch 更快:godbolt.org/z/wcJhvS(请注意,这里还有其他多个优化)
th33lf

1
好吧,无论如何,计数周期在现代的超标量OOO结构上都不起作用。:-)来自内存的负载不会比错误预测的分支慢,所以问题是预测分支的可能性有多大?这个问题适用于条件分支的所有方式,无论是由显式if语句生成还是由编译器自动生成。我不是ARM专家,所以我不太确定您是否宣称switch要比if真实速度快。这将取决于分支预测错误的代价,而这实际上取决于哪个 ARM。
科迪·格雷

0

一个可能的理由是,如果较低的值num更有可能(例如始终为0),则为第一个生成的代码可能会更快。所生成的switch代码花费所有时间相等的时间。

根据此表比较最佳情况。有关表格的说明,请参见此答案

如果num == 0,对于“如果”,您有xor,test,je(带有跳转),重新输入。延迟:1 + 1 +跳跃。但是,xor和test是独立的,因此实际执行速度将比1 + 1周期快。

如果num < 7对于“ switch”,有mov,cmp,ja(无跳转),mov,ret。延迟时间:2 + 1 +无跳跃+ 2。

不导致跳转的跳转指令比导致跳转的指令快。但是,该表未定义跳跃的等待时间,因此我不清楚哪个更好。最后一个可能总是更好,而GCC根本无法对其进行优化。


1
嗯,有趣的理论,但是对于ifs vs switch,您有:xor,test,jmp vs mov,cmp jmp。三个指令,每个指令最后一个跳转。在最好的情况下似乎相等,不是吗?
chacham15

3
“不导致跳转的跳转指令要比导致跳转的指令快。” 重要的是分支预测。
geza
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.