切换if-else语句的优势


168

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

26
这被编辑为“主观”吗?真?当然,“主观”是针对无法以一种或另一种方式证明的事物?
亚历山德拉·弗兰克斯

当然,您可以从生成最高效代码的角度看到它,但是任何现代编译器都应同样高效。最后,这更多是关于自行车棚颜色的问题。
jfs

8
我不同意,我不认为这是主观的。一个简单的ASM差异很重要,在许多情况下,您不能仅仅忽略几秒钟的优化。在这个问题上,这不是一场宗教战争或辩论,有一个合理的解释说明为什么这样做会更快,只需阅读已接受的答案即可。
chakrit


@RichardFranks题外话:gra!你是我见过的第一个接管人的人
jungle_mole 2015年

Answers:


162

使用开关。

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

在最佳情况下,优化器可能会找到一种更好的方式来生成代码。编译器执行的常见操作是构建二进制决策树(在平均情况下保存比较和跳转)或仅构建跳转表(完全不进行比较的工作)。


2
从技术上讲,仍然会有一个比较,以确保枚举的值位于跳转表之内。
0124816

耶 确实如此。开启枚举并处理所有情况可能会摆脱最后的比较。
Nils Pipenbrinck

4
请注意,从理论上讲,一系列if可以被编译器分析为与switch相同,但是为什么要抓住机会呢?通过使用开关,您可以准确地传达所需的信息,这确实使代码生成更加容易。
jakobengblom2

5
jakoben:可以这样做,但仅适用于类似开关的if / else链。实际上,由于程序员使用switch,所以不会发生这种情况。我研究了编译器技术并深信不疑:找到这样的“无用”构造会花费很多时间。对于编译人员来说,这样的优化确实有意义。
Nils Pipenbrinck

5
用的放心建设伪递归的@NilsPipenbrinck if- else模板元编程链,产生的困难switch case链,该映射可能会变得更加重要。(是的,古老的评论,但网络永远存在,或者至少直到下个星期二)
Yakk-Adam Nevraumont

45

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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


5
这是真的。可读性比switch和if语句都好得多。实际上,我本人将要回答这样的问题,但是您击败了我。:-)
mlarsen

如果您的枚举值都很小,那么您就不需要哈希,只需一个表即可。例如const std::bitset<MAXERR> specialerror(initializer); 与一起使用if (specialerror[numError]) { fire_special_event(); }。如果要进行边界检查,bitset::test(size_t)将对边界值抛出异常。(bitset::operator[]不进行范围检查)。 cplusplus.com/reference/bitset/bitset/test。这可能会胜过由编译器生成的实现switchesp的跳转表。在非特殊情况下,这将是一个未采用的分支。
彼得·科德斯

@PeterCordes我仍然认为最好将表放入自己的函数中。就像我说的那样,当您执行此操作时,会打开很多选项,但我并没有尝试列举所有选项。
Mark Ransom

@MarkRansom:我并不是要不同意抽象它。既然您使用给出了一个示例实现std::set,我想我会指出这可能是一个糟糕的选择。事实证明,gcc已经编译了OP的代码以在32位立即数中测试位图。godbolt:goo.gl/qjjv0e。gcc 5.2甚至会针对该if版本执行此操作。同样,最近的gcc将使用bit-test指令,bt而不是转移到1正确位置并使用test reg, imm32
彼得·科德斯

此立即恒定位图是一个很大的胜利,因为在位图上没有缓存未命中。如果“特殊”错误代码均在64或更小范围内,则该方法有效。(对于旧的32位代码,则为32。)如果非零,则编译器会减去最小的大小写值。结论是,最近的编译器足够聪明,您可能会从使用的任何逻辑中获取良好的代码,除非您告诉它使用庞大的数据结构。
彼得·科德斯

24

该开关更快。

只需尝试/是否在循环内添加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 ;
    }
}

不会编译。

然后,大多数人将使用define(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添加了以下注释:“ 在头文件中将常量定义为枚举是处理此问题的另一种方式”。

当然是的。

外部类型的意义在于将值与源分离。将此值定义为宏,简单的const int声明或枚举都具有内联该值的副作用。因此,如果定义,枚举值或const int值更改,则需要重新编译。extern声明表示在值更改的情况下无需重新编译,但是另一方面,使得无法使用switch。结论是使用开关将增加开关代码和用作实例的变量之间的耦合。确定时,请使用开关。如果不是,那也就不足为奇了。

编辑2013-01-15:

弗拉德·拉扎连科Vlad Lazarenko)评论了我的回答,并链接到他对开关生成的汇编代码的深入研究。很有启发性:http ://lazarenko.me/switch/


在头文件中将常量定义为枚举是另一种处理方式。
bk1e


1
@Vlad Lazarenko:感谢您的链接!这是一个非常有趣的阅读。
paercebal

1
@AhmedHussein user404725的链接已死。幸运的是,我在WayBack Machine中找到了它:web.archive.org/web/20131111091431/http ://lazarenko.me/2013/01/… 。确实,WayBack Machine可以算是一件幸事。
杰克·吉芬

非常感谢,这非常有帮助
Ahmed Hussein

20

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


3
编译器很可能不会碰if-then-else。实际上,gcc不会肯定会这样做(有充分的理由)。Clang会将这两种情况都优化为二进制搜索。例如,参见this

7

开关(如果仅出于可读性考虑)。在我看来,如果语句更难以维护且更难以阅读,那将是巨大的。

ERROR_01://故意掉线

要么

(ERROR_01 == numError)||

与第一个相比,后者更容易出错,并且需要更多的键入和格式设置。


6

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


6

使用开关,这是它的用途,也是程序员的期望。

不过,我会放入多余的箱子标签-只是为了让人们感到舒服,我试图记住何时/什么规则将其排除在外。
您不希望下一位从事此工作的程序员必须对语言细节进行任何不必要的思考(可能几个月后就可以了!)


4

编译器确实擅长优化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的errNumber(用lea将该操作与移动相结合)。这样就可以使位图适合32位立即数,避免立即使用64位movabsq占用更多指令字节的情况。

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

    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(Bulldozer之前)受益:“ rep ret”是什么意思?。没有它,分支预测在那些过时的CPU上将无法正常工作。

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

Agner Fog的指令表中可以看到,可变计数移位指令的速度比bt最近的Intel 慢(2微秒而不是1微秒,并且移位不能完成所需的其他一切)。


4

switch()优于其他情况的优点是:1.与其他情况相比,switch效率更高,因为每种情况都不依赖于先前的情况,这与需要检查单个语句是否为真或不正确的情况不同。

  1. 当没有。对于单个表达式的值,切换大小写比if更灵活,因为在其他情况下,判断仅基于两个值,即true或false。

  2. switch中的值是用户定义的,而其他情况下的值则基于约束。

  3. 如果有错误,可以很容易地交叉检查和更正switch中的语句,如果if else语句则比较难检查。

  4. 开关盒更为紧凑,易于阅读和理解。


2

国际海事组织,这是造成交换机掉线的一个完美示例。


在C#中,这是唯一发生跌倒思想的情况。好论点就在那里。
BCS

2

如果将来您的案例可能仍会分组(如果一个案例对应一个以上的案例),则证明该开关更易于阅读和维护。


2

他们同样出色地工作。对于现代编译器,性能几乎相同。

我更喜欢if语句而不是case语句,因为它们更具可读性,并且更灵活-您可以添加不基于数字等式的其他条件,例如“ || max <min”。但是对于您在此处发布的简单案例,这并不重要,只需执行您最可读的内容即可。


2

开关绝对是首选。与读取long if条件相比,查看交换机的案例列表并确定其操作要容易得多。

这种if情况下的重复很难看。假设其中之一==被写成!=; 你会注意到吗?或者,如果将“ numError”的一个实例写为“ nmuError”,而该实例恰好可以编译?

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

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


2

我同意交换机解决方案的兼容性,但是IMO您正在此处劫持交换机
开关的目的是根据值进行不同的处理。
如果必须用伪代码解释算法,则应使用if,因为从语义上讲就是这样:if what_error这样做 ...
因此,除非您打算有一天打算更改代码以针对每个错误使用特定代码,我会使用if


2
我不同意,因为与不同意情况相同。我将开关读为“在01、07、0A,10、15、16和20发生火灾特殊事件的情况下”。没有其他内容可以讨论。,这只是C ++语法的一个人工产物,您在其中为每个值重复使用'case'关键字。
MSalters

1

为了清楚和方便起见,我会选择if语句,尽管我确信有些人会不同意。毕竟,您想做if一些符合条件的事情!用一个动作进行切换似乎有点...不必要。


1

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

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

甚至可能针对各种可能性(一个数组)测试您的条件(在本例中为numerror),因此除非确定会出现结果,否则甚至不使用您的SWITCH。


总共大约有30个错误。10需要采取特殊措施,因此我将默认值用于不需要采取措施的
〜20

1

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


哇,这似乎是一种将简单问题变成复杂问题的方法。当编译器将为您做得很好时,为什么还要去解决所有这些麻烦。另外,它显然是一个错误处理程序,因此它对速度的要求不大。到目前为止,开关是最容易阅读和维护的东西。
MrZebra

表几乎不复杂-实际上,它可能比切换代码更简单。声明确实提到性能是一个因素。
格雷格·惠特菲尔德

这听起来像过早的优化。只要您保持枚举值较小且连续,编译器就应该为您执行此操作。将开关放在单独的函数中可以使使用该开关的代码保持美观和小巧,就像Mark Ransom在他的回答中所建议的那样,也具有相同的小代码优势。
彼得·科德斯

另外,如果您要自己实施任何操作,请制作一个std::bitset<MAXERR> specialerror;,然后if (specialerror[err]) { special_handler(); }。特别是,这将比跳转表更快。在不采取的情况下。
彼得·科德斯

1

我不确定最佳做法,但我会使用switch-然后通过“默认”陷阱故意掉线


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

4
语言的语法确实会影响我们实现解决方案的方式... =>在C语言中看起来丑陋而在Python中则很好。:)
rlerallut

使用位图?如果error_0a是0x0a等,则可以将它们长时间放置。long long special_events = 1LL << 1 | 1LL << 7 | 1LL << 0xa ...然后使用if(special_events&(1LL << numError)fire_special_event()
paperhorse 2009年

1
uck 您已将O(1)最坏情况操作(如果生成了跳转表)转换为O(N)最坏情况(其中N是要处理的情况数),并且您使用了break外部a case(是,未成年人)罪,但仍然是罪)。:)
Mac

uck?他说,性能和空间并不关键。我只是在提出另一种解决问题的方法。如果我们可以用人类少思考的方式来表示问题,那么我通常不在乎这是否意味着计算机必须多思考。
mbac32768 2011年

1
while (true) != while (loop)

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


这似乎是对McAnix答案的评论。这只是在Java 中尝试计时if而不是switch作为循环结束条件的问题之一。
彼得·科德斯

1

关于编译程序,我不知道是否有任何区别。但是对于程序本身以及使代码尽可能简单,我个人认为这取决于您要执行的操作。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";

但是,如果不是这样的话,语句可能会变得很复杂和混乱(尽管您会尽力而为)。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()case块。


0

我不是要告诉您有关速度和内存使用情况的人,但是查看转换语句是一件容易理解的事,而不是那么大的if声明(尤其是下线2-3个月)


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

循环计数的变化很大:

而if / else:5毫秒切换:1毫秒最大循环:100000

而if / else:5毫秒切换:3毫秒最大循环次数:1000000

而if / else:5毫秒切换:14毫秒最大循环:10000000

而if / else:5毫秒切换:149毫秒最大循环次数:100000000

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


3
很好,但是抱歉,老兄,您使用的是错误的语言。语言变化很大;)
Gabriel Schreiber

2
if(max) break循环在固定时间运行,无论循环计数?听起来JIT编译器足够聪明,可以优化到的循环counter2=max。如果第一次调用的currentTimeMillis开销更大,也许它比switch慢,因为还不是JIT编译的所有东西吗?将循环按其他顺序放置可能会得出不同的结果。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.