编译后的表达式树给出了与等效代码不同的结果

问题描述 投票:6回答:2

以下代码:

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模式下正常工作。

c# .net expression expression-trees
2个回答
5
投票

我不是这方面的专家,但我会对此发表看法。

首先,只有在使用调试标志进行编译时才会出现问题(在发布模式下它不会出现),并且实际上只有在运行为x86时才出现。

如果我们反编译表达式编译的方法,我们将看到这个(在调试和发布中):

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 

但是,如果我们查看在调试模式下编译的类似方法的IL代码,我们将看到:

.locals init (
  [0] float64 V_0
)
IL_0001: ldc.r8       182273
IL_000a: call         float64 [mscorlib]System.Math::Sin(float64)
IL_000f: ldc.r8       0.888
IL_0018: add          
IL_0019: stloc.0      // save to local
IL_001a: br.s         IL_001c // basically nop
IL_001c: ldloc.0      // V_0 // pop from local to stack
IL_001d: ret          // return

您会看到编译器将(不必要的)结果保存并加载到本地变量(可能用于调试目的)。现在我不确定,但据我所知,在x86架构上,双值可能存储在80位CPU寄存器中(引自here):

默认情况下,在x86体系结构的代码中,编译器使用协处理器的80位寄存器来保存浮点计算的中间结果。这会提高程序速度并减少程序大小。但是,由于计算涉及在内存中表示的浮点数据类型少于80位,因此通过冗长的计算可以产生精度为80位的额外位减去较小浮点类型中的位数。结果不一致。

所以我的猜测是这个本地存储和从本地加载导致从64位转换到80位(因为寄存器)和返回,这会导致你观察到的行为。

另一种解释可能是JIT在调试和释放模式之间表现不同(可能仍然与将中间计算结果存储在80位寄存器中有关)。

希望有些知道更多的人可以确认我是否正确。

更新以回应评论。反编译表达式的一种方法是创建动态程序集,将表达式编译到那里的方法,保存到磁盘,然后查看任何反编译器(我使用JetBrains DotPeek)。例:

 var asm = AppDomain.CurrentDomain.DefineDynamicAssembly(
     new AssemblyName("dynamic_asm"),
     AssemblyBuilderAccess.Save);

 var module = asm.DefineDynamicModule("dynamic_mod", "dynamic_asm.dll");
 var type = module.DefineType("DynamicType");
 var method = type.DefineMethod(
     "DynamicMethod", MethodAttributes.Public | MethodAttributes.Static);
 Expression.Lambda<Func<double>>(sum).CompileToMethod(method);
 type.CreateType();
 asm.Save("dynamic_asm.dll");

3
投票

正如已经说过的那样,这是因为x86上的Debug和Release模式之间存在差异。它在您的代码中以调试模式浮出水面,因为已编译的lambda表达式始终在Release模式下进行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模式将sin的结果保存在浮点寄存器中,而Debug模式写入然后将其读入内存(说明fstp qword ptr [ebp-40h]fld qword ptr [ebp-40h])。这样做是因为它将sin的结果从80位精度舍入到64位精度,从而产生不同的值。

奇怪的是,.Net Core(x64)上相同代码的结果是另一个值:0.082907514933846627。该案例的反汇编显示它使用的是SSE指令,而不是x87(尽管.Net Framework x64也是如此,因此区别在于被调用的函数):

00007FFD5C180B80  sub         rsp,28h  
00007FFD5C180B84  movsd       xmm0,mmword ptr [7FFD5C180BA0h]  
00007FFD5C180B8C  call        00007FFDBBEC1C30  
00007FFD5C180B91  addsd       xmm0,mmword ptr [7FFD5C180BA8h]  
00007FFD5C180B99  add         rsp,28h  
00007FFD5C180B9D  ret  
© www.soinside.com 2019 - 2024. All rights reserved.