178

switch使用语句与使用if语句进行 30 个枚举的最佳实践是什么,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;       
}

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();
}
4

23 回答 23

174

使用开关。

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

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

于 2008-09-18T23:32:46.613 回答
45

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}

我并不是说这是 RequiresSpecialEvent 的最佳实现,只是它是一个选项。您仍然可以使用 switch 或 if-else 链,或查找表,或对值进行一些位操作,等等。你的决策过程变得越模糊,你就会从一个独立的函数中获得更多的价值。

于 2008-09-24T20:01:19.853 回答
25

切换速度更快。

只需在一个循环中尝试 if/else-ing 30 个不同的值,然后使用 switch 将其与相同的代码进行比较,看看 switch 的速度有多快。

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

// 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 ;
    }
}

不会编译。

然后大多数人将使用定义(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 ;
    }
}

所以,最终,开发者必须在“速度+清晰度”与“代码耦合”之间做出选择。

(并不是说开关不能写得令人困惑……我目前看到的大多数开关都属于这种“令人困惑”的类别”……但这是另一个故事……)

编辑 2008-09-21:

bk1e添加了以下注释:“在头文件中将常量定义为枚举是另一种处理方式”。

当然是的。

extern 类型的意义在于将值与源解耦。将此值定义为宏、简单的 const int 声明,甚至是枚举,都会产生内联值的副作用。因此,如果定义、枚举值或 const int 值发生变化,则需要重新编译。extern 声明意味着在值变化的情况下不需要重新编译,但另一方面,使得无法使用 switch。结论是Using switch 将增加 switch 代码和用作 case 的变量之间的耦合。没问题的时候再用switch。如果不是,那就不足为奇了。

.

编辑 2013-01-15:

Vlad Lazarenko对我的回答发表了评论,并提供了一个链接,指向他对开关生成的汇编代码的深入研究。非常有启发性:http: //lazarenko.me/switch/

于 2008-09-19T16:12:55.857 回答
18

编译器无论如何都会对其进行优化 - 选择开关,因为它是最易读的。

于 2008-09-18T23:30:31.953 回答
6

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

ERROR_01 : // 故意掉线

或者

(ERROR_01 == numError) ||

后者比第一个更容易出错,并且需要更多的打字和格式化。

于 2008-09-18T23:30:52.717 回答
6

代码可读性。如果您想知道什么性能更好,请使用分析器,因为优化和编译器各不相同,而且性能问题很少出现在人们认为的地方。

于 2008-09-24T19:26:06.647 回答
6

编译器真的很擅长优化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()

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

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()

请注意它是如何减去 1 的errNumberlea将该操作与移动相结合)。这使它可以将位图适合 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

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

rep ret为了旧的 AMD K8 和 K10(推土机之前)的利益,用于分支目标和以下条件分支:“rep ret”是什么意思?. 没有它,分支预测在那些过时的 CPU 上就无法正常工作。

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

Agner Fog 的指令表中,可变计数移位指令比bt最近的 Intel 上的要慢(2 uop 而不是 1,并且 shift 并不能做其他所有需要的事情)。

于 2015-09-02T14:37:11.837 回答
5

使用 switch,它是它的用途和程序员所期望的。

不过,我会将多余的案例标签放入其中——只是为了让人们感到舒服,我试图记住何时/什么规则将它们排除在外。
你不希望下一个开发它的程序员不得不对语言细节做任何不必要的思考(几个月后可能就是你了!)

于 2008-09-24T20:54:31.863 回答
2

IMO 这是一个完美的例子,说明了切换失败的目的。

于 2008-09-18T23:31:11.797 回答
2

它们同样工作得很好。给定现代编译器,性能大致相同。

我更喜欢 if 语句而不是 case 语句,因为它们更易读、更灵活——您可以添加其他不基于数值相等的条件,例如“|| max < min”。但是对于您在此处发布的简单案例,这并不重要,只需做对您来说最易读的事情。

于 2008-09-18T23:33:49.467 回答
1

我不确定最佳实践,但我会使用 switch - 然后通过“默认”捕获故意的失败

于 2008-09-18T23:30:56.523 回答
1

如果您的案例将来可能会保持分组状态(如果多个案例对应一个结果),则转换可能会更易于阅读和维护。

于 2008-09-18T23:31:35.773 回答
1

switch 绝对是首选。查看开关的案例列表并确定它在做什么比阅读长 if 条件更容易。

这种情况下的重复if对眼睛来说很难。假设其中一个==是写的!=;你会注意到吗?或者,如果“numError”的一个实例写成“nmuError”,而恰好编译?

我通常更喜欢使用多态性而不是 switch,但是如果没有更多的上下文细节,很难说。

至于性能,最好的办法是使用分析器来测量应用程序在与您在野外所期望的条件相似的条件下的性能。否则,您可能会在错误的位置和错误的方式进行优化。

于 2008-09-18T23:34:21.587 回答
1

我同意交换机解决方案的紧凑性,但 IMO 你在这里劫持了交换机
开关的目的是根据值进行不同的处理。
如果你必须用伪代码解释你的算法,你会使用 if,因为从语义上讲,这就是它的本质:if whatever_error do this ...
所以除非你打算有一天改变你的代码,为每个错误提供特定的代码,我会使用if

于 2008-09-18T23:47:15.270 回答
1

从美学上讲,我倾向于这种方法。

unsigned int special_events[] = {
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20
 };
 int special_events_length = sizeof (special_events) / sizeof (unsigned int);

 void process_event(unsigned int numError) {
     for (int i = 0; i < special_events_length; i++) {
         if (numError == special_events[i]) {
             fire_special_event();
             break;
          }
     }
  }

让数据更智能一点,这样我们就可以让逻辑更笨一点。

我意识到它看起来很奇怪。这是灵感(来自我在 Python 中的做法):

special_events = [
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20,
    ]
def process_event(numError):
    if numError in special_events:
         fire_special_event()
于 2008-09-24T20:46:11.027 回答
1
while (true) != while (loop)

可能第一个是由编译器优化的,这可以解释为什么第二个循环在增加循环计数时会变慢。

于 2010-09-30T21:18:51.430 回答
1

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

所以,如果你使用:

switch (numError) { case ERROR_A: case ERROR_B: ... }

或者:

if(numError == ERROR_A || numError == ERROR_B || ...) { ... }

或者:

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)) { ... }

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

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

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

实际上,clang计算了在functionH()functionA()和中明确提供的位域functionB()。但是,它在 and 中使用了条件分支functionA()functionB()这使得这些分支变得很慢,因为分支预测经常失败,而它adcfunctionH(). 虽然它未能在其他变体中应用这种明显的优化,但我不知道。

生成的代码g++看起来比 - 复杂得多,clang但实际上运行速度要functionA()快一些,对于functionC(). 在非手动优化的函数中,functionC()是最快的g++,比clang. 相反,functionH()编译时需要两倍的执行时间g++而不是 with clang,主要是因为g++不进行循环展开。

以下是详细结果:

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

如果常量在整个代码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

加速的原因是,如果测试的最高值是 63,编译器会删除一些不必要的绑定检查,因为rnd无论如何, 的值都绑定到 63。请注意,删除绑定检查后,未优化的functionA()using simple if()ong++执行速度几乎与 hand-optimized 一样快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;
}
于 2021-03-01T20:52:45.187 回答
0

为了清晰和约定,我会选择 if 语句,尽管我相信有些人会不同意。毕竟,你想做某事,if某些条件是真的!用一个动作切换似乎有点……不必要。

于 2008-09-18T23:31:30.330 回答
0

我不是告诉您速度和内存使用情况的人,但是查看 switch 语句比大型 if 语句更容易理解(尤其是 2-3 个月后)

于 2008-09-18T23:31:48.003 回答
0

我会说使用SWITCH。这样你只需要实现不同的结果。您的十个相同案例可以使用默认值。如果您只需要进行一项更改就是显式实施更改,则无需编辑默认值。从 SWITCH 添加或删除案例也比编辑 IF 和 ELSEIF 容易得多。

switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

甚至可以根据可能性列表(在这种情况下为 numerror)测试您的条件(在这种情况下为 numerror),可能是一个数组,因此您的 SWITCH 甚至不会被使用,除非肯定会有结果。

于 2008-09-18T23:37:34.607 回答
0

看到你只有 30 个错误代码,编写你自己的跳转表,然后你自己做出所有优化选择(跳转总是最快的),而不是希望编译器会做正确的事情。它还使代码非常小(除了跳转表的静态声明)。它还有一个附带的好处,即使用调试器,您可以根据需要在运行时修改行为,只需直接戳表数据即可。

于 2008-09-19T13:18:15.497 回答
0

我知道它很旧,但是

public class SwitchTest {
static final int max = 100000;

public static void main(String[] args) {

int counter1 = 0;
long start1 = 0l;
long total1 = 0l;

int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;

start1 = System.currentTimeMillis();
while (true) {
  if (counter1 == max) {
    break;
  } else {
    counter1++;
  }
}
total1 = System.currentTimeMillis() - start1;

start2 = System.currentTimeMillis();
while (loop) {
  switch (counter2) {
    case max:
      loop = false;
      break;
    default:
      counter2++;
  }
}
total2 = System.currentTimeMillis() - start2;

System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);

System.exit(0);
}
}

改变循环计数变化很大:

While if/else: 5ms Switch: 1ms Max Loops: 100000

While if/else: 5ms Switch: 3ms Max Loops: 1000000

While if/else: 5ms Switch: 14ms Max Loops: 10000000

While if/else: 5ms Switch: 149ms Max Loops: 100000000

(如果需要,添加更多语句)

于 2009-11-23T13:50:48.653 回答
0

到编译程序的时候,不知道有没有什么区别。但至于程序本身以及代码尽量简单,我个人认为还是要看你想做什么。if else if else 语句有其优点,我认为是:

允许您针对特定范围测试变量,您可以使用函数(标准库或个人)作为条件。

(例子:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 if( a > 0 && a < 5)
   {
     cout<<"a is between 0, 5\n";

   }else if(a > 5 && a < 10)

     cout<<"a is between 5,10\n";

   }else{

       "a is not an integer, or is not in range 0,10\n";

但是,If else if else 语句可能会很快变得复杂和混乱(尽管您已尽了最大努力)。Switch 语句往往更清晰、更简洁、更易于阅读;但只能用于针对特定值进行测试(例如:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 switch(a)
 {
    case 0:
    case 1:
    case 2: 
    case 3:
    case 4:
    case 5:
        cout<<"a is between 0,5 and equals: "<<a<<"\n";
        break;
    //other case statements
    default:
        cout<<"a is not between the range or is not a good value\n"
        break;

我更喜欢 if - else if - else 语句,但这完全取决于你。如果您想使用函数作为条件,或者您想针对范围、数组或向量进行测试和/或您不介意处理复杂的嵌套,我建议您使用 If else if else 块。如果您想针对单个值进行测试,或者您想要一个干净且易于阅读的块,我建议您使用 switch() 案例块。

于 2017-02-23T03:39:48.260 回答