【问题标题】:Why is a switch not optimized the same way as chained if else in c/c++?为什么在 c/c++ 中,开关的优化方式与链式 if else 的方式不同?
【发布时间】:2020-05-23 09:16:07
【问题描述】:

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无法将top的优化成bottom的?

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

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

【问题讨论】:

  • “未定义的行为”是什么意思?只要可观察的行为相同,编译器就可以生成它想要的任何汇编/机器代码
  • @user207421 忽略returns;案例没有breaks,因此开关也有特定的执行顺序。 if/else 链在每个分支中都有返回,这种情况下的语义是等价的。优化不是impossible。作为反例icc 没有优化任何功能。
  • 也许是最简单的答案...... gcc 只是无法看到这个结构并优化它(还)。
  • 我同意@user1810087。您只是找到了编译器细化过程的当前边界。当前未被识别为可优化的子子案例(某些编译器)。事实上,并非所有 else-if 链都可以这样优化,只有 SAME 变量针对常量值进行测试的子集。
  • if-else 执行顺序不同,从上到下。尽管如此,用 if 语句替换代码并没有改进机器代码。另一方面,switch 没有预定义的执行顺序,本质上只是一个美化的 goto 跳转表。话虽如此,编译器可以对此处的可观察行为进行推理,因此 if-else 版本的糟糕优化令人非常失望。

标签: c++ c gcc optimization compiler-optimization


【解决方案1】:

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

现在来到if-else,情况正好相反。 switch-case 以恒定时间执行,而与分支数量无关,if-else 针对较少数量的分支进行了优化。在这里,您会期望编译器基本上按照您编写它们的顺序生成一系列比较。

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

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

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

【讨论】:

  • 是的,优化是一把多刃剑:他们写什么,他们想要什么,他们得到什么,我们为此诅咒谁。
  • "...然后将其“优化”为表查找实际上会导致我的代码运行速度比我预期的要慢..." 你能提供一个理由吗?这?为什么跳转表会比 两个 可能的条件分支(根据01 检查输入)慢?
  • @CodyGray 我不得不承认我没有达到计数周期的水平 - 我只是直觉地认为通过指针从内存中加载可能需要比比较更多的周期,并且跳,但我可能是错的。但是,我希望您同意我的观点,即使在这种情况下,至少对于 '0',if 显然更快?现在,这是一个平台示例,其中使用 if 时 0 和 1 都会比使用 switch 时更快:godbolt.org/z/wcJhvS(请注意,这里还有多种其他优化)
  • 嗯,计数周期无论如何都不适用于现代超标量 OOO 架构。 :-) 从内存中加载不会比预测错误的分支慢,所以问题是预测分支的可能性有多大?该问题适用于各种条件分支,无论是由显式 if 语句生成还是由编译器自动生成。我不是 ARM 专家,所以我不确定您关于 switchif 快的说法是否属实。这将取决于对错误预测分支的惩罚,而这实际上取决于 which ARM。
【解决方案2】:

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

比较最佳案例,根据this table。表格说明见this answer

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

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

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

【讨论】:

  • 嗯,有趣的理论,但是对于 ifs vs switch 你有:xor,test,jmp vs mov,cmp jmp。三个指令,最后一个是跳转。在最好的情况下似乎相等,不是吗?
  • “不导致跳转的跳转指令比导致跳转的跳转指令快。”。重要的是分支预测。
猜你喜欢
  • 2019-04-11
  • 2021-03-03
  • 1970-01-01
  • 2015-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-30
  • 2015-10-28
相关资源
最近更新 更多