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

时间:2014-08-19 10:24:30

标签: c# .net

以下代码在.NET 4.5上运行发布配置时会生成以下输出...

Without virtual: 0.333333333333333
With virtual:    0.333333343267441

(在调试中运行时,两个版本都会以0.333333343267441为结果。)

我可以看到将一个浮点除以一个short并将其返回一个double可能会在一定点之后产生垃圾。

我的问题是:当分母中提供短片的属性是虚拟还是非虚拟时,有人可以解释为什么结果会有所不同吗?

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);
    }
}

3 个答案:

答案 0 :(得分:1)

我相信詹姆斯的推测是正确的,这是一个JIT优化。 JIT在可能的情况下执行不太精确的划分,从而导致差异。以下代码示例在使用x64目标的发布模式下编译时重复您的结果,并直接从命令提示符执行。我正在使用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位浮点数(双)。事实上(几乎)相同的IL - 唯一的区别是callvirtcall指令 - 在JIT中直接导致不同的结果点。

在第三种情况下,编译器已经将除法执行为常量。对于这种情况,不执行div IL指令。

在最后一种情况下,我使用Parse操作来最小化语句优化的可能性(我说“阻止”但我对编译器正在做的事情不太了解)。此案例的结果与virtual调用的结果相匹配。似乎JIT正在优化非虚拟方法,或者它以不同的方式执行除法。

有趣的是,如果你删除了parsedThree变量并且仅针对第四种情况resultParsed = 1.0f / short.Parse("3")调用以下内容,则结果与第一种情况相同。同样,看起来JIT正在以不同的方式执行除法。

答案 1 :(得分:0)

我在.Net 4.5下测试了你的代码 在Visual Studio 2012中运行时,我总能得到相同的结果:
0.333333333333333在Rel / Dbg 32位中运行时 在Rel / Dbg 64位中运行时为0.333333343267441

我在运行exe时得到你的结果,而不是从视觉工作室的提示中启动它,只有当代码是:

  • 以64位模式运行(我在任何CPU中运行,代码是在没有Prefer 32位检查的情况下编译的)
  • in Release

优化代码选项没有任何区别。

我唯一能想到的是使用虚拟力稍后评估double类型,因此运行时使用浮点数执行1/3,然后将结果提升一倍,而不使用虚拟属性则提升操作数在进行操作之前直接加倍

答案 2 :(得分:0)

它可能是JITter优化而不是编译器优化。编译器在这里没有太多优化,但是JITter可以很容易地内联非虚拟版本并最终得到(double)1.0f / 3而不是(double)(1.0f / 3)。无论如何,您无法完全依赖浮点结果。