.net switch语句的性能取决于未输入的内部大小写的代码大小

.net switch语句的性能取决于未输入的内部大小写的代码大小,.net,performance,assembly,.net,Performance,Assembly,我的C#代码生成器将嵌套的switch语句吐出到类中的某个方法中,我在运行时加载并实例化该类,然后执行。与必须使用哈希表的非编译的通用版本相比,它的执行时间要快100倍(因为只有在运行时才知道哈希表键,它在编译版本中变为开关情况) 当switch语句变大时,性能基本保持不变,如果实际执行的“switch hop”的数量不变,即在未执行的case语句中添加代码不会影响性能 但是,这会一直工作到特定的代码大小,然后性能突然下降7倍(在32位模式下运行)或12倍(在本机64位模式下运行) 我看了一下J

我的C#代码生成器将嵌套的switch语句吐出到类中的某个方法中,我在运行时加载并实例化该类,然后执行。与必须使用哈希表的非编译的通用版本相比,它的执行时间要快100倍(因为只有在运行时才知道哈希表键,它在编译版本中变为开关情况)

当switch语句变大时,性能基本保持不变,如果实际执行的“switch hop”的数量不变,即在未执行的case语句中添加代码不会影响性能

但是,这会一直工作到特定的代码大小,然后性能突然下降7倍(在32位模式下运行)或12倍(在本机64位模式下运行)

我看了一下JITted代码,事实上,随着代码的增长,没有更改的代码部分会发生更改。(不熟悉汇编和指令集)我假设存在“短跳转”和“长跳转”,前者受到它可以跳转的字节数的限制有人能向高级程序员解释为什么生成的机器代码必须是或是不同的吗?

注意,我知道我正在测试的代码几乎什么都不做,所以机器代码中最小的差异自然会对相对性能产生巨大影响。但所有这些的要点是生成代码,尽可能不做任何事情,因为它被称为每秒数十万次

以下是两种不同版本的switch语句头,在32位模式下运行时,总体代码大小相对较小且性能良好:

        switch (a)
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  dec         edx 
00000004  cmp         edx,3Bh 
00000007  jae         0000021D 
0000000d  jmp         dword ptr [edx*4+00773AD8h] 
        {
            case 1: return 1;
而且,在联合国输入的案例块中,代码稍微多了一点,但速度仍然一样快:

        switch (a)
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  lea         eax,[edx-1] 
00000006  cmp         eax,3Bh 
00000009  jae         00001C51 
0000000f  jmp         dword ptr [eax*4+00A35830h] 
        {
            case 1:
                {
这是更大代码的版本,结果是慢了7倍

        switch (a)
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  sub         esp,0FCh 
0000000b  mov         esi,ecx 
0000000d  lea         edi,[ebp+FFFFFEFCh] 
00000013  mov         ecx,3Eh 
00000018  xor         eax,eax 
0000001a  rep stos    dword ptr es:[edi] 
0000001c  mov         ecx,esi 
0000001e  mov         dword ptr [ebp-0Ch],edx 
00000021  mov         eax,dword ptr [ebp-0Ch] 
00000024  mov         dword ptr [ebp-10h],eax 
00000027  mov         eax,dword ptr [ebp-10h] 
0000002a  dec         eax 
0000002b  cmp         eax,3Bh 
0000002e  jae         00000037 
00000030  jmp         dword ptr [eax*4+0077C488h] 
00000037  jmp         0000888F 
        {
            case 1:
                {

注意:我只发布switch语句的开头,因为这是我测试中唯一执行的东西,因为我总是用一个不在case语句中的值调用有问题的方法(并且没有默认的case),所以它会失败并且(我希望)不在交换机内执行任何代码。

前两个示例与后一个示例之间的主要区别似乎是,正如Jester所指出的,后一个示例在堆栈上分配252个字节并将其归零。这并不是因为switch语句中的代码更大,而是因为switch语句中的代码使用了前两个示例没有使用的局部变量和临时变量。前两个例子要么不使用任何局部变量或临时变量,要么JIT优化器设法在寄存器中分配它们

最后一个示例的另一个值得注意的问题是地址0000001e-00000027处的MOV指令。这些指令将开关值
a
存储在堆栈的两个不同位置,并每次从堆栈重新加载该值。我的猜测是,存储在堆栈上的值永远不会被再次使用,这使得此代码完全没有必要。即使它们稍后在代码中使用,也不需要从堆栈中重新加载值。无论哪种方式,优化器都失败了。如果我是对的,并且这些堆栈位置未使用,那么优化器可能无法消除其他不必要的临时性,从而导致使用的堆栈空间比需要的还要多

我应该指出,第一个和第二个示例之间的差异显示了优化器如何正确处理类似的情况。前两个示例中的代码不同,因为优化器在第二个示例中保留了
a
的值,这可能是因为
a
稍后在代码中使用。在所有示例中,汇编代码将switch语句的范围从1-60规范化为0-59。这将在跳转表中保存一个条目和两条指令。在第一个示例中,
a
的值在执行此操作时丢失,在后两个示例中,
a
的值保留。第二个示例只是将a的值保留在它传递到函数的寄存器中。第三个示例还将其保留在原始寄存器中,然后将另外两个副本保存在堆栈上的不同位置

如果最常见的情况是switch语句中没有执行case,那么一个可能的解决方案是检查switch值是否在其自身函数的范围内。然后,该函数将仅在必要时调用包含switch语句的函数。否则,您可以尝试将使用频率较低和/或堆栈较高的用例从switch语句中移到它们自己的函数中


(我不熟悉Microsoft的JIT优化器,但您可能需要使用来防止将分离的函数内联在一起。)

第二个版本似乎正在为局部变量分配一大块(252字节)堆栈并将其归零,这可能是在一个开关情况下使用的。这是一个非常有用的答案,也是一个有趣的问题,尽管有违直觉,但建议将代码分离为单独的、不可线性的函数。我会检查一下,如果我能将您的提示转换为实际性能。