switch语句如何执行?

时间:2018-05-18 13:24:01

标签: c# .net

我正在逐步完成两个不同的代码块,以了解switch语句和if之间的执行差异。

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

5 个答案:

答案 0 :(得分:9)

  

虽然似乎检查每一个条件,但是切换似乎直接跳到评估为true的条件。在预检之前,编译器如何知道哪个语句将评估为真?

让我们首先纠正您对行话的使用。 C#编译器将C#转换为IL。运行时有一个jit编译器,可以将IL转换为机器代码。 C#编译器还会生成信息,通知调试器,当您单步执行时,它应该导致中断。

所以你的问题并不清楚。问题是" C#编译器如何生成IL以避免线性搜索?"或者它是" C#编译器如何通知调试器它应该跳过显示交换机codegen的搜索部分?"或者它是" jit编译器如何为交换机生成机器代码?"

告诉你什么,我会在这里轻轻一点,给你模糊的答案来描述策略而不是细节。如果您对详细信息有疑问,请发布更具体的问题。特别是,我将使用"编译器"表示C#编译器或jit编译器,而不是说哪个。有时候工作是由C#编译器完成的,有时它会将工作推迟到抖动,但不管怎样,工作都完成了。

切换代码很复杂。让我们从简单的开始吧。编译器可以选择编译一个开关,好像它是一堆if-else语句,并且实际上它经常这样做。如果开关非常小,特别是如例子那样。

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

如果开关很大会怎么样?做一个巨大的if-else来找到正确的开关部分可能很慢。让我们再做一个简单的案例,看看一个充满字符串的大开关。在这种情况下,编译器将执行以下操作:

  • 计算每个开关部分的地址
  • 生成从字符串到int
  • 构建静态字典的代码
  • 开关变为"在字典中查找字符串;如果它不存在,如果没有默认值,则转到默认情况或开关的结尾。如果有,请转到字典中的地址"

现在快速查找字符串而不检查其中每一个字符串的工作都留给了字典,该字典使用哈希表来减少字符串比较的数量。

如果我们有一个大的开关但是这些情况是整数怎么办?

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

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

  • 如果该值小于1000或大于1999,请转至默认情况
  • 否则,通过取消引用数组来查找地址,然后去那里。

所以现在我们分为两个比较和一个数组解除引用;这确实直接跳到了正确的部分。

这个怎么样?

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

没有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,请转到默认情况
  • 否则,请查看跳转表

等等。我确定你可以看到当开关很大并且有许多密集的值范围时,这会变得非常复杂。

优化社区已经开展了大量工作,以确定创建混合交换机策略的最佳平衡点。

还有很多机会让事情出错。我将结束20世纪90年代的战争故事。当时的MSVC编译器 - 我认为它是版本4 - 有一个错误,在我的特定程序中,混合跳转表生成错误。基本上,当它不需要时,它会将一堆连续的范围分解成单独的跳转表。

通常这不会有问题,但是这个特定的开关是Microsoft VBScript和JScript引擎中逻辑的最里面的循环,它们调度脚本中的下一条指令。 (当时VBScript和JScript编译为专有的字节码语言。)坏的开关破坏了脚本引擎的性能。

MSVC团队无法为我们更快地确定错误的优先级,因此我们最终编写了英雄宏代码,让我们以一种看起来合理的方式表达switch语句,但是在幕后宏会生成适当的跳转表!

幸运的是,您可以依靠C#编译器和抖动为您生成良好的代码。您不必担心为交换机生成的代码。

答案 1 :(得分:3)

  

<强>声明
  正如其他人所评论的那样,底层编译的IL与您看到断点移动的方式不同。在引擎盖下,它实际上是一个接一个地检查每个值   但是,你的断点并没有显示出来。

     

阅读你的问题,我认为最重要的是要注意你的if else if else if结构是一个块;它不是。我的答案从逻辑的角度关注这种不正确的期望,而不是深入研究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 * ****** ______|______ | | ******** ******** * THEN * * ELSE * ******** ******** | ****** * IF * ****** ______|______ | | ******** ******** * THEN * * ELSE * ******** ******** | ****** * IF * ****** ______|______ | | ******** ******** * THEN * * ELSE * ******** ******** 就会出现:

Dim pdfReader As New PdfReader(pdfTemplate)

你是正确的,他们语法看起来非常相似,但是当你观察他们的逻辑流程时它们是非常不同的。

答案 2 :(得分:2)

在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开始加载switch变量,然后加载静态值“a”(IL_000D),比较它们(IL_0012)然后如果true跳转到IL_0035( IL_0017)。然后针对每个案例陈述重复此操作。在最后一个case语句之后,它跳转到最后(跳过每个案例中的所有代码)(IL_0033)。

因此,您的观察“'Switch'似乎直接跳到评估为true的条件”实际上并非如此。当单步执行调试器时,它可能看起来像这样,但这并不代表底层编译代码的工作方式。

我应该注意,现在总是如何编译switch语句,只是这就是这个特定的编译方式。

如果你增加case语句的数量,那么它将使用跳转表来实现它,在这种情况下,它创建一个私有字典,以case字符串作为键,然后索引作为值。然后它在该字典中查找switch变量,并在跳转表中使用它来计算IL中移动执行的位置。所以在这种情况下, 能够直接进入正确的分支,就像在字典中查找值时一样,它不需要检查每个单独的名称/值对。 / p>

Is there any significant difference between using if/else and switch-case in C#?(感谢@ Sudsy1002进行链接)更详细地讨论了其中的一些内容。

答案 3 :(得分:1)

TL; DR:逻辑,依次检查switch语句中的每个case,并且第一个匹配&#34; wins&#34;。作为实现细节,编译器通常能够优化它。

当每个&#34;案例&#34;是一个常量,编译器可能能够生成一个跳转表来优化事物。这将始终在逻辑上给出与依次检查每个案例相同的结果。它如何创建该表很可能取决于开关类型 - 例如,切换整数比切换字符串更简单。

当案例包含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
&#34;&#34;秒&#34;消息未被记录,因为模式的开头并不匹配:我们没有达到保护条款(when)。但执行可能仍然需要检查。 可能该实现可以有效地切换类型,并且知道如果objint,则需要检查第一和第三种模式。

基本上,从C#7开始,编译器有很多优化的自由度 - 并且当所有案例标签都是常量时非常非常可能这样做 - 但逻辑上它需要检查一次一个案例。

答案 4 :(得分:0)

我记得有人说switch内部有string个案例会转换为if-else结构。基本上说它只是语法糖。

带有ints / enums的

switch语句可能实际上有所不同(因此比if / else更快)