为什么使用虚拟或非虚拟属性会得到不同的结果?

本文关键字:虚拟 结果 属性 为什么 | 更新日期: 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——唯一的区别是callvirtcall指令——导致不同的结果直接指向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而当不使用虚拟属性时,它在执行

操作之前直接将操作数提升为double

这可能是抖动优化而不是编译器优化。这里没有太多需要编译器优化的地方,但是JITter可以很容易地内联非虚拟版本,最终得到(double)1.0f/3而不是(double)(1.0f/3)。无论如何,你不能指望浮点数的结果就是你所期望的。