switch语句如何执行?

Pho*_*ams 6 .net c#

我正在逐步完成两个不同的代码块,以了解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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 10

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

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

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

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


切换codegen很复杂.让我们从简单的开始吧.编译器可以选择编译一个开关,好像它是一堆if-else语句,并且实际上它经常这样做.如果开关非常小,特别是在您的示例中,它尤其如此.

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


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

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

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


如果我们有一个大型开关但是整数是整数怎么办?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  default: … break;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


那这个呢?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  case 2001: … break;
  default: … break;
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器可以像以前一样生成跳转表,使用2002条目,在2000插槽中,它只是放置默认部分的地址.

那这个呢?

switch(whatever)
{  
  case 1000: … break;
  case 1001: … break;
  … many more …
  case 1999: … break;
  case 1000001: … break;
  default: … break;
}
Run Code Online (Sandbox Code Playgroud)

现在我们处在一个有趣的案例中!编译器不会生成包含1000002个条目的跳转表,其中大多数是"转到默认情况".在这种情况下,编译器可以生成混合交换机:

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

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

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

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

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

MSVC团队无法优先为我们快速修复bug,因此我们最终编写了英雄宏代码,让我们以一种看起来合理的方式表达switch语句,但在幕后,宏会产生适当的跳转表!

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