As has already been said, this is because of a difference between the Debug and Release modes on x86. It surfaced in your code in Debug mode, because the compiled lambda expression is always JIT compiled in Release mode.
The difference is not caused by the C# compiler. Consider the following version of your code:
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;
}
The output is 0.082907514933846516
in Debug mode and 0.082907514933846488
in Release mode, but the IL is the same for both:
.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
}
}
The difference lies in the generated machine code. Disassembly of Compute
for Debug mode is:
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
For Release mode:
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
Apart from using a function call to compute sin
instead of using fsin
directly, which doesn't seem to make a difference, the main change is that Release mode keeps the result of the sin
in the floating point register, while Debug mode writes and then reads it into memory (instructions fstp qword ptr [ebp-40h]
and fld qword ptr [ebp-40h]
). What this does is that it rounds the result of the sin
from the 80-bit precision to 64-bit precision, resulting in different values.
Curiously, the result of the same code on .Net Core (x64) is yet another value: 0.082907514933846627
. The disassembly for that case shows that it's using SSE instructions, rather than x87 (though .Net Framework x64 does the same, so the difference is going to be in the called function):
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