As a sidenote there was a change in how sin/cos/tan are calculated .NET. It happened on 2 Jun 2016, with this commit on .NET Core. As the author wrote in floatdouble.cpp:
Sin, Cos, and Tan on AMD64 Windows were previously implemented in vm\amd64\JitHelpers_Fast.asm
by calling x87 floating point code (fsin, fcos, fptan) because the CRT helpers were too slow. This
is no longer the case and the CRT call is used on all platforms.
Note that the CRT is the C Language Runtime. This explains why newer versions of .NET Core give the same result as C++ when used on the same platform. They are using the same CRT other C++ programs are using.
The last version of the "old" code for those interested: 1 and 2.
It is unclear if/when .NET Framework inherited these changes. From some tests it seems that
.NET Core >= 2.0 (haven't tested previous versions): Math.Sin(6.2831853071795856E+45) == 0.824816390616968
.NET Framework 4.8 (32 and 64 bits): Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45
so it didn't inherit them.
Funny addendum
Made some checks, and Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45
is the "official" answer of the fsin
opcode of x87 assembly, so it is the "official" answer of Intel (about 1980) about how much is the sin of 6.2831853071795856E+45, so if you use an Intel, you must trust that Math.Sin(6.2831853071795856E+45) == 6.28318530717959E+45
, otherwise you are a traitor!
If you want to doublecheck:
public static class TrigAsm
{
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
private static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, out uint lpflOldProtect);
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(IntPtr lpAddress, IntPtr dwSize, uint dwFreeType);
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_EXECUTE = 0x10;
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RELEASE = 0x8000;
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate double Double2Double(double d);
public static readonly Double2Double Sin;
static TrigAsm()
{
// Opcoes generated with https://defuse.ca/online-x86-assembler.htm
byte[] body = Environment.Is64BitProcess ?
new byte[]
{
0xF2, 0x0F, 0x11, 0x44, 0x24, 0x08, // movsd QWORD PTR [rsp+0x8],xmm0
0xDD, 0x44, 0x24, 0x08, // fld QWORD PTR [rsp+0x8]
0xD9, 0xFE, // fsin
0xDD, 0x5C, 0x24, 0x08, // fstp QWORD PTR [rsp+0x8]
0xF2, 0x0F, 0x10, 0x44, 0x24, 0x08, // movsd xmm0,QWORD PTR [rsp+0x8]
0xC3, // ret
} :
new byte[]
{
0xDD, 0x44, 0x24, 0x04, // fld QWORD PTR [esp+0x4]
0xD9, 0xFE, // fsin
0xC2, 0x08, 0x00, // ret 0x8
};
IntPtr buf = IntPtr.Zero;
try
{
// We VirtualAlloc body.Length bytes, with R/W access
// Note that from what I've read, MEM_RESERVE is useless
// if the first parameter is IntPtr.Zero
buf = VirtualAlloc(IntPtr.Zero, (IntPtr)body.Length, MEM_COMMIT, PAGE_READWRITE);
if (buf == IntPtr.Zero)
{
throw new Win32Exception();
}
// Copy our instructions in the buf
Marshal.Copy(body, 0, buf, body.Length);
// Change the access of the allocated memory from R/W to Execute
uint oldProtection;
bool result = VirtualProtect(buf, (IntPtr)body.Length, PAGE_EXECUTE, out oldProtection);
if (!result)
{
throw new Win32Exception();
}
// Create a delegate to the "function"
Sin = (Double2Double)Marshal.GetDelegateForFunctionPointer(buf, typeof(Double2Double));
buf = IntPtr.Zero;
}
finally
{
// There was an error!
if (buf != IntPtr.Zero)
{
// Free the allocated memory
bool result = VirtualFree(buf, IntPtr.Zero, MEM_RELEASE);
if (!result)
{
throw new Win32Exception();
}
}
}
}
}
In .NET you can't have inline assembly (and the inline assembler of Visual Studio is 32 bits only). I solved it putting the assembly opcodes in a block of memory and marking the memory as executable.
Post scriptum: note that no-one uses anymore the x87 instruction set, because it is slow.