Roslyn: Constant folding produces different IL depending on host architecture

Created on 29 Jul 2019  ·  6Comments  ·  Source: dotnet/roslyn

Version Used:

3.3.0-beta2-19374-02

Steps to Reproduce:

  1. Compile and run the following program:
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));
        }
    }
}
  1. Repeat step 1 on different architectures (x64, ARM64)

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
Area-Compilers Bug Concept-Determinism

All 6 comments

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:

  1. Constant-folding any unchecked explicit conversion from a floating-point type to an integral type that would throw an exception if it were checked should convert to 0 at compile-time on every host architecture. This has maximal determinism but minimal compatibility (with previous compiler behavior)
  2. Simulate the Intel behavior. This has unknown complexity and may be impossible if it turns out that different Intel processors differ.
  3. Fix only the single identified difference, and be willing to accept future reports as they arise. This is simple to do but of course leaves the possibility that the underlying problem may have to be addressed again in the future.

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/NegativeInfinity casts and always output float/double ldc opcode followed by appropriate conv opcode

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ashmind picture ashmind  ·  3Comments

joshua-mng picture joshua-mng  ·  3Comments

MadsTorgersen picture MadsTorgersen  ·  3Comments

codingonHP picture codingonHP  ·  3Comments

marler8997 picture marler8997  ·  3Comments