切换if-else语句的优点

时间:2008-09-18 23:28:24

标签: c++ if-statement optimization switch-statement

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

24 个答案:

答案 0 :(得分:149)

使用开关。

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

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

答案 1 :(得分:42)

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

if (RequiresSpecialEvent(numError))
    fire_special_event();

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

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

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

答案 2 :(得分:19)

开关 更快。

只需在循环中尝试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 ;
    }
}

不会编译。

大多数人将使用定义(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声明,或者甚至作为枚举具有内联值的副作用。因此,如果define,enum值或const int值发生变化,则需要重新编译。 extern声明意味着在价值变化的情况下不需要重新编译,但另一方面,使得无法使用开关。结论是使用开关将增加开关代码和用作情况的变量之间的耦合。如果是,则使用开关。如果不是,那就不足为奇了。

  

编辑2013-01-15:

     

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

答案 3 :(得分:18)

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

答案 4 :(得分:6)

Switch,只是为了便于阅读。在我看来,巨大的if语句难以维护且难以阅读。

ERROR_01 ://故意落实

(ERROR_01 == numError)||

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

答案 5 :(得分:5)

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

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

答案 6 :(得分:5)

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

答案 7 :(得分:3)

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

我在godbolt上制作了一些测试用例。

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

e.g。 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编译为位图,但使用mov / shift执行1U<<errNumber。它将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()

请注意它如何从errNumber中减去1(使用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无所不在,且为a compiler bug。)

rep ret用于分支目标,并遵循条件分支,以利​​用旧的AMD K8和K10(推土机前):What does `rep ret` mean?。没有它,分支预测在那些过时的CPU上也不起作用。

具有寄存器arg的

bt(位测试)很快。它结合了左移1到errNumber位并执行test的工作,但仍然是1个周期延迟,只有一个英特尔uop。由于其过于CISC的语义,因此内存arg速度很慢:对于&#34;位字符串&#34;的内存操作数,要测试的字节的地址是基于另一个计算的arg(除以8),并且不限于内存操作数指向的1,2,4或8字节块。

Agner Fog's instruction tables开始,可变计数移位指令比最近的英特尔上的bt慢(2 uops而不是1,并且shift不会执行其他所有操作需要)。

答案 8 :(得分:2)

IMO这是一个完美的例子,说明了切换的后果。

答案 9 :(得分: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()(使用 if)和 functionB()(使用 switch)产生与 clang 完全相同的汇编输出! functionC() 使用了一个跳转表,尽管许多其他海报认为跳转表是 switch 的专有功能。然而,尽管许多人认为跳转表是最佳的,但实际上这是 clang 上最慢的解决方案:functionC() 需要比 functionA()functionB() 多大约 20% 的执行时间。

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

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

functionH() 生成的代码看起来比 g++ 复杂得多 - 但实际上 clang 的运行速度要快一些,functionA() 的运行速度要快得多。在非手动优化的函数中,functionC()functionC() 上是最快的,并且比 g++ 上的任何函数都快。相反,使用 clang 而不是 functionH() 编译时,g++ 需要两倍的执行时间,主要是因为 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

加速的原因是,如果测试的最高值为 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 上使用简单 functionA() 的非优化 if() 的执行速度几乎与手动优化的 g++ 一样快,并且它还产生相当类似的汇编输出。

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

未来的编译器版本将越来越好地优化您的代码,并更接近您的手动优化。所以不要在这上面浪费时间,除非周期对您来说真的很重要。

这里是测试代码:

g++

答案 10 :(得分:1)

while (true) != while (loop)

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

答案 11 :(得分: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()

答案 12 :(得分:1)

我同意交换机解决方案的强大功能,但IMO你在这里劫持交换机
切换的目的是根据值进行不同的处理 如果你必须用伪代码来解释你的算法,你会使用if,因为在语义上它是这样的: if whatever_error这样做 ......
因此,除非您打算在某一天更改代码以获得每个错误的特定代码,否则我会使用 if

答案 13 :(得分:1)

切换绝对是首选。查看交换机的案例列表会更容易。知道它在做什么比阅读long if条件。

if条件下的重复很难看。假设其中一个==被写为!=;你会注意到吗?或者,如果'numError'的一个实例被写成'nmuError',那恰好是编译?

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

至于性能,最好的办法是使用分析器在类似于野外期望的条件下测量应用程序的性能。否则,你可能在错误的地方以错误的方式进行优化。

答案 14 :(得分:1)

他们的工作同样出色。鉴于现代编译器,性能大致相同。

我更喜欢if语句而不是case语句因为它们更易读,更灵活 - 你可以添加其他不基于数字相等的条件,比如“|| max&lt; min”。但是对于你在这里发布的简单案例,它并不重要,只要做你最可读的事情。

答案 15 :(得分:1)

如果你的案件将来可能会被分组 - 如果不止一个案件对应一个结果 - 那么交换机可能更容易阅读和维护。

答案 16 :(得分:1)

我不确定最佳练习,但是我会使用开关 - 然后通过'默认'陷入故意堕落

答案 17 :(得分:0)

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

答案 18 :(得分:0)

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

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

甚至可能会根据可能性列表测试你的情况(在这种情况下是numerror),也许这样一个数组可能会使你的SWITCH甚至没有被使用,除非肯定会有结果。

答案 19 :(得分:0)

我不是那个告诉你速度和内存使用情况的人,但是看一下开关声明是一个很容易理解的地方然后是一个很大的if声明(特别是2-3个月之后)

答案 20 :(得分:0)

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

答案 21 :(得分: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 if / else:5ms 开关:1ms 最大循环次数:100000

if if / else:5ms 开关:3ms 最大循环次数:1000000

if if / else:5ms 开关:14ms 最大循环次数:10000000

if if / else:5ms 开关:149ms Max Loops:100000000

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

答案 22 :(得分:0)

为了清晰和惯例,我会选择if语句,虽然我确信有些人不同意。毕竟,你想要做某事if某些条件是真的!有一个动作的开关似乎有点......不必要。

答案 23 :(得分:0)

在编写程序时,我不知道是否存在任何差异。但至于程序本身并保持代码尽可能简单,我个人认为这取决于你想做什么。 if if else if语句有哪些优点,我认为是:

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

(示例:

`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语句可以匆忙地变得复杂和混乱(尽管你最好的尝试)。切换语句往往更清晰,更清晰,更易于阅读;但只能用于测试特定值(例如:

`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()大小写块。