switch语句性能取决于未输入内部案例的代码大小

时间:2015-07-10 05:37:25

标签: .net performance assembly

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

当switch语句变得更大时,如果实际执行的“switch hop”的数量没有改变,那么性能几乎保持不变,即在不执行的语句中添加代码不会影响性能。

然而,直到某个代码大小为止,然后突然性能下降了7倍(在32位模式下运行时)或12(在本机64位模式下运行)。

我看了一下JITted代码,事实上,随着代码的增长,它会改变代码中未更改的部分内容。 (不熟悉汇编和指令集)我假设有类似“短跳”和“跳远”的东西,前者受到它可以跳跃的字节数的限制。 有人可以向高级程序员阐明为什么生成的机器代码必须或者不同?

N.B。我知道我正在测试几乎什么也没做的代码,因此机器代码中的最小差异自然会对相对性能产生巨大影响。但所有这一切的重点是生成尽可能接近零的代码,因为它每秒被称为数十万次。

当整体代码大小相对较小且性能良好时,以下是两个不同版本的switch语句头,如copied from Visual Studio using an JIT optimized Release build,以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:
                {

N.B。我只发布了switch语句的头部,因为这是我在测试中唯一被执行的东西,因为我总是使用一个不包含任何语句的值来调用有问题的方法(并且没有默认情况),所以它会掉进去(我希望)不会在交换机内部执行任何代码。

1 个答案:

答案 0 :(得分:2)

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

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

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

如果绝大多数情况下执行switch语句中没有任何情况,那么可能的解决方案是检查switch值是否在其自己的函数中。那么该函数只在必要时才调用包含switch语句的函数。否则,您可以尝试将不常使用和/或高堆栈使用情况从switch语句中移出到它们自己的函数中。

(我不熟悉Microsoft的JIT优化器,但您可能需要使用NoInlining attribute来阻止它将分离的函数重新组合在一起。)