我观察到有关以下代码结果的一些奇怪行为:
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
课程取自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之间的结果不同,公式越复杂,它就越有可能导致更大的偏差。
答案 0 :(得分:3)
ECMA-335允许这样做 I.12.1.3处理浮点数据类型:
[...]浮点数(静态,数组元素和类的字段)的存储位置具有固定大小。支持的存储空间大小为
float32
和float64
。其他地方(在评估堆栈上,作为参数,作为返回类型和作为局部变量)浮点数使用内部浮点类型表示。在每个这样的实例中,变量或表达式的名义类型是float32
或float64
,但其值可以在内部用额外的范围和/或精度表示。 [...]
由于@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
以分配给数组元素或字段,以及访问那些数组元素或字段。