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

Arithmetic calculation using Linq.Expressions yields different results on 32 vs 64bit

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

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 在同一架构上保持相同?

DoubleConverterclass取自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 或任何 Cpu,调试:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.434653115359243003013034467585384845733642578125

x64,调试:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

x86 或任意Cpu,版本:

Result1: -20.434653115359243003013034467585384845733642578125
Result2: -20.434653115359243003013034467585384845733642578125

x64,版本:

Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875

关键是Debug、Release、x86、x64的结果都不一样,公式越复杂越容易造成较大的偏差。

这是 ECMA-335 允许的 I.12.1.3 浮点数据类型的处理:

[...] Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type. In each such instance, the nominal type of the variable or expression is either float32 or float64, but its value can be represented internally with additional range and/or precision. [...]

正如@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 to allow multiple unrelated sub-expressions to be executed sequentially, Expression.Assign分配给数组元素或字段,以及访问这些数组元素或字段。