为什么使用虚拟或非虚拟属性会得到不同的结果?
本文关键字:虚拟 结果 属性 为什么 | 更新日期: 2023-09-27 18:18:50
下面的代码,当在。net 4.5上运行一个发布配置时,产生以下输出…
Without virtual: 0.333333333333333
With virtual: 0.333333343267441
(当在调试中运行时,两个版本的结果都是0.333333343267441
)
我可以看到,将float除以short并以double类型返回,很可能在某个a点之后产生垃圾。
我的问题是:有人能解释为什么当提供分母中的short的属性是虚的或非虚的时,结果是不同的吗?
public class ProvideThreeVirtually
{
public virtual short Three { get { return 3; } }
}
public class GetThreeVirtually
{
public double OneThird(ProvideThreeVirtually provideThree)
{
return 1.0f / provideThree.Three;
}
}
public class ProvideThree
{
public short Three { get { return 3; } }
}
public class GetThree
{
public double OneThird(ProvideThree provideThree)
{
return 1.0f / provideThree.Three;
}
}
class Program
{
static void Main()
{
var getThree = new GetThree();
var result = getThree.OneThird(new ProvideThree());
Console.WriteLine("Without virtual: {0}", result);
var getThreeVirtually = new GetThreeVirtually();
var resultV = getThreeVirtually.OneThird(new ProvideThreeVirtually());
Console.WriteLine("With virtual: {0}", resultV);
}
}
我相信James的猜想是正确的,这是一个JIT优化。JIT在可能的情况下执行不太精确的除法,从而导致差异。下面的代码示例在使用x64目标在Release模式下编译并直接从命令提示符执行时复制了您的结果。我使用的是Visual Studio 2008和。NET 3.5。
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位浮点数(double)。(几乎)相同的IL——唯一的区别是callvirt
和call
指令——导致不同的结果直接指向JIT。
在第三种情况下,编译器已经将其除法为一个常量。在这种情况下,div
IL指令不执行。
在最后一种情况下,我使用Parse
操作来最小化语句得到优化的机会(我想说"防止",但我不知道编译器在做什么)。这种情况下的结果与virtual
调用的结果相匹配。看起来JIT要么在优化非虚拟方法,要么在以不同的方式执行除法。
parsedThree
变量并简单地对第四种情况resultParsed = 1.0f / short.Parse("3")
调用以下代码,那么结果与第一种情况相同。同样,JIT执行除法的方式似乎有所不同。
我已经在。net 4.5下测试了你的代码
在Visual Studio 2012中运行时,我总是得到相同的结果:
0.33333333333333333当运行在Rel/Dbg 32位
0.333333343267441运行在Rel/Dbg 64位
当运行exe而不从没有visual studio的提示符启动它时,只有当代码是:
- 在64位模式下运行(我在任何CPU上运行,代码编译时没有首选32位检查) 在发布
优化代码选项没有任何区别。
我能想到的唯一一件事是使用虚拟强制对double类型进行稍后的评估所以运行时使用浮点数进行1/3然后将结果提升为double而当不使用虚拟属性时,它在执行
这可能是抖动优化而不是编译器优化。这里没有太多需要编译器优化的地方,但是JITter可以很容易地内联非虚拟版本,最终得到(double)1.0f/3而不是(double)(1.0f/3)。无论如何,你不能指望浮点数的结果就是你所期望的。