为什么开关的优化方式与c/c++中的链式if else不一样?

下面这个square的实现产生了一系列的cmp/je语句,就像我所期望的链式if语句一样。

int square(int num) {
    如果(num == 0){
        返回0。
    } else if (num == 1){
        返回1。
    } else if (num == 2){
        返回4。
    } else if (num == 3){
        返回9。
    } else if (num == 4){
        返回16。
    } else if (num == 5){
        返回25。
    } else if (num == 6){
        返回36。
    } else if (num == 7){
        返回49。
    } else {
        返回num * num。
    }
}

而下面产生一个返回的数据表。

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

为什么gcc无法将上面的优化成下面的?

反汇编供参考:https://godbolt.org/z/UP_igi

EDIT: 有趣的是,MSVC在switch的情况下生成了一个跳转表而不是数据表。而令人惊讶的是,clang将它们优化成了相同的结果。

对该问题的评论 (11)

为 "switch-case "生成的代码通常使用一个跳转表。在这种情况下,通过查找表直接返回似乎是一种优化,利用了这里的每个案例都涉及到返回的事实。虽然标准没有对此做出保证,但如果编译器为传统的开关案例生成一系列比较而不是跳转表,我会感到惊讶。

现在到了 "if-else",情况正好相反。虽然switch-case在恒定的时间内执行,不管有多少分支,if-else是针对较少的分支数量进行优化的。在这里,你会期望编译器基本上按照你写的顺序生成一系列的比较。

因此,如果我使用 "if-else "是因为我希望大多数对 "square() "的调用是针对 "0 "或 "1",而很少针对其他值,那么将其优化为表查询实际上会导致我的代码运行得比我预期的慢,从而违背了我使用 "if "而不是 "switch "的目的。因此,尽管有争议,我觉得GCC做的是正确的事情,而clang在优化方面过于激进。

有人在评论中分享了一个链接,clang做了这种优化,并为if-else生成了基于查找表的代码。当我们把clang的案例数量减少到只有两个(和一个缺省)时,一些值得注意的事情发生了。它再次为if和switch生成了相同的代码,但这一次。 切换到比较和移动,而不是用查找表的方法,对这两种情况都是如此!。

总之,"if-else "的比较序列和 "switch-case "的跳转表是编译器倾向于遵循的标准模式,也是开发者在编写代码时倾向于期待的模式。然而,在某些特殊情况下,一些编译器可能会选择打破这种模式,因为他们认为这样可以提供更好的优化。其他编译器可能只是选择坚持这个模式,即使是明显的次优,相信开发者知道他想要什么。这两种方法都是有效的,都有各自的优势和劣势。

评论(4)

一个可能的理由是,如果 "num "的低值更有可能,例如总是0,那么第一个值的生成代码可能会更快。生成的switch的代码对所有的值都需要相同的时间。

根据此表,比较最佳情况。该表的解释见本答案

如果num == 0,对于"if" 你有xor, test, je (with jump), ret。延迟:1+1+跳转。然而,xor和test是独立的,所以实际执行速度会比1+1周期快。

如果num < 7,对于"switch" 你有mov, cmp, ja (不带跳转), mov, ret.延迟:2+1+无跳转+2。

没有跳转的跳转指令比有跳转的指令要快。然而,该表并没有定义跳转的延迟,所以我不清楚哪一个更好。有可能最后一条总是更好,而GCC根本无法优化它。

评论(2)