Version Used:
3.3.0-beta2-19374-02
Steps to Reproduce:
Test program:
using System;
namespace HelloWorld
{
class Program
{
static double nan = double.NaN;
static float nanf = float.NaN;
static double ninf = double.NegativeInfinity;
static float ninff = float.NegativeInfinity;
static double inf = double.PositiveInfinity;
static float inff = float.PositiveInfinity;
static void Main(string[] args)
{
Console.WriteLine(unchecked((ulong)double.NaN));
Console.WriteLine(unchecked((ulong)nan));
Console.WriteLine(unchecked((uint)double.NaN));
Console.WriteLine(unchecked((uint)nan));
Console.WriteLine(unchecked((ulong)float.NaN));
Console.WriteLine(unchecked((ulong)nanf));
Console.WriteLine(unchecked((uint)float.NaN));
Console.WriteLine(unchecked((uint)nanf));
Console.WriteLine(unchecked((ulong)double.NegativeInfinity));
Console.WriteLine(unchecked((ulong)ninf));
Console.WriteLine(unchecked((uint)double.NegativeInfinity));
Console.WriteLine(unchecked((uint)ninf));
Console.WriteLine(unchecked((ulong)float.NegativeInfinity));
Console.WriteLine(unchecked((ulong)ninff));
Console.WriteLine(unchecked((uint)float.NegativeInfinity));
Console.WriteLine(unchecked((uint)ninff));
Console.WriteLine(unchecked((ulong)double.PositiveInfinity));
Console.WriteLine(unchecked((ulong)inf));
Console.WriteLine(unchecked((uint)double.PositiveInfinity));
Console.WriteLine(unchecked((uint)inf));
Console.WriteLine(unchecked((ulong)float.PositiveInfinity));
Console.WriteLine(unchecked((ulong)inff));
Console.WriteLine(unchecked((uint)float.PositiveInfinity));
Console.WriteLine(unchecked((uint)inff));
}
}
}
Expected Behavior:
Identical IL is produced.
Actual Behavior:
(ulong)double.PositiveInfinity is constant folded to 0 on x64 host. On ARM host it is folded to ulong.MaxValue. Similarly for all the other variations of PositiveInfinity.
While the actual behavior for this conversion in C# may be undefined this causes some hard to debug issues. When program is compiled and run on the same architecture the values are self-consistent. However, when the program is compiled on x64 and run on ARM64 it produces inconsistent results.
This was hit in practice with CoreFX tests for System.Linq.Expressions. The tests are compiled on x64 and consumed in binary form for CoreCLR and Mono tests. Cross-reference: https://github.com/mono/mono/pull/15871
x64 code:
IL_0000: ldc.i4.0
IL_0001: conv.i8
IL_0002: call void [System.Console]System.Console::WriteLine(uint64)
IL_0007: ldsfld float64 HelloWorld.Program::nan
IL_000c: conv.u8
IL_000d: call void [System.Console]System.Console::WriteLine(uint64)
IL_0012: ldc.i4.0
IL_0013: call void [System.Console]System.Console::WriteLine(uint32)
IL_0018: ldsfld float64 HelloWorld.Program::nan
IL_001d: conv.u4
IL_001e: call void [System.Console]System.Console::WriteLine(uint32)
IL_0023: ldc.i4.0
IL_0024: conv.i8
IL_0025: call void [System.Console]System.Console::WriteLine(uint64)
IL_002a: ldsfld float32 HelloWorld.Program::nanf
IL_002f: conv.u8
IL_0030: call void [System.Console]System.Console::WriteLine(uint64)
IL_0035: ldc.i4.0
IL_0036: call void [System.Console]System.Console::WriteLine(uint32)
IL_003b: ldsfld float32 HelloWorld.Program::nanf
IL_0040: conv.u4
IL_0041: call void [System.Console]System.Console::WriteLine(uint32)
IL_0046: ldc.i8 -9223372036854775808
IL_004f: call void [System.Console]System.Console::WriteLine(uint64)
IL_0054: ldsfld float64 HelloWorld.Program::ninf
IL_0059: conv.u8
IL_005a: call void [System.Console]System.Console::WriteLine(uint64)
IL_005f: ldc.i4.0
IL_0060: call void [System.Console]System.Console::WriteLine(uint32)
IL_0065: ldsfld float64 HelloWorld.Program::ninf
IL_006a: conv.u4
IL_006b: call void [System.Console]System.Console::WriteLine(uint32)
IL_0070: ldc.i8 -9223372036854775808
IL_0079: call void [System.Console]System.Console::WriteLine(uint64)
IL_007e: ldsfld float32 HelloWorld.Program::ninff
IL_0083: conv.u8
IL_0084: call void [System.Console]System.Console::WriteLine(uint64)
IL_0089: ldc.i4.0
IL_008a: call void [System.Console]System.Console::WriteLine(uint32)
IL_008f: ldsfld float32 HelloWorld.Program::ninff
IL_0094: conv.u4
IL_0095: call void [System.Console]System.Console::WriteLine(uint32)
IL_009a: ldc.i4.0
IL_009b: conv.i8
IL_009c: call void [System.Console]System.Console::WriteLine(uint64)
IL_00a1: ldsfld float64 HelloWorld.Program::inf
IL_00a6: conv.u8
IL_00a7: call void [System.Console]System.Console::WriteLine(uint64)
IL_00ac: ldc.i4.0
IL_00ad: call void [System.Console]System.Console::WriteLine(uint32)
IL_00b2: ldsfld float64 HelloWorld.Program::inf
IL_00b7: conv.u4
IL_00b8: call void [System.Console]System.Console::WriteLine(uint32)
IL_00bd: ldc.i4.0
IL_00be: conv.i8
IL_00bf: call void [System.Console]System.Console::WriteLine(uint64)
IL_00c4: ldsfld float32 HelloWorld.Program::inff
IL_00c9: conv.u8
IL_00ca: call void [System.Console]System.Console::WriteLine(uint64)
IL_00cf: ldc.i4.0
IL_00d0: call void [System.Console]System.Console::WriteLine(uint32)
IL_00d5: ldsfld float32 HelloWorld.Program::inff
IL_00da: conv.u4
IL_00db: call void [System.Console]System.Console::WriteLine(uint32)
IL_00e0: ret
ARM64 code:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: conv.i8
IL_0003: call void [System.Console]System.Console::WriteLine(uint64)
IL_0008: nop
IL_0009: ldsfld float64 HelloWorld.Program::nan
IL_000e: conv.u8
IL_000f: call void [System.Console]System.Console::WriteLine(uint64)
IL_0014: nop
IL_0015: ldc.i4.0
IL_0016: call void [System.Console]System.Console::WriteLine(uint32)
IL_001b: nop
IL_001c: ldsfld float64 HelloWorld.Program::nan
IL_0021: conv.u4
IL_0022: call void [System.Console]System.Console::WriteLine(uint32)
IL_0027: nop
IL_0028: ldc.i4.0
IL_0029: conv.i8
IL_002a: call void [System.Console]System.Console::WriteLine(uint64)
IL_002f: nop
IL_0030: ldsfld float32 HelloWorld.Program::nanf
IL_0035: conv.u8
IL_0036: call void [System.Console]System.Console::WriteLine(uint64)
IL_003b: nop
IL_003c: ldc.i4.0
IL_003d: call void [System.Console]System.Console::WriteLine(uint32)
IL_0042: nop
IL_0043: ldsfld float32 HelloWorld.Program::nanf
IL_0048: conv.u4
IL_0049: call void [System.Console]System.Console::WriteLine(uint32)
IL_004e: nop
IL_004f: ldc.i4.0
IL_0050: conv.i8
IL_0051: call void [System.Console]System.Console::WriteLine(uint64)
IL_0056: nop
IL_0057: ldsfld float64 HelloWorld.Program::ninf
IL_005c: conv.u8
IL_005d: call void [System.Console]System.Console::WriteLine(uint64)
IL_0062: nop
IL_0063: ldc.i4.0
IL_0064: call void [System.Console]System.Console::WriteLine(uint32)
IL_0069: nop
IL_006a: ldsfld float64 HelloWorld.Program::ninf
IL_006f: conv.u4
IL_0070: call void [System.Console]System.Console::WriteLine(uint32)
IL_0075: nop
IL_0076: ldc.i4.0
IL_0077: conv.i8
IL_0078: call void [System.Console]System.Console::WriteLine(uint64)
IL_007d: nop
IL_007e: ldsfld float32 HelloWorld.Program::ninff
IL_0083: conv.u8
IL_0084: call void [System.Console]System.Console::WriteLine(uint64)
IL_0089: nop
IL_008a: ldc.i4.0
IL_008b: call void [System.Console]System.Console::WriteLine(uint32)
IL_0090: nop
IL_0091: ldsfld float32 HelloWorld.Program::ninff
IL_0096: conv.u4
IL_0097: call void [System.Console]System.Console::WriteLine(uint32)
IL_009c: nop
IL_009d: ldc.i4.m1
IL_009e: conv.i8
IL_009f: call void [System.Console]System.Console::WriteLine(uint64)
IL_00a4: nop
IL_00a5: ldsfld float64 HelloWorld.Program::inf
IL_00aa: conv.u8
IL_00ab: call void [System.Console]System.Console::WriteLine(uint64)
IL_00b0: nop
IL_00b1: ldc.i4.m1
IL_00b2: call void [System.Console]System.Console::WriteLine(uint32)
IL_00b7: nop
IL_00b8: ldsfld float64 HelloWorld.Program::inf
IL_00bd: conv.u4
IL_00be: call void [System.Console]System.Console::WriteLine(uint32)
IL_00c3: nop
IL_00c4: ldc.i4.m1
IL_00c5: conv.i8
IL_00c6: call void [System.Console]System.Console::WriteLine(uint64)
IL_00cb: nop
IL_00cc: ldsfld float32 HelloWorld.Program::inff
IL_00d1: conv.u8
IL_00d2: call void [System.Console]System.Console::WriteLine(uint64)
IL_00d7: nop
IL_00d8: ldc.i4.m1
IL_00d9: call void [System.Console]System.Console::WriteLine(uint32)
IL_00de: nop
IL_00df: ldsfld float32 HelloWorld.Program::inff
IL_00e4: conv.u4
IL_00e5: call void [System.Console]System.Console::WriteLine(uint32)
IL_00ea: nop
IL_00eb: ret
My guess is something like this https://github.com/dotnet/roslyn/pull/30587 needs to be done also for double.PositiveInfinity/double.NegativeInfinity.
The unchecked conversion of double.PositiveInfinity to ulong produces a different result on Intel than it does on ARM. This is fine, as the result of the conversion is not defined (Specifically, it is specified to be “an unspecified value”).
Unfortunately, this means that the compiler gets a different result (and produces different IL) when constant-folding the expression unchecked((ulong)double.PositiveInfinity) on different host architectures.
We are lucky that is the only one of the forms tested that differs between the two architectures. But in fact we have no idea how many different virtual machines Roslyn might run on in the future, each with its own behavior for these "unspecified" operations.
Here are three options for addressing this:
I do realize that the behavior is unspecified. Nevertheless I would expect the output to be consistent between platforms and preferably also consistent with the runtime behavior on the same platform.
There's one more option for solving the problem - don't fold the NaN/PositiveInfinity/NegativeInfinity casts and always output float/double ldc opcode followed by appropriate conv opcode. The double/float representation is well defined and the burden would be shifted to the CLR runtime.
That won't work for things like attribute arguments, I think? And array literals. Also normal consts perhaps, particularly if they're referenced from other assemblies.
Notes from my study of the changes in Mono and their affect on PPC indicate PPC at least apparently has similar properties to amd64 with NaN (infinity not tested).
Nevertheless I would expect the output to be consistent between platforms and preferably also consistent with the runtime behavior on the same platform
Cross-compiling is a common scenario, so the mismatched behavior remains an issue that we cannot fully solve.
don't fold the
NaN/PositiveInfinity/NegativeInfinitycasts and always output float/doubleldcopcode followed by appropriateconvopcode
That doesn't work. These expressions are specified to be folded at compile-time, as they can for example be used in contexts like a case label.