.net switch语句的性能取决于未输入的内部大小写的代码大小
我的C#代码生成器将嵌套的switch语句吐出到类中的某个方法中,我在运行时加载并实例化该类,然后执行。与必须使用哈希表的非编译的通用版本相比,它的执行时间要快100倍(因为只有在运行时才知道哈希表键,它在编译版本中变为开关情况) 当switch语句变大时,性能基本保持不变,如果实际执行的“switch hop”的数量不变,即在未执行的case语句中添加代码不会影响性能 但是,这会一直工作到特定的代码大小,然后性能突然下降7倍(在32位模式下运行)或12倍(在本机64位模式下运行) 我看了一下JITted代码,事实上,随着代码的增长,没有更改的代码部分会发生更改。(不熟悉汇编和指令集)我假设存在“短跳转”和“长跳转”,前者受到它可以跳转的字节数的限制有人能向高级程序员解释为什么生成的机器代码必须是或是不同的吗? 注意,我知道我正在测试的代码几乎什么都不做,所以机器代码中最小的差异自然会对相对性能产生巨大影响。但所有这些的要点是生成代码,尽可能不做任何事情,因为它被称为每秒数十万次 以下是两种不同版本的switch语句头,在32位模式下运行时,总体代码大小相对较小且性能良好:.net switch语句的性能取决于未输入的内部大小写的代码大小,.net,performance,assembly,.net,Performance,Assembly,我的C#代码生成器将嵌套的switch语句吐出到类中的某个方法中,我在运行时加载并实例化该类,然后执行。与必须使用哈希表的非编译的通用版本相比,它的执行时间要快100倍(因为只有在运行时才知道哈希表键,它在编译版本中变为开关情况) 当switch语句变大时,性能基本保持不变,如果实际执行的“switch hop”的数量不变,即在未执行的case语句中添加代码不会影响性能 但是,这会一直工作到特定的代码大小,然后性能突然下降7倍(在32位模式下运行)或12倍(在本机64位模式下运行) 我看了一下J
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字节)堆栈并将其归零,这可能是在一个开关情况下使用的。这是一个非常有用的答案,也是一个有趣的问题,尽管有违直觉,但建议将代码分离为单独的、不可线性的函数。我会检查一下,如果我能将您的提示转换为实际性能。