您的问题涉及到循环不变式分析——编译器能否检测到循环中某个表达式的计算不依赖于循环状态,且没有副作用?
有理由期待编译器能够满足这两个条件——编译器可以足够智能地知道在枚举上调用 ToString() 方法永远不会改变,而且在枚举上调用 ToString() 方法没有任何显著的副作用。
也许有原因使得编译器 actively 决定不提升不变式——或许调用该函数比在堆栈上存储额外变量更快。
问题归结为它是否实现了。
我使用 VS2012 编译了以下程序,目标是 .Net 4.6,并启用了优化。看起来编译器没有选择将不变式提升出循环:
public static void Main()
{
for( int i = 0; i < 10; i++ )
{
Console.Out.WriteLine( i );
Console.Out.WriteLine( Test.Option1.ToString() );
}
}
public enum Test
{
Option1,
Option2,
Option3
}
以下是我使用ILSpy 2.3.1获得的程序原始IL代码。请注意,在循环中间调用了ToString()
。
.method public hidebysig static
void Main () cil managed
{
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2050
// Code size 46 (0x2e)
.maxstack 2
.entrypoint
.locals init (
[0] int32 i
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_0028
// loop start (head: IL_0028)
IL_0004: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
IL_0009: ldloc.0
IL_000a: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(int32)
IL_000f: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
IL_0014: ldc.i4.0
IL_0015: box TestProject.Program/Test
---> IL_001a: callvirt instance string [mscorlib]System.Object::ToString()
IL_001f: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(string)
IL_0024: ldloc.0
IL_0025: ldc.i4.1
IL_0026: add
IL_0027: stloc.0
IL_0028: ldloc.0
IL_0029: ldc.i4.s 10
IL_002b: blt.s IL_0004
// end loop
IL_002d: ret
} // end of method Program::Main
我也很好奇运行时JIT编译器是否会提取不变量,但它似乎并没有。为了使汇编代码更清晰,我将代码更改为以下内容:
public static void Main()
{
TextWriter cons = Console.Out;
for( int i = 0; i < 10; i++ )
{
cons.WriteLine( i );
cons.WriteLine( Test.Option1.ToString() );
}
}
然后使用VS的调试器获取程序集,注意确保VS允许JITer进行优化。但它仍然没有提升对ToString()的调用:
TextWriter cons = Console.Out
00000000 push rdi
00000001 push rsi
00000002 sub rsp,28h
00000006 call 0000000050D76460
0000000b mov rsi,rax
for( int i = 0
0000000e xor edi,edi
{
cons.WriteLine( i )
00000010 mov rcx,rsi
00000013 mov edx,edi
00000015 mov rax,qword ptr [rsi]
00000018 mov rax,qword ptr [rax+60h]
0000001c call qword ptr [rax+28h]
cons.WriteLine( Test.Option1.ToString() )
0000001f mov rcx,7FE90116770h
00000029 call 000000005F6302D0
0000002e mov rcx,rsi
00000031 xor ecx,ecx
00000033 mov dword ptr [rax+8],ecx
00000036 mov rcx,rax
00000039 mov rax,qword ptr [rax]
0000003c mov rax,qword ptr [rax+40h]
00000040 call qword ptr [rax] <---- call System.Enum.ToString()
00000042 mov rdx,rax
00000045 mov rcx,rsi
00000048 mov rax,qword ptr [rsi]
0000004b mov rax,qword ptr [rax+68h]
0000004f call qword ptr [rax+20h]
for( int i = 0
00000052 inc edi
00000054 cmp edi,0Ah
00000057 jl 0000000000000010
00000059 add rsp,28h
}
}
Test.OptionOne.ToString()
是一个固定的字符串,永远不会改变。可以通过生成一个单一的 const 字符串并与之比较来进行优化,而不是在每个循环中生成一个新的字符串。 - Scott Chamberlain