切换if-else语句的优点

Zin*_*ng- 162 c++ optimization if-statement switch-statement

使用switch语句与使用if30个unsigned枚举的语句的最佳实践是什么,其中大约10个具有预期的操作(目前是相同的操作).需要考虑性能和空间,但并不重要.我已经抽象了代码片段,所以不要因为命名惯例而讨厌我.

switch 声明:

// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing

switch (numError)
{  
  case ERROR_01 :  // intentional fall-through
  case ERROR_07 :  // intentional fall-through
  case ERROR_0A :  // intentional fall-through
  case ERROR_10 :  // intentional fall-through
  case ERROR_15 :  // intentional fall-through
  case ERROR_16 :  // intentional fall-through
  case ERROR_20 :
  {
     fire_special_event();
  }
  break;

  default:
  {
    // error codes that require no additional action
  }
  break;       
}
Run Code Online (Sandbox Code Playgroud)

if 声明:

if ((ERROR_01 == numError)  ||
    (ERROR_07 == numError)  ||
    (ERROR_0A == numError)  || 
    (ERROR_10 == numError)  ||
    (ERROR_15 == numError)  ||
    (ERROR_16 == numError)  ||
    (ERROR_20 == numError))
{
  fire_special_event();
}
Run Code Online (Sandbox Code Playgroud)

Nil*_*nck 152

使用开关.

在最坏的情况下,编译器将生成与if-else链相同的代码,因此您不会丢失任何内容.如果有疑问,请将最常见的案例放在switch语句中.

在最好的情况下,优化器可能会找到更好的方法来生成代码.编译器所做的常见事情是构建二进制决策树(在一般情况下保存比较和跳转)或者只是构建一个跳转表(根本没有比较).

  • jakoben:可以这样做,但仅适用于类似开关的if/else链.实际上,这些都不会发生,因为程序员使用switch.我深入研究编译器技术并相信我:找到这样"无用"的构造需要花费很多时间.对于编译器人来说,这样的优化确实很有意义. (5认同)
  • @NilsPipenbrinck易于在模板元编程中构建伪递归的`if`-`else`链,以及生成`switch``case`链的难度,该映射可能变得更加重要.(是的,古老的评论,但网络是永远的,或至少到下周二) (5认同)
  • 从技术上讲,仍然会进行一次比较,以确保枚举的值位于跳转表内。 (3认同)
  • 请注意,理论上可以将一系列ifs分析出来与编译器的切换相同,但为什么要抓住机会呢?通过使用开关,您可以准确地传达您想要的内容,这可以使代码生成更容易. (3认同)
  • @Yakk-AdamNevraumont:确实,在这种情况下,现代编译器(例如 GCC5 及更高版本,clang 早在 3.0 )就能够转换 `(ERROR_01 == numError) || (ERROR_07 == numError) || ...` 进入与 switch 相同的 asm,检查立即位图。(https://gcc.godbolt.org/z/M3G7svaPa)。请参阅[我的答案](/sf/ask/6859121/​​56125#32356125)关于这个问题,在你发表评论后几年,但是我现在才偶然注意到你的评论。:P (2认同)

Mar*_*som 42

对于您在示例中提供的特殊情况,最清晰的代码可能是:

if (RequiresSpecialEvent(numError))
    fire_special_event();
Run Code Online (Sandbox Code Playgroud)

显然,这只会将问题移到代码的不同区域,但现在您有机会重用此测试.您还有更多选择如何解决它.你可以使用std :: set,例如:

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}
Run Code Online (Sandbox Code Playgroud)

我并不是说这是RequiresSpecialEvent的最佳实现,只是它是一个选项.您仍然可以使用开关或if-else链,或查找表,或对值进行一些位操作,无论如何.您的决策过程越模糊,您在孤立函数中获得的价值就越大.

  • 这是真的.可读性比switch和if语句都要好得多.我自己实际上会回答这样的问题,但是你打败了我.:-) (4认同)

pae*_*bal 20

该开关更快.

只需在循环中尝试if/else-ing 30个不同的值,并使用开关将其与相同的代码进行比较,以查看开关的速度.

现在,交换机有一个真正的问题:交换机必须在编译时知道每种情况下的值.这意味着以下代码:

// WON'T COMPILE
extern const int MY_VALUE ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}
Run Code Online (Sandbox Code Playgroud)

不会编译.

然后大多数人将使用定义(Aargh!),其他人将在同一编译单元中声明和定义常量变量.例如:

// WILL COMPILE
const int MY_VALUE = 25 ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,最终,开发人员必须在"速度+清晰度"与"代码耦合"之间进行选择.

(并不是说开关不能被写成令人困惑的地狱......我目前看到的大多数开关都是这个"令人困惑"的类别"......但这是另一个故事......)

编辑2008-09-21:

bk1e添加了以下注释:" 将常量定义为头文件中的枚举是另一种处理此问题的方法".

当然如此.

extern类型的要点是将值与源分离.将此值定义为宏,作为简单的const int声明,或者甚至作为枚举具有内联值的副作用.因此,如果define,enum值或const int值发生变化,则需要重新编译.extern声明意味着在价值变化的情况下不需要重新编译,但另一方面,使得无法使用开关.结论是使用开关将增加开关代码和用作情况的变量之间的耦合.如果是,则使用开关.如果不是,那就不足为奇了.

.

编辑2013-01-15:

Vlad Lazarenko评论了我的答案,给出了他对交换机生成的汇编代码的深入研究的链接.非常enlightning:http://741mhz.com/switch/

  • 切换[不__always__更快](http://lazarenko.me/2013/01/13/switch-statement-machine-code/). (5认同)
  • @AhmedHussein user404725 的链接已失效。值得庆幸的是,我在 WayBack Machine 中找到了它:http://web.archive.org/web/20131111091431/http://lazarenko.me/2013/01/13/switch-statement-machine-code/。事实上,WayBack Machine 可以说是一件相当幸运的事情。 (2认同)

Ale*_*nks 18

编译器无论如何都会对它进行优化 - 选择最易读的开关.

  • 有可能编译器不会触及if-then-else.事实上,`gcc`肯定不会那样做(这是有充分理由的).Clang将两种情况都优化为二进制搜索.例如,请参见[this](http://lazarenko.me/2013/01/13/switch-statement-machine-code/). (3认同)

Kai*_*zke 7

很抱歉不同意当前接受的答案。今年是 2021 年。现代编译器及其优化器不应再区分switch和 等效if链。如果他们仍然这样做,并且为任一变体创建了优化不佳的代码,请写信给编译器供应商(或在此处公开,这具有更高的受尊重的变化),但不要让微优化影响您的编码风格。

所以,如果您使用:

switch (numError) { case ERROR_A: case ERROR_B: ... }
Run Code Online (Sandbox Code Playgroud)

或者:

if(numError == ERROR_A || numError == ERROR_B || ...) { ... }
Run Code Online (Sandbox Code Playgroud)

或者:

template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
    return std::find(cont.begin(), cont.end(), el) != cont.end();
}

constexpr std::array errList = { ERROR_A, ERROR_B, ... };
if(has(errList, rnd)) { ... }
Run Code Online (Sandbox Code Playgroud)

不应该对执行速度产生影响。但根据您正在从事的项目,它们可能会在编码清晰度和代码可维护性方面产生很大差异。例如,如果您必须在代码的许多地方检查某个错误列表,则模板化has()可能更容易维护,因为 errList 只需要在一个地方更新。

谈到当前的编译器,我使用clang++ -O3 -std=c++1z(版本 10 和 11)和g++ -O3 -std=c++1z. 两个 clang 版本都提供了类似的编译代码和执行时间。所以从现在开始我只讨论版本 11。最值得注意的是,functionA()(使用if) 和functionB()(使用switch) 使用 ! 产生完全相同的汇编器输出clang。并functionC()使用跳转表,尽管许多其他海报认为跳转表是switch. 然而,尽管许多人认为跳转表是最佳的,但这实际上是 上最慢的解决方案clang:需要比或functionC()多大约 20% 的执行时间。functionA()functionB()

手工优化的版本functionH()是迄今为止最快的clang。它甚至部分展开循环,在每个循环上进行两次迭代。

实际上,clang计算了位域,该位域在functionH()、 和 中functionA()明确提供functionB()functionA()然而,它在和中使用了条件分支functionB(),这使得速度变慢,因为分支预测经常失败,而它在 中使用了效率更高的adc(“带进位相加”)functionH()。虽然它未能在其他变体中应用这种明显的优化,但我不知道。

生成的代码g++看起来比clang- 复杂得多,但实际上运行速度更快一点functionA(),并且运行速度快得多functionC()。在非手工优化的函数中,functionC()是最快的g++,并且比 上的任何函数都快clang。相反,使用 withfunctionH()编译时需要两倍的执行时间,主要是因为不进行循环展开。g++clangg++

以下是详细结果:

clang:
functionA: 109877 3627
functionB: 109877 3626
functionC: 109877 4192
functionH: 109877 524

g++:
functionA: 109877 3337
functionB: 109877 4668
functionC: 109877 2890
functionH: 109877 982
Run Code Online (Sandbox Code Playgroud)

如果整个代码中的常量32更改为,性能会发生巨大变化:63

clang:
functionA: 106943 1435
functionB: 106943 1436
functionC: 106943 4191
functionH: 106943 524

g++:
functionA: 106943 1265
functionB: 106943 4481
functionC: 106943 2804
functionH: 106943 1038
Run Code Online (Sandbox Code Playgroud)

加速的原因是,如果最高测试值是 63,编译器会删除一些不必要的边界检查,因为rnd无论如何, 的值都绑定到 63。functionA()请注意,删除绑定检查后,使用 simple if()on进行非优化的g++执行速度几乎与手动优化的一样快functionH(),并且它还产生相当相似的汇编器输出。

结论是什么?如果您大量手动优化和测试编译器,您将获得最快的解决方案。任何假设是否switch更好if,都是无效的 - 它们在 上是相同的clang。用于检查array值的易于编码的解决方案实际上是最快的情况g++(如果省略手动优化和偶然匹配列表的最后一个值)。

未来的编译器版本将会越来越好地优化你的代码,越来越接近你手上的优化。因此,不要在上面浪费时间,除非周期对您的情况确实至关重要。

这里是测试代码:

#include <iostream>
#include <chrono>
#include <limits>
#include <array>
#include <algorithm>

unsigned long long functionA() {
    unsigned long long cnt = 0;

    for(unsigned long long i = 0; i < 1000000; i++) {
        unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
        if(rnd == 1 || rnd == 7 || rnd == 10 || rnd == 16 ||
           rnd == 21 || rnd == 22 || rnd == 63)
        {
            cnt += 1;
        }
    }

    return cnt;
}

unsigned long long functionB() {
    unsigned long long cnt = 0;

    for(unsigned long long i = 0; i < 1000000; i++) {
        unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
        switch(rnd) {
        case 1:
        case 7:
        case 10:
        case 16:
        case 21:
        case 22:
        case 63:
            cnt++;
            break;
        }
    }

    return cnt;
}

template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
    return std::find(cont.begin(), cont.end(), el) != cont.end();
}

unsigned long long functionC() {
    unsigned long long cnt = 0;
    constexpr std::array errList { 1, 7, 10, 16, 21, 22, 63 };

    for(unsigned long long i = 0; i < 1000000; i++) {
        unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
        cnt += has(errList, rnd);
    }

    return cnt;
}

// Hand optimized version (manually created bitfield):
unsigned long long functionH() {
    unsigned long long cnt = 0;

    const unsigned long long bitfield =
        (1ULL << 1) +
        (1ULL << 7) +
        (1ULL << 10) +
        (1ULL << 16) +
        (1ULL << 21) +
        (1ULL << 22) +
        (1ULL << 63);

    for(unsigned long long i = 0; i < 1000000; i++) {
        unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
        if(bitfield & (1ULL << rnd)) {
            cnt += 1;
        }
    }

    return cnt;
}

void timeit(unsigned long long (*function)(), const char* message)
{
    unsigned long long mintime = std::numeric_limits<unsigned long long>::max();
    unsigned long long fres = 0;

    for(int i = 0; i < 100; i++) {
        auto t1 = std::chrono::high_resolution_clock::now();
        fres = function();
        auto t2 = std::chrono::high_resolution_clock::now();

        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
        if(duration < mintime) {
            mintime = duration;
        }
    }

    std::cout << message << fres << " " << mintime << std::endl;
}


int main(int argc, char* argv[]) {
    timeit(functionA, "functionA: ");
    timeit(functionB, "functionB: ");
    timeit(functionC, "functionC: ");
    timeit(functionH, "functionH: ");
    timeit(functionA, "functionA: ");
    timeit(functionB, "functionB: ");
    timeit(functionC, "functionC: ");
    timeit(functionH, "functionH: ");
    timeit(functionA, "functionA: ");
    timeit(functionB, "functionB: ");
    timeit(functionC, "functionC: ");
    timeit(functionH, "functionH: ");

    return 0;
}
Run Code Online (Sandbox Code Playgroud)


scu*_*bbl 6

Switch,如果只是为了可读性.在我看来,巨大的if语句难以维护且难以阅读.

ERROR_01://故意落空

要么

(ERROR_01 == numError)||

后者更容易出错,需要比第一次更多的打字和格式化.


Bdo*_*ror 5

可读性代码.如果您想知道哪些性能更​​好,请使用分析器,因为优化和编译器各不相同,性能问题很少出现在人们认为的情况下.


Mar*_*ett 5

使用开关,它是它的用途和程序员的期望.

我会把冗余的案例标签放进去 - 只是为了让人们感觉舒服,我试图记住什么时候/什么规则让他们离开.
你不希望下一个编程人员不得不对语言细节做任何不必要的思考(可能是你几个月后!)


Pet*_*des 5

编译器非常擅长优化switch. 最近的 gcc 也擅长优化if.

我在Godbolt上做了一些测试用例。

case值组合在一起时,gcc、clang 和 icc 都足够聪明,可以使用位图来检查值是否是特殊值之一。

例如 gcc 5.2 -O3 编译switch到(和if非常相似的东西):

errhandler_switch(errtype):  # gcc 5.2 -O3
    cmpl    $32, %edi
    ja  .L5
    movabsq $4301325442, %rax   # highest set bit is bit 32 (the 33rd bit)
    btq %rdi, %rax
    jc  .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()
Run Code Online (Sandbox Code Playgroud)

请注意,位图是直接数据,因此没有潜在的数据缓存未命中访问它或跳转表。

gcc 4.9.2 -O3 将 编译switch为位图,但1U<<errNumber使用 mov/shift 执行。它将if版本编译为一系列分支。

errhandler_switch(errtype):  # gcc 4.9.2 -O3
    leal    -1(%rdi), %ecx
    cmpl    $31, %ecx    # cmpl $32, %edi  wouldn't have to wait an extra cycle for lea's output.
              # However, register read ports are limited on pre-SnB Intel
    ja  .L5
    movl    $1, %eax
    salq    %cl, %rax   # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
    testl   $2150662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()
Run Code Online (Sandbox Code Playgroud)

请注意它如何从errNumber(withlea将该操作与移动相结合) 中减去 1 。这使它可以将位图装入 32 位立即数,避免movabsq占用更多指令字节的 64 位立即数。

更短的(机器代码)序列是:

    cmpl    $32, %edi
    ja  .L5
    mov     $2150662721, %eax
    dec     %edi   # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
    bt     %edi, %eax
    jc  fire_special_event
.L5:
    ret
Run Code Online (Sandbox Code Playgroud)

(使用失败jc fire_special_event无处不在,并且是编译器错误。)

rep ret为了旧的 AMD K8 和 K10(Bulldozer 之前)的利益,在分支目标和条件分支之后使用:“rep ret”是什么意思?. 没有它,分支预测在那些过时的 CPU 上不能很好地工作。

bt(位测试)与寄存器 arg 很快。它结合了按errNumber位左移 1和执行 a 的工作test,但仍然是 1 个周期的延迟,并且只有一个 Intel uop。由于其过于 CISC 语义,内存 arg 很慢:对于“位字符串”的内存操作数,要测试的字节的地址是基于另一个 arg(除以 8)计算的,并且不是'不限于内存操作数指向的 1、2、4 或 8 字节块。

Agner Fog 的指令表中,可变计数移位指令比bt最近的 Intel 上的要慢(2 uops 而不是 1,并且 shift 不执行其他所有需要的操作)。