Skip to content

Very inefficient IL Generation for nullable numeric comparisons #44109

@ThadHouse

Description

@ThadHouse

The following function

        public static string TestNullableChar(char? nextToken) {
                if (nextToken != null && nextToken == '=')
                {
                    return "Equals";
                }
                return "NullOrNotEquals";
        }

generates the following IL

    .method public hidebysig static 
        string TestNullableChar (
            valuetype [System.Private.CoreLib]System.Nullable`1<char> nextToken
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 79 (0x4f)
        .maxstack 2
        .locals init (
            [0] valuetype [System.Private.CoreLib]System.Nullable`1<int32>,
            [1] int32,
            [2] valuetype [System.Private.CoreLib]System.Nullable`1<char>,
            [3] valuetype [System.Private.CoreLib]System.Nullable`1<int32>
        )

        IL_0000: ldarga.s nextToken
        IL_0002: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<char>::get_HasValue()
        IL_0007: brfalse.s IL_0049 // This is the nullable null check, as expected.

        IL_0009: ldarg.0 // This code is literally copying the argument into a local, and then null checking the local again.
        IL_000a: stloc.2
        IL_000b: ldloca.s 2
        IL_000d: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<char>::get_HasValue()
        IL_0012: brtrue.s IL_001f // Because the argument was already null checked, this will always branch, as the nullable will always have a value at this point.

        IL_0014: ldloca.s 3 // This code is dead, up to 001d, as the above line will always have branched
        IL_0016: initobj valuetype [System.Private.CoreLib]System.Nullable`1<int32>
        IL_001c: ldloc.3
        IL_001d: br.s IL_002b

        IL_001f: ldloca.s 2 // This is grabbing the value (out of the local and not the arg) and is constructing a nullable int.
        IL_0021: call instance !0 valuetype [System.Private.CoreLib]System.Nullable`1<char>::GetValueOrDefault()
// I believe this code is actually unverifyable. on the top of the stack is a char, however this is passed to a constructor taking an int.
        IL_0026: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)

        IL_002b: stloc.0 // storing the nullable (not the actual value)
        IL_002c: ldc.i4.s 61
        IL_002e: stloc.1
        IL_002f: ldloca.s 0 // loading the nullable, and then grabbing the value
        IL_0031: call instance !0 valuetype [System.Private.CoreLib]System.Nullable`1<int32>::GetValueOrDefault()
        IL_0036: ldloc.1
        IL_0037: ceq // comparing the 2 variables
        IL_0039: ldloca.s 0
        IL_003b: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue()
        IL_0040: and // Doing another null check?
        IL_0041: brfalse.s IL_0049

        IL_0043: ldstr "Equals"
        IL_0048: ret

        IL_0049: ldstr "NullOrNotEquals"
        IL_004e: ret
    } // end of method C::TestNullableChar

This generated IL is extremely inefficient, contains unhittable branches, multiple extra locals, completely bypassed code, and is very hard to tell that even the behavior is right. The extra branches cause issues for one of my classes, which wanted to see 100% code coverage, which is impossible because of the guaranteed not taken branch at IL_0012.

There is actually 3 null checks for the same value in this code (0007, 0012, 0040)

At IL_0021 and IL_0026, I believe this is actually generating unverifyable code, since a char variable on the stack is getting passed to a function taking an int at that location on the stack.

I feel like this is an attempt to promote the char comparisons to int comparisons, but this seems to be promoting the actual nullable, and then comparing, rather then promoting just the value.

Version Used: .NET Core 3.1

Steps to Reproduce:

  1. Compile the above code.
  2. Observe the generated output

Expected Behavior:
Either direct char comparisons, or promotion of the actual value to an int, and then int comparison.

Actual Behavior:
The nullable gets promoted, causing dead code, dead branches, and multiple checks for the same thing, in addition to a potential verification bug.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions