我的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语句的头部,因为这是我在测试中唯一被执行的东西,因为我总是使用一个不包含任何语句的值来调用有问题的方法(并且没有默认情况),所以它会掉进去(我希望)不会在交换机内部执行任何代码。
答案 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来阻止它将分离的函数重新组合在一起。)