使用Linq.Expressions的算术计算在32对64位上产生不同的结果

时间:2015-11-20 18:26:42

标签: c# linq double expression precision

我观察到有关以下代码结果的一些奇怪行为:

namespace Test {
  class Program {
    private static readonly MethodInfo Tan = typeof(Math).GetMethod("Tan", new[] { typeof(double) });
    private static readonly MethodInfo Log = typeof(Math).GetMethod("Log", new[] { typeof(double) });

    static void Main(string[] args) {
    var c1 = 9.97601998143507984195821336470544338226318359375d;
    var c2 = -0.11209109500765944422706610339446342550218105316162109375d;

    var result1 = Math.Pow(Math.Tan(Math.Log(c1) / Math.Tan(c2)), 2);

    var p1 = Expression.Parameter(typeof(double));
    var p2 = Expression.Parameter(typeof(double));
    var expr = Expression.Power(Expression.Call(Tan, Expression.Divide(Expression.Call(Log, p1), Expression.Call(Tan, p2))), Expression.Constant(2d));
    var lambda = Expression.Lambda<Func<double, double, double>>(expr, p1, p2);
    var result2 = lambda.Compile()(c1, c2);

    var s1 = DoubleConverter.ToExactString(result1);
    var s2 = DoubleConverter.ToExactString(result2);

    Console.WriteLine("Result1: {0}", s1);
    Console.WriteLine("Result2: {0}", s2);
  }
}

为x64编译的代码给出了相同的结果:

Result1: 4888.95508254035303252749145030975341796875
Result2: 4888.95508254035303252749145030975341796875

但是当为x86或Any Cpu编译时,结果会有所不同:

Result1: 4888.95508254035303252749145030975341796875
Result2: 4888.955082542781383381225168704986572265625

为什么result1保持不变,result2取决于目标架构?有没有办法让result1result2在同一架构上保持一致?

DoubleConverter课程取自http://jonskeet.uk/csharp/DoubleConverter.cs。在您告诉我使用decimal之前,我不需要更高的精确度,我只需要将结果保持一致。目标框架是.NET 4.5.2,测试项目是在调试模式下构建的。我在Windows 10上使用Visual Studio 2015 Update 1 RC。

感谢。

修改

在用户djcouchycouch的建议下,我尝试进一步简化示例:

  var c1 = 9.97601998143507984195821336470544338226318359375d;
  var c2 = -0.11209109500765944422706610339446342550218105316162109375d;
  var result1 = Math.Log(c1) / Math.Tan(c2);
  var p1 = Expression.Parameter(typeof(double));
  var p2 = Expression.Parameter(typeof(double));
  var expr = Expression.Divide(Expression.Call(Log, p1), Expression.Call(Tan, p2));
  var lambda = Expression.Lambda<Func<double, double, double>>(expr, p1, p2);
  var result2 = lambda.Compile()(c1, c2);

x86或AnyCpu,Debug:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.434653115359243003013034467585384845733642578125

x64,调试:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

x86或AnyCpu,发布:

Result1: -20.434653115359243003013034467585384845733642578125
Result2: -20.434653115359243003013034467585384845733642578125

x64,发布:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

关键是调试,发布,x86和x64之间的结果不同,公式越复杂,它就越有可能导致更大的偏差。

1 个答案:

答案 0 :(得分:3)

ECMA-335允许这样做 I.12.1.3处理浮点数据类型

  

[...]浮点数(静态,数组元素和类的字段)的存储位置具有固定大小。支持的存储空间大小为float32float64。其他地方(在评估堆栈上,作为参数,作为返回类型和作为局部变量)浮点数使用内部浮点类型表示。在每个这样的实例中,变量或表达式的名义类型是float32float64,但其值可以在内部用额外的范围和/或精度表示。 [...]

由于@harold对您的问题发表评论,因此可以在x86模式下使用80位FPU寄存器。这是在启用优化时发生的情况,对于您的用户代码,当您在发布模式下构建并且不进行调试时,会发生这种情况,但对于已编译的表达式,始终是这样。

为了确保获得一致的舍入,您需要将中间结果存储在字段或数组中。这意味着为了可靠地获得非Expression版本的结果,您需要将其编写为:

var tmp = new double[2];
tmp[0] = Math.Log(c1);
tmp[1] = Math.Tan(c2);
tmp[0] /= tmp[1];
tmp[0] = Math.Tan(tmp[0]);
tmp[0] = Math.Pow(tmp[0], 2);

然后您可以安全地将tmp[0]分配给本地变量。

是的,这太丑了。

对于Expression版本,您需要的实际语法更糟糕,我不会写出来。它涉及Expression.Block以允许顺序执行多个不相关的子表达式,Expression.Assign以分配给数组元素或字段,以及访问那些数组元素或字段。