以下代码:
double c1 = 182273d;
double c2 = 0.888d;
Expression c1e = Expression.Constant(c1, typeof(double));
Expression c2e = Expression.Constant(c2, typeof(double));
Expression<Func<double, double>> sinee = a => Math.Sin(a);
Expression sine = ((MethodCallExpression)sinee.Body).Update(null, new[] { c1e });
Expression sum = Expression.Add(sine, c2e);
Func<double> f = Expression.Lambda<Func<double>>(sum).Compile();
double r = f();
double rr = Math.Sin(c1) + c2;
Console.WriteLine(r.ToString("R"));
Console.WriteLine(rr.ToString("R"));
将输出:
0.082907514933846488
0.082907514933846516
为什么r和rr不同?
更新:
发现如果要选择“x86”平台目标或使用“任何CPU”检查“首选32位”,则会重现此操作。在64x模式下正常工作。
答案 0 :(得分:5)
我不是这方面的专家,但我会对此发表看法。
首先,只有在使用 debug 标志进行编译时才会出现问题(在发布模式下它不会出现),实际上只有在运行为x86时才出现。
如果我们反编译你的表达式编译的方法,我们将看到这个(在调试和发布中):
LastRow = ws2.Range("A" & ws2.Rows2.Count).End(xlUp).row + 1
但是,如果我们查看在调试模式下编译的类似方法的IL代码,我们将看到:
<div id="checkboxes">
<input type="checkbox" name="qcol2" value="1" > Sarah
<br />
<input type="checkbox" name="qcol2" value="9" > Sundae
<br />
<input type="checkbox" name="qcol2" value="10" > Summer
<br />
<input type="checkbox" name="qcol2" value="11" > GeeZee
<br />
<input type="checkbox" name="qcol2" value="12" > Husband
<br />
</div>
$(document).ready(function() {
var initValues = [{my_id: "10"}, {my_id: "11"}, {my_id: "12" }];
var myjson = JSON.stringify(initValues);
//alert(myjson);
$('#checkboxes').find(':checkbox[name^="qcol2"]').each(function() {
$(this).prop("checked", ($.inArray($(this).val(), myjson) != -1));
//alert(myjson);
});
});
您看到编译器将(不必要的)结果保存并加载到本地变量(可能用于调试目的)。现在我不确定,但据我所知,在x86架构上,双值可能存储在 80位 CPU寄存器中(引自here):< / p>
默认情况下,在x86架构的代码中,编译器使用 协处理器的80位寄存器用于保存中间结果 浮点计算。这提高了程序速度和 减少程序大小。但是,因为计算涉及 内存中表示的浮点数据类型小于 80位,携带额外的精度位80位减去数字 通过冗长的计算,在较小的浮点类型中的位数 会产生不一致的结果。
所以我的猜测是这个存储到本地并从本地加载会导致从64位转换到80位(因为寄存器)和返回,这会导致你观察到的行为。
另一种解释可能是JIT在调试和释放模式之间表现不同(可能仍然与将中间计算结果存储在80位寄存器中有关)。
希望有些知道更多的人可以确认我是否正确。
更新以回应评论。反编译表达式的一种方法是创建动态程序集,将表达式编译到那里的方法,保存到磁盘,然后查看任何反编译器(我使用JetBrains DotPeek)。例如:
IL_0000: ldc.r8 182273 // push first value
IL_0009: call float64 [mscorlib]System.Math::Sin(float64) // call Math.Sin()
IL_000e: ldc.r8 0.888 // push second value
IL_0017: add // add
IL_0018: ret
答案 1 :(得分:3)
正如已经说过的,这是因为x86上的Debug和Release模式之间存在差异。它在您的代码中以调试模式浮出水面,因为编译的lambda表达式总是在发布模式下进行JIT编译。
差异不是由C#编译器引起的。请考虑以下版本的代码:
using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main() => Console.WriteLine(Compute().ToString("R"));
[MethodImpl(MethodImplOptions.NoInlining)]
static double Compute() => Math.Sin(182273d) + 0.888d;
}
在调试模式下输出为0.082907514933846516
,在释放模式下为0.082907514933846488
,但两者的IL相同:
.class private abstract sealed auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init ([0] float64 V_0)
IL_0000: call float64 Program::Compute()
IL_0005: stloc.0 // V_0
IL_0006: ldloca.s V_0
IL_0008: ldstr "R"
IL_000d: call instance string [mscorlib]System.Double::ToString(string)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
IL_0017: ret
}
.method private hidebysig static float64 Compute() cil managed noinlining
{
.maxstack 8
IL_0000: ldc.r8 182273
IL_0009: call float64 [mscorlib]System.Math::Sin(float64)
IL_000e: ldc.r8 0.888
IL_0017: add
IL_0018: ret
}
}
不同之处在于生成的机器代码。调试模式的Compute
的反汇编是:
012E04B2 in al,dx
012E04B3 push edi
012E04B4 push esi
012E04B5 push ebx
012E04B6 sub esp,34h
012E04B9 xor ebx,ebx
012E04BB mov dword ptr [ebp-10h],ebx
012E04BE mov dword ptr [ebp-1Ch],ebx
012E04C1 cmp dword ptr ds:[1284288h],0
012E04C8 je 012E04CF
012E04CA call 71A96150
012E04CF fld qword ptr ds:[12E04F8h]
012E04D5 sub esp,8
012E04D8 fstp qword ptr [esp]
012E04DB call 71C87C80
012E04E0 fstp qword ptr [ebp-40h]
012E04E3 fld qword ptr [ebp-40h]
012E04E6 fadd qword ptr ds:[12E0500h]
012E04EC lea esp,[ebp-0Ch]
012E04EF pop ebx
012E04F0 pop esi
012E04F1 pop edi
012E04F2 pop ebp
012E04F3 ret
对于发布模式:
00C204A0 push ebp
00C204A1 mov ebp,esp
00C204A3 fld dword ptr ds:[0C204B8h]
00C204A9 fsin
00C204AB fadd qword ptr ds:[0C204C0h]
00C204B1 pop ebp
00C204B2 ret
除了使用函数调用来计算sin
而不是直接使用fsin
,这似乎没有什么区别,主要的变化是Release模式保留了{{{ 1}}在浮点寄存器中,而调试模式写入然后将其读入存储器(指令sin
和fstp qword ptr [ebp-40h]
)。这样做的是它将fld qword ptr [ebp-40h]
的结果从80位精度四舍五入到64位精度,从而产生不同的值。
奇怪的是,.Net Core(x64)上相同代码的结果是另一个值:sin
。该案例的反汇编显示它使用的是SSE指令,而不是x87(尽管.Net Framework x64也是如此,因此区别在于被调用的函数):
0.082907514933846627