代码之家  ›  专栏  ›  技术社区  ›  bsneeze

切换if else语句的优点

  •  152
  • bsneeze  · 技术社区  · 16 年前

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

    转换 声明:

    // 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 ((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 回复  |  直到 16 年前
        1
  •  147
  •   Drew Dormann    12 年前

    使用开关。

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

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

        2
  •  42
  •   Mark Ransom    16 年前

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

    if (RequiresSpecialEvent(numError))
        fire_special_event();
    

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

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

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

        3
  •  19
  •   Community CDub    8 年前

    开关 更快。

    只需尝试在一个循环中使用if/else 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 添加了以下注释:“ 在头文件中将常量定义为枚举是另一种处理方法”。

    当然是。

    外部类型的要点是将值与源分离。将该值定义为宏、简单的const int声明或甚至是枚举,都会产生内嵌该值的副作用。因此,如果define、枚举值或const值更改,则需要重新编译。extern声明意味着在值发生变化时不需要重新编译,但另一方面,它使使用switch变得不可能。结论是 使用switch将增加switch代码和用作cases的变量之间的耦合 . 正常时,使用开关。如果不是的话,那就不奇怪了。

    .

    编辑2013-01-15:

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

        4
  •  18
  •   Alexandra Franks    16 年前

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

        5
  •  6
  •   scubabbl    16 年前

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

    Error 01 ://故意摔倒

    (错误_01==numeror)||

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

        6
  •  5
  •   Bdoserror    16 年前

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

        7
  •  5
  •   Martin Beckett    16 年前

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

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

        8
  •  3
  •   Community CDub    8 年前

    编译器非常擅长优化 switch . 最近的GCC还擅长在 if .

    我做了一些测试 godbolt .

    case 值被紧密地分组在一起,gcc、clang和icc都足够智能,可以使用位图检查值是否是特殊值之一。

    例如,GCC 5.2-O3编译了 转换 到(和) 如果 非常相似的东西):

    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汇编了 转换 到位图,但是 1U<<errNumber 带MOV/SHIFT。它编译了 如果 一系列分支的版本。

    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 无所不在,而且 a compiler bug )

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

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

    Agner Fog's instruction tables ,变量计数移位指令比 英国电信 在最近的Intel上(2个Uops而不是1个,而shift不做任何其他需要的事情)。

        9
  •  2
  •   Nescio    16 年前

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

        10
  •  1
  •   TSomKes    16 年前

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

        11
  •  1
  •   SquareCog    16 年前

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

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

        12
  •  1
  •   Jay Bazuzi Buck Hodges    16 年前

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

    复制在 if 这种情况对眼睛很不利。假设其中一个 == 是书面的 != 你会注意到吗?或者,如果“numeror”的一个实例被写为“nmuerror”,这是编译时发生的?

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

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

        13
  •  1
  •   Francesca    16 年前

    我同意交换解决方案的复杂性,但我认为 劫持开关 在这里。
    开关的目的是 不同的 根据值进行处理。
    如果你必须用伪代码来解释你的算法,你应该使用if,因为,从语义上来说,这就是它: 如果有什么错误
    所以,除非有一天你打算改变你的代码,为每一个错误设置特定的代码,否则我会使用 如果 .

        14
  •  1
  •   da5id    14 年前

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

        15
  •  1
  •   mbac32768    14 年前

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

    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()
    
        16
  •  1
  •   Jérôme Verstrynge    13 年前
    while (true) != while (loop)
    

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

        17
  •  0
  •   William Keller    16 年前

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

        18
  •  0
  •   David Nehme    16 年前

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

        19
  •  0
  •   Ed Brown    16 年前

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

        20
  •  0
  •   lewis    16 年前

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

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

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

        21
  •  0
  •   Greg Whitfield    16 年前

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

        22
  •  0
  •   McAnix    15 年前

    我知道它很古老,但是

    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

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

        23
  •  0
  •   Jordan Effinger    8 年前

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

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

    (例如:

    `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语句会很快变得复杂和混乱(尽管你尽了最大的努力)。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块。