首页 文章

切换if-else语句的优点

提问于
浏览
148

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

23 回答

  • 19

    使用开关 .

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

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

  • 2

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

    if (RequiresSpecialEvent(numError))
        fire_special_event();
    

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

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

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

  • 5

    切换 is 更快 .

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

    现在, switch has one real problem :交换机必须在编译时知道每种情况下的值 . 这意味着以下代码:

    // 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声明意味着在 Value 变化的情况下不需要重新编译,但另一方面,使得无法使用开关 . 结论是使用开关将增加开关代码和用作情况的变量之间的耦合 . 如果是,则使用开关 . 如果不是,那就不足为奇了 .

    .

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

  • 4

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

  • 0

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

    ERROR_01 ://故意落空

    要么

    (ERROR_01 == numError) ||

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

  • 0

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

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

  • 2

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

  • 1

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

  • 1

    编译器非常擅长优化 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 编译为位图,但使用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 . 它_1183370_限于内存操作数指向的1,2,4或8字节块 .

    Agner Fog's instruction tables开始,可变计数移位指令比最近的英特尔上的 bt 慢(2 uops而不是1,并且需要shift = 't do everything else that') .

  • 18

    如果您的案例可能在将来保持分组 - 如果多个案例对应一个结果 - 那么交换机可能更容易阅读和维护 .

  • 145

    他们同样运作良好 . 鉴于现代编译器,性能大致相同 .

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

  • 0

    开关绝对是首选 . 查看一个开关的案例列表更容易,并且知道它正在做什么比阅读long if条件更容易 .

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

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

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

  • 0

    我同意交换机解决方案的强大功能,但IMO你在这里是 hijacking the switch .
    切换的目的是根据值进行 different 处理 .
    如果你必须用伪代码解释你的算法,那么你就是这样的: if whatever_error do this ...
    因此,除非您打算在某一天更改代码以获得每个错误的特定代码,否则我会使用 if .

  • 1

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

  • 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()
    
  • 0
    while (true) != while (loop)
    

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

  • 0

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

  • 1

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

  • 1

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

  • 42

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

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

    甚至可能测试你的条件(在这种情况下是numerror)对可能的列表,一个数组也许所以你的SWITCH甚至没有被使用,除非肯定会有结果 .

  • 0

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

  • 6

    我知道它的老了但是

    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:5ms开关:1ms Max Loops:100000

    而if / else:5ms开关:3ms Max Loops:1000000

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

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

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

  • 1

    在编译程序时,我不知道是否有任何区别 . 但至于程序本身并尽可能简化代码,我个人认为这取决于你想做什么 . 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()大小写块 .

相关问题