.net switch语句如何执行?

ccgok5k5  于 2023-03-20  发布在  .NET
关注(0)|答案(5)|浏览(230)

我将逐步执行两个不同的代码块,以了解switchif语句在执行方面的区别。
虽然if似乎会检查每个条件,但switch会直接跳到求值为true的条件。编译器如何知道哪个语句将在预检查时求值为true?请参见以下代码:

public void IFvSwitch()
    {
        string x = "f";
        switch (x)  // Compiler skips directly to case "f":
        {
            case "a":
                MessageBox.Show(x);
                break;
            case "b":
                MessageBox.Show(x);
                break;
            case "f":
                MessageBox.Show(x);
                break;
        }

        if (x == "a") // Compiler checks all conditions 
        {
            MessageBox.Show(x);
        }
        else if (x == "b")
        {
            MessageBox.Show(x);
        }
        else if (x == "f")
        {
            MessageBox.Show(x);
        }
    }
kkih6yb8

kkih6yb81#

免责声明

正如其他人所评论的那样,底层编译的IL与您看到的断点移动方式不同。在其内部,它实际上是逐个检查每个值。
但是,您的断点没有显示这一点。
阅读你的问题,我认为这里最重要的事情是你假设你的if else if else if结构是一个块;我的回答是从逻辑的Angular 来关注这种不正确的期望,而不是深入研究IL。
你漏掉了一条重要线索:

if (x == "a") // Compiler checks all conditions 
    {
        MessageBox.Show(x);
    }
    else if (x == "b")
    {
        MessageBox.Show(x);
    }
    else if (x == "f")
    {
        MessageBox.Show(x);
    }

这些都是单独的if语句,它们一个接一个地堆叠(在彼此的else情况下)。它们单独执行是因为它们是单独的步骤!
您的示例具有非常相似的if求值,但是您可以在这里做非常不同的事情:

if (x == "a") // Compiler checks all conditions 
    {
        MessageBox.Show(x);
    }
    else if (IsElvisStillAlive())
    {
        MessageBox.Show(x);
    }
    else if (sonny + cher == love)
    {
        MessageBox.Show(x);
    }

一次评估对下一次评估毫无意义,它们没有任何联系。
注意缩进(省略大括号)可以帮助展示这一点,缩进隐藏了这些步骤是嵌套的事实。

if (x == "a") // Compiler checks all conditions 
    {
        MessageBox.Show(x);
    }
    else 
    {
        if (x == "b")
        {
            MessageBox.Show(x);
        }
        else 
        {
            if (x == "f")
            {
                MessageBox.Show(x);
            }
        }
    }

这个修改后的缩进,虽然冗长得不必要(尽管有些人喜欢这样),但在解释为什么这些是单独的步骤方面要清楚得多。
注意你没有问if如何知道如何进入第一种或第二种情况,这是相当明显的。
一个switch实际上是一个具有两个以上结果的if
你可以这样想:switch只需执行一个步骤:

**********
                * SWITCH *
                **********
     _________ _/ |    |  \ _________      
    /             |    |             \  
********    ********  ********    ********
* CASE *    * CASE *  * CASE *    * CASE *
********    ********  ********    ********

但是您的if福尔斯3种不同的if情况:

******                             
        * IF *                             
        ******  
    ______|______
    |            |
********    ********  
* THEN *    * ELSE *  
********    ********    
                 |
               ******                      
               * IF *                      
               ******  
           ______|______
           |           |
       ********    ********  
       * THEN *    * ELSE *  
       ********    ********      
                      |
                   ******                  
                   * IF *                  
                   ******  
               ______|______
              |             |
           ********    ********  
           * THEN *    * ELSE *  
           ********    ********

您说得对,它们在语法上看起来非常相似,但是当您观察它们的逻辑流时,它们是非常不同的。

wqnecbli

wqnecbli2#

在linqpad 4中,我编译了以下方法来查看底层IL:

void sw()
{
        string x = "f";
        switch (x)  // Compile skips directly to  case "f":
        {
            case "a":
                Console.WriteLine(x);
                break;
            case "b":
                Console.WriteLine(x);
                break;
            case "f":
                Console.WriteLine(x);
                break;
        }
}

生成的IL如下所示:

IL_0000:  nop         
IL_0001:  ldstr       "f"
IL_0006:  stloc.0     // x
IL_0007:  ldloc.0     // x
IL_0008:  stloc.1     // CS$4$0000
IL_0009:  ldloc.1     // CS$4$0000
IL_000A:  brfalse.s   IL_0050
IL_000C:  ldloc.1     // CS$4$0000
IL_000D:  ldstr       "a"
IL_0012:  call        System.String.op_Equality
IL_0017:  brtrue.s    IL_0035
IL_0019:  ldloc.1     // CS$4$0000
IL_001A:  ldstr       "b"
IL_001F:  call        System.String.op_Equality
IL_0024:  brtrue.s    IL_003E
IL_0026:  ldloc.1     // CS$4$0000
IL_0027:  ldstr       "f"
IL_002C:  call        System.String.op_Equality
IL_0031:  brtrue.s    IL_0047
IL_0033:  br.s        IL_0050
IL_0035:  ldloc.0     // x
IL_0036:  call        System.Console.WriteLine
IL_003B:  nop         
IL_003C:  br.s        IL_0050
IL_003E:  ldloc.0     // x
IL_003F:  call        System.Console.WriteLine
IL_0044:  nop         
IL_0045:  br.s        IL_0050
IL_0047:  ldloc.0     // x
IL_0048:  call        System.Console.WriteLine
IL_004D:  nop         
IL_004E:  br.s        IL_0050
IL_0050:  ret

一般来说,您感兴趣的位从IL_000C开始,它加载开关变量,然后加载静态值“a”(IL_000D),比较它们(IL_0012),如果为真,则跳转至IL_0035(IL_0017)。然后,它对每个case语句重复此操作。在最后一个case语句之后,它跳到末尾(跳过每种情况下的所有代码)(IL_0033)。
因此,您观察到的“'Switch'似乎直接跳到计算结果为true的条件”实际上并不正确。在单步调试调试时,它可能看起来像这样,但这并不代表底层编译代码的工作方式。
我应该注意到,现在switch语句总是这样编译的,只是这个特定的语句是这样编译的。
如果你增加case语句的数量,那么它将使用一个跳转表来实现它,在这种情况下,它创建一个私有字典,把你的case字符串作为键,然后用一个索引作为值,然后在字典中查找switch变量,并在跳转表中使用它来计算出在IL中将执行移动到哪里,所以在这种情况下,它 * 将 *能够以同样的方式直接转到右边的分支,当在字典中查找值时,它不需要检查每一个名称/值对。
Is there any significant difference between using if/else and switch-case in C#?(感谢@Sudsy1002提供链接)更详细地讨论了其中的一些内容。

goqiplq2

goqiplq23#

TL;DR:* 逻辑上 *,switch语句中的每个case都被依次检查,第一个匹配的“wins”。作为一个 * 实现细节 *,编译器通常能够优化这一点。
当每个“case”都是常量时,编译器就可以生成一个跳转表来优化它,这在逻辑上总是和依次检查每个case给予相同的结果,它如何创建跳转表取决于开关类型,例如,切换整数比切换字符串简单。
当案例包含C#7中引入的模式时,编译器可能无法创建跳转表。下面的例子证明了可以在单个switch语句中检查多个case标签:

using System;

class Program
{
    public static void Main()
    {
        object obj = 200;
        switch (obj)
        {
            case int x when Log("First when", x < 10):
                Console.WriteLine("First case");
                break;
            case string y when Log("Second when", y == "This won't happen"):
                Console.WriteLine("Second case");
                break;
            case int z when Log("Third when", true):
                Console.WriteLine("Third case");
                break;                
        }
    }

    static bool Log(string message, bool result)
    {
        Console.WriteLine(message);
        return result;
    }
}

输出为:

First when
Third when
Third case

由于模式的开头不匹配,因此未记录“Second when”消息:我们还没有深入到保护子句(when),但是执行 * 可能 * 仍然需要检查它,实现 * 可能 * 会有效地切换类型,并且知道如果objint,它 * 只 * 需要检查第一个和第三个模式。
基本上,从C# 7开始,编译器有很大的优化余地--当所有的case标签都是常量时,编译器非常非常有可能这样做--但 * 逻辑上 *,它需要一次检查一个case。

ewm0tg9j

ewm0tg9j4#

我记得有人说过,一个带有string case的switch内部会被转换成一个if-else结构,基本上就是说这只是语法上的糖衣。
带int/enums的switch语句实际上的工作方式可能有所不同(因此比if/else更快)

t8e9dugd

t8e9dugd5#

if似乎检查了每一个条件,而switch似乎直接跳到了求值为true的条件。编译器如何在预检查之前知道哪个语句求值为true?
让我们从纠正你的行话开始。C#编译器将C#翻译成IL。运行时有一个jit编译器将IL翻译成机器码。C#编译器还生成信息,通知 * 调试器 * 当你单步执行时它应该在哪里引起中断。
因此,我们并不清楚您的问题是什么。是“C#编译器如何生成IL以避免线性搜索?”还是“C#编译器如何通知调试器它应该跳过显示开关代码生成器的搜索部分?”还是“JIT编译器如何为开关生成机器码?”
告诉你吧,我会在这里挥挥手,给予你一些模糊的答案,描述 * 策略 *,但不描述细节。如果你对细节有疑问,那么就提出一个更具体的问题。特别是,我会使用“编译器”来表示C#编译器或JIT编译器,但不会说明是哪一个。有时工作是由C#编译器完成的,有时它会把工作交给抖动。但不管怎样,任务都完成了
Switch代码生成很复杂。让我们从简单的开始。编译器可以选择编译一个switch,就好像它是一堆if-else语句一样,实际上它经常这样做。特别是当switch很小的时候,就像你的例子一样。

调试器如何知道不向您显示实际发生的比较步骤?编译器还生成调试信息,通知调试器在单步执行时哪些指令应该跳过,哪些指令应该生成自动断点。因此,C#编译器、JIT编译器和调试器一起工作,以确保开发人员在单步执行开关时有正确的体验。

如果switch很大会发生什么?执行大量的if-else来找到正确的switch部分可能会很慢。让我们再做一个简单的例子,看看一个充满字符串的大switch。在这种情况下,编译器会做以下事情:

  • 计算每个开关段的地址
  • 生成将静态字典从string构建为int的代码
  • 开关变为“在字典中查找字符串;如果不存在,则转到默认大小写,如果没有默认值,则转到开关的末尾。如果存在,则转到字典中的地址”

现在,快速查找字符串而不检查每一个字符串的工作留给了字典,字典使用哈希表将字符串比较的次数减少了一个巨大的因子。
如果我们有一个很大的开关,但是大小写都是整数呢?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  default: … break;
}

在这种情况下,编译器可以创建一个包含一千个开关情况地址的静态数组,然后生成如下代码:

  • 如果该值小于1000或大于1999,则转到默认大小写
  • 否则,通过解引用数组来查找地址,然后转到那里。

现在我们只剩下两次比较和一次数组解引用;这确实直接跳到了正确的部分。
这个呢?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  case 2001: … break;
  default: … break;
}

这里没有case 2000,在这种情况下编译器可以像以前一样生成一个跳转表,有2002个条目,在2000槽中,它只放置默认部分的地址。
这个呢?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  case 1000001: … break;
  default: … break;
}

现在我们遇到了一个有趣的情况!编译器不会生成一个包含1000002个条目的跳转表,其中大部分都是“转到默认情况”。在这种情况下,编译器可以生成一个混合开关:

  • 如果值小于1000,则转到默认情况
  • 如果值为1000001,则转到其大小写
  • 如果值大于1999,则转到默认情况
  • 否则,请检查跳转表

等等,我相信你们可以看到,当开关很大,并且有很多密集的取值范围时,这会变得非常复杂。
优化社区已经做了大量工作,研究创建混合交换机策略的最佳平衡。
也有很多出错的机会。我将用一个90年代的战争故事来结束。那时的MSVC编译器--我想是版本4 --有一个bug,在我的特定程序中,混合跳转表生成不正确。基本上,它在不需要的时候,把一堆连续的范围分解成单独的跳转表。
正常情况下,这不是问题,但这个特定的开关是Microsoft VBScript和JScript引擎中最内层的逻辑循环,它调度脚本中的下一条指令。(VBScript和JScript当时编译为专有字节码语言。)这个错误的开关会降低脚本引擎的性能。
MSVC团队无法为我们快速地修复bug,所以我们最终编写了英雄宏代码,让我们以一种看起来合理的方式来表达switch语句,但在幕后宏会生成适当的跳转表!

幸运的是,你可以依靠C#编译器和jitter为你生成好的代码,你不必担心为开关生成什么代码。

相关问题