我相信 James 的猜想是正确的,这是一个 JIT 优化。 JIT 在可能的情况下执行的除法精度较低,这会导致差异。以下代码示例复制了您在发布模式下使用 x64 目标编译并直接从命令提示符执行时的结果。我正在使用带有 NET 3.5 的 Visual Studio 2008。
public static void Main()
{
double result = 1.0f / new ProvideThree().Three;
double resultVirtual = 1.0f / new ProvideVirtualThree().Three;
double resultConstant = 1.0f / 3;
short parsedThree = short.Parse("3");
double resultParsed = 1.0f / parsedThree;
Console.WriteLine("Result of 1.0f / ProvideThree = {0}", result);
Console.WriteLine("Result of 1.0f / ProvideVirtualThree = {0}", resultVirtual);
Console.WriteLine("Result of 1.0f / 3 = {0}", resultConstant);
Console.WriteLine("Result of 1.0f / parsedThree = {0}", resultParsed);
Console.ReadLine();
}
public class ProvideThree
{
public short Three
{
get { return 3; }
}
}
public class ProvideVirtualThree
{
public virtual short Three
{
get { return 3; }
}
}
结果如下:
Result of 1.0f / ProvideThree = 0.333333333333333
Result of 1.0f / ProvideVirtualThree = 0.333333343267441
Result of 1.0f / 3 = 0.333333333333333
Result of 1.0f / parsedThree = 0.333333343267441
IL 相当简单:
.locals init ([0] float64 result,
[1] float64 resultVirtual,
[2] float64 resultConstant,
[3] int16 parsedThree,
[4] float64 resultParsed)
IL_0000: ldc.r4 1. // push 1 onto stack as 32-bit float
IL_0005: newobj instance void Romeo.Program/ProvideThree::.ctor()
IL_000a: call instance int16 Romeo.Program/ProvideThree::get_Three()
IL_000f: conv.r4 // convert result of method to 32-bit float
IL_0010: div
IL_0011: conv.r8 // convert result of division to 64-bit float (double)
IL_0012: stloc.0
IL_0013: ldc.r4 1. // push 1 onto stack as 32-bit float
IL_0018: newobj instance void Romeo.Program/ProvideVirtualThree::.ctor()
IL_001d: callvirt instance int16 Romeo.Program/ProvideVirtualThree::get_Three()
IL_0022: conv.r4 // convert result of method to 32-bit float
IL_0023: div
IL_0024: conv.r8 // convert result of division to 64-bit float (double)
IL_0025: stloc.1
IL_0026: ldc.r8 0.33333333333333331 // constant folding
IL_002f: stloc.2
IL_0030: ldstr "3"
IL_0035: call int16 [mscorlib]System.Int16::Parse(string)
IL_003a: stloc.3 // store result of parse in parsedThree
IL_003b: ldc.r4 1.
IL_0040: ldloc.3
IL_0041: conv.r4 // convert result of parse to 32-bit float
IL_0042: div
IL_0043: conv.r8 // convert result of division to 64-bit float (double)
IL_0044: stloc.s resultParsed
前两种情况几乎相同。 IL 首先将 1 作为 32 位浮点数压入堆栈,从两种方法之一获得 3,将 3 转换为 32 位浮点数,执行除法,然后将结果转换为 64 位浮点数(双倍的)。 (几乎)相同的 IL 的事实——唯一的区别是 callvirt 与 call 指令——导致不同的结果直接指向 JIT。
在第三种情况下,编译器已经将除法执行为常量。在这种情况下不会执行div IL 指令。
在最后一种情况下,我使用Parse 操作来最大程度地减少语句被优化的机会(我会说“防止”,但我对编译器正在做什么知之甚少)。这种情况的结果与virtual 调用的结果相匹配。似乎 JIT 要么优化掉非虚拟方法,要么以不同的方式执行除法。
有趣的是,如果你消除了parsedThree 变量并简单地为第四种情况调用以下代码resultParsed = 1.0f / short.Parse("3"),结果与第一种情况相同。同样,JIT 似乎正在以不同的方式执行除法。