使用 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
取决于目标架构?有什么方法可以使 result1
和 result2
在同一架构上保持相同?
DoubleConverter
class取自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
分配给数组元素或字段,以及访问这些数组元素或字段。
我观察到关于以下代码结果的一些奇怪行为:
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
取决于目标架构?有什么方法可以使 result1
和 result2
在同一架构上保持相同?
DoubleConverter
class取自http://jonskeet.uk/csharp/DoubleConverter.cs。在你告诉我使用 decimal
之前,我不需要更高的精度,我只需要结果一致。目标框架是 .NET 4.5.2,测试项目是在调试模式下构建的。我在 Windows 10.
谢谢。
编辑
根据用户 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
andfloat64
. 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 eitherfloat32
orfloat64
, 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
分配给数组元素或字段,以及访问这些数组元素或字段。