关于C++:switch 与IF语句的优势

Advantage of switch over if-else statement

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

switch声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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声明:

1
2
3
4
5
6
7
8
9
10
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();
}


使用开关。

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

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


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

1
2
if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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


开关更快。

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

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

1
2
3
4
5
6
7
8
9
10
11
// 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!)和其他人将在同一编译单元中声明和定义常量变量。例如:

1
2
3
4
5
6
7
8
9
10
11
// 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 ;
    }
}

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

(不是说一个开关不能写得像地狱一样混乱……我现在看到的大多数开关都属于这种"混乱"类别…但这是另一个故事…)

Edit 2008-09-21:

bk1e added the following comment:"Defining constants as enums in a header file is another way to handle this".

Of course it is.

The point of an extern type was to decouple the value from the source. Defining this value as a macro, as a simple const int declaration, or even as an enum has the side-effect of inlining the value. Thus, should the define, the enum value, or the const int value change, a recompilation would be needed. The extern declaration means the there is no need to recompile in case of value change, but in the other hand, makes it impossible to use switch. The conclusion being Using switch will increase coupling between the switch code and the variables used as cases. When it is Ok, then use switch. When it isn't, then, no surprise.

.

Edit 2013-01-15:

Vlad Lazarenko commented on my answer, giving a link to his in-depth study of the assembly code generated by a switch. Very enlightning: http://741mhz.com/switch/


编译器会对它进行优化,因为它是最可读的。


开关,如果只是为了可读性。巨人如果声明是难以维持和难以阅读在我看来。

错误1://intential fall through

(错误_01==numeror)||

后者更容易出错,并且比前者需要更多的类型和格式。


使用开关,它是为程序员所期望的。

我会把多余的箱子标签放进去——只是为了让人们感觉舒服,我试着记住什么时候/什么时候把它们放在外面的规则。您不希望下一个正在开发它的程序员必须对语言细节做任何不必要的思考(可能是几个月后的事情!)


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


编译器非常擅长优化switch。最近的GCC还擅长优化if中的一系列条件。

我在Godbolt上做了一些测试。

case值被紧密地分组在一起时,gcc、clang和icc都足够聪明,可以使用位图检查某个值是否是其中一个特殊值。

例如,GCC 5.2-O3将switch编译为(和if非常相似):

1
2
3
4
5
6
7
8
9
10
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编译为位图,但使用mov/shift将1U<编译为位图。它将if版本编译为一系列分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

注意它是如何从errNumber中减去1的(使用lea将该操作与移动结合起来)。这使得它可以将位图调整为32位立即数,从而避免了64位立即数movabsq,后者需要更多的指令字节。

较短的(机器代码)序列为:

1
2
3
4
5
6
7
8
    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上就不能正常工作。

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

从Agner Fog的指令表来看,可变计数移位指令比最近Intel上的bt慢(2个Uops而不是1个,移位并不能完成所需的所有其他操作)。


在我看来,这是一个完美的例子,说明什么是开关掉下来的原因。


我同意交换解决方案的复杂性,但在我看来,你是在劫持交换。开关的用途是根据值进行不同的处理。如果你必须用伪代码来解释你的算法,你会使用if,因为,从语义上来说,这就是它的意义:如果无论什么错误都会这样做…所以,除非有一天您打算将代码更改为针对每个错误的特定代码,否则我将使用if。


我不确定最佳实践,但我会使用开关-然后通过"默认"陷阱故意摔倒。


如果您的案例在将来很可能保持分组(如果多个案例对应一个结果),那么这个切换可能会更容易阅读和维护。


他们工作得同样好。现代编译器的性能基本相同。

我更喜欢if语句而不是case语句,因为它们更可读、更灵活——您可以添加其他不基于数字相等的条件,如"max


首选开关。查看交换机的案例列表比读取长if条件更容易,更容易确定它在做什么。

if状态下的重复对眼睛很难。假设其中一个==!=写的,你会注意到吗?或者,如果"numeror"的一个实例被写为"nmuerror",这是编译时发生的?

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中的灵感:

1
2
3
4
5
6
7
8
9
10
11
12
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()


1
while (true) != while (loop)

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


在编译程序时,我不知道有什么不同。但是对于程序本身和尽可能简单地保存代码,我个人认为这取决于您想要做什么。如果不是这样的话,如果其他陈述有它们的优势,我认为是:

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

(例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
`int a;
 cout<<"enter value:
"
;
 cin>>a;

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

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

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

   }else{

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

然而,如果不是这样的话,if else语句会很快变得复杂和混乱(尽管你尽了最大的努力)。switch语句往往更清晰、更清晰、更易于阅读;但只能用于针对特定值进行测试(例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
`int a;
 cout<<"enter value:
"
;
 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<<"
"
;
        break;
    //other case statements
    default:
        cout<<"a is not between the range or is not a good value
"

        break;

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


我不是那个告诉你速度和内存使用情况的人,但是看一个开关站是一个非常容易理解的地方,然后是一个大型的if语句(尤其是2-3个月)。


请使用开关。if语句将花费与条件数量成比例的时间。


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


我会选择国际单项体育联合会的声明是为了明确和约定,尽管我相信有些人会不同意。毕竟,你想做点什么,有些情况是真的!一个动作切换似乎有点…不必要的。


我知道它很古老,但是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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);
}
}

改变循环计数会改变很多:

如果/否则:5毫秒开关:1ms最大循环数:100000

如果/否则:5毫秒开关:3ms最大循环数:1000000

如果/否则:5毫秒开关:14MS最大循环数:10000000

如果/否则:5毫秒开关:149MS最大循环数:100000000

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


我会说使用开关。这样,您只需要实现不同的结果。您的十个相同的案例可以使用默认值。如果您只需要一次更改就可以显式地实现更改,那么不需要编辑默认值。从交换机添加或删除案例也比编辑if和elseif容易得多。

1
2
3
4
switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

甚至可以根据一系列可能性来测试您的条件(在本例中是numeror),或者一个数组,这样您的开关就不会被使用,除非有明确的结果。