为什么 switch 没有像 c/c++ 中的 if else 链式优化那样进行优化?

cha*_*m15 42 c c++ optimization gcc compiler-optimization

下面的 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

下面生成一个用于返回的数据表:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

为什么gcc无法将顶部的优化为底部的呢?

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

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

th3*_*3lf 31

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

现在来看if-else,情况恰恰相反。虽然switch-case无论分支数量如何,都会以恒定时间执行,if-else针对较少数量的分支进行了优化。在这里,您希望编译器基本上按照您编写的顺序生成一系列比较。

因此,如果我使用它是if-else因为我希望大多数调用square()都是针对0or1而很少针对其他值,那么将其“优化”为表查找实际上可能会导致我的代码运行速度比我预期的慢,从而违背了我if使用的switch. 因此,尽管存在争议,但我认为 GCC 正在做正确的事情,而 clang 在优化方面过于激进。

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

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

  • *“...那么将其‘优化’为表查找实际上会导致我的代码运行速度比我预期的慢...”* 您能为此提供理由吗?为什么跳转表会比*两个*可能的条件分支(检查输入“0”和“1”)慢? (3认同)
  • 是的,优化是一把多刃剑:他们写什么,他们想要什么,他们得到什么,以及我们为此诅咒的人。 (2认同)
  • 无论如何,计算周期在现代超标量 OOO 架构上不起作用。:-) 从内存中加载不会比错误预测的分支慢,所以问题是分支被预测的可能性有多大?这个问题适用于所有形式的条件分支,无论是由显式的“if”语句生成还是由编译器自动生成。我不是 ARM 专家,所以我不确定您关于“switch”比“if”更快的说法是否属实。这将取决于错误预测分支的惩罚,而这实际上取决于*哪个* ARM。 (2认同)

vll*_*vll 1

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

根据此表比较最佳情况。有关该表的解释,请参阅此答案。

如果num == 0,对于“如果”,你有 xor、test、je(带有跳转)、ret。延迟:1+1+跳跃。然而,xor 和 test 是独立的,因此实际执行速度会比 1 + 1 个周期更快。

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

不导致跳转的跳转指令比导致跳转的跳转指令更快。但是,该表没有定义跳转的延迟,因此我不清楚哪一个更好。有可能最后一个总是更好,而 GCC 根本无法优化它。

  • “不导致跳转的跳转指令比导致跳转的跳转指令更快。”。重要的是分支预测。 (3认同)