【问题标题】:Why do I get different results using a virtual or non-virtual property?为什么我使用虚拟或非虚拟属性会得到不同的结果?
【发布时间】:2014-08-19 10:24:30
【问题描述】:

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

Without virtual: 0.333333333333333
With virtual:    0.333333343267441

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

我可以看到,将 float 除以 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);
    }
}

【问题讨论】:

标签: c# .net


【解决方案1】:

我相信 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 的事实——唯一的区别是 callvirtcall 指令——导致不同的结果直接指向 JIT。

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

在最后一种情况下,我使用Parse 操作来最大程度地减少语句被优化的机会(我会说“防止”,但我对编译器正在做什么知之甚少)。这种情况的结果与virtual 调用的结果相匹配。似乎 JIT 要么优化掉非虚拟方法,要么以不同的方式执行除法。

有趣的是,如果你消除了parsedThree 变量并简单地为第四种情况调用以下代码resultParsed = 1.0f / short.Parse("3"),结果与第一种情况相同。同样,JIT 似乎正在以不同的方式执行除法。

【讨论】:

    【解决方案2】:

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

    我在运行 exe 时得到你的结果,而无需从 Visual Studio 的提示符启动它,并且只有代码是:

    • 在 64 位模式下运行(我在任何 CPU 中运行,并且代码是在没有首选 32 位检查的情况下编译的)
    • 发布中

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

    我唯一能想到的是,使用 virtual 会强制稍后对 double 类型进行评估,因此运行时使用浮点数执行 1/3,然后将结果提升为 double,而当不使用 virtual 属性时,它会提升操作数在做操作之前直接加倍

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-12-28
        • 1970-01-01
        • 2015-08-15
        • 1970-01-01
        • 2010-09-24
        • 2016-10-26
        相关资源
        最近更新 更多