C# 使用Linq.表达式的算术计算在32位和64位上产生不同的结果
我观察到关于以下代码结果的一些奇怪行为:C# 使用Linq.表达式的算术计算在32位和64位上产生不同的结果,c#,linq,double,expression,precision,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(do
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);
}
}
但在为x86或任何Cpu编译时,结果会有所不同:
Result1: 4888.95508254035303252749145030975341796875
Result2: 4888.955082542781383381225168704986572265625
为什么result1
保持不变,而result2
则取决于目标体系结构?有没有办法使result1
和result2
在同一架构上保持不变
DoubleConverter
类取自。在你告诉我使用十进制之前,我不需要更高的精度,我只需要结果保持一致。目标框架是.NET4.5.2,测试项目是在调试模式下构建的。我正在Windows 10上使用Visual Studio 2015 Update 1 RC
谢谢
编辑
根据用户的建议,我尝试进一步简化示例:
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);
x64,调试:
Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875
x86或任意CPU,版本:
Result1: -20.434653115359243003013034467585384845733642578125
Result2: -20.434653115359243003013034467585384845733642578125
Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875
x64,发布:
Result1: -20.434653115359243003013034467585384845733642578125
Result2: -20.434653115359243003013034467585384845733642578125
Result1: -20.43465311535924655572671326808631420135498046875
Result2: -20.43465311535924655572671326808631420135498046875
关键是调试、发布、x86和x64的结果各不相同,公式越复杂,越有可能导致更大的偏差。这是ECMA-335I.12.1.3浮点数据类型处理所允许的: […]浮点数(静态、数组元素和类字段)的存储位置大小固定。支持的存储大小为
float32
和float64
。在其他任何地方(在计算堆栈上,作为参数、返回类型和局部变量),浮点数都使用内部浮点数类型表示。在每个这样的实例中,变量或表达式的标称类型为float32
或float64
,但其值可以在内部用额外的范围和/或精度表示。[……]
正如@harold对您的问题的评论,这允许在x86模式下使用80位FPU寄存器。这就是启用优化时发生的情况,这意味着对于用户代码,当您在发布模式下构建并且不进行调试时,但对于编译的表达式,总是这样
为了确保获得一致的舍入,需要将中间结果存储在字段或数组中。这意味着为了可靠地获得非表达式
版本的结果,您需要将其编写为:
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]
赋值给局部变量
是的,很难看
对于
表达式
版本,您需要的实际语法更糟糕,我不会写出来。它包括允许顺序执行多个不相关的子表达式,分配给数组元素或字段,以及访问这些数组元素或字段。我无法在VS2012(Windows 7 SP1)上重现此问题。这两种架构都产生了相同的结果。我在Windows7,VS2015 x86/任何CPU上看到了与OP相同的结果。在Windows7,VS2015上重现。结果取决于附加的配置和调试器。在发布模式下,使用调试器(F5)和不使用调试器(Ctrl+F5)运行时,结果不同。32位JIT使用x87指令和80位临时结果,64位JIT不使用。但对于x87指令,如果它将临时内存保存到内存中,它将再次被截断为64位。因此,有时会有所不同,但不一定,这取决于确切的codegen。我认为Eric Lippert对以下问题的反应:“C#编译器、jitter和运行时都有广泛的用途,可以在任何时候、一时兴起的情况下为您提供比规范要求的更精确的结果——它们不需要一致地选择这样做,事实上也不需要。”当JIT升级(如果有)以优化内存访问时(可耻的是,现在它几乎从来没有这样做过)这将导致中断。或者,数组访问是否会根据规范强制精确?我认为对float/double进行冗余转换也会起作用。@usr规范,我引用的部分,说字段和数组元素可能没有额外的精度。不需要强制转换来删除多余的精度。好的,这样就可以了。也可以使用结构{float F;}
和一个通过该结构的助手方法。谢谢!非常有用。