Why does the C# compiler translate this != comparison as if it were a > comparison?
Asked Answered
R

1

151

I have by pure chance discovered that the C# compiler turns this method:

static bool IsNotNull(object obj)
{
    return obj != null;
}

…into this CIL:

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

…or, if you prefer looking at decompiled C# code:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

How come that the != gets translated as a ">"?

Repent answered 28/2, 2015 at 12:43 Comment(0)
R
202

Short answer:

There is no "compare-not-equal" instruction in IL, so the C# != operator has no exact correspondence and cannot be translated literally.

There is however a "compare-equal" instruction (ceq, a direct correspondence to the == operator), so in the general case, x != y gets translated like its slightly longer equivalent (x == y) == false.

There is also a "compare-greater-than" instruction in IL (cgt) which allows the compiler to take certain shortcuts (i.e. generate shorter IL code), one being that inequality comparisons of objects against null, obj != null, get translated as if they were "obj > null".

Let's go into some more detail.

If there is no "compare-not-equal" instruction in IL, then how will the following method get translated by the compiler?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

As already said above, the compiler will turn the x != y into (x == y) == false:

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

It turns out that the compiler does not always produce this fairly long-winded pattern. Let's see what happens when we replace y with the constant 0:

static bool IsNotZero(int x)
{
    return x != 0;
}

The IL produced is somewhat shorter than in the general case:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

The compiler can take advantage of the fact that signed integers are stored in two's complement (where, if the resulting bit patterns are interpreted as unsigned integers — that's what the .un means — 0 has the smallest possible value), so it translates x == 0 as if it were unchecked((uint)x) > 0.

It turns out the compiler can do just the same for inequality checks against null:

static bool IsNotNull(object obj)
{
    return obj != null;
}

The compiler produces almost the same IL as for IsNotZero:

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

Apparently, the compiler is allowed to assume that the bit pattern of the null reference is the smallest bit pattern possible for any object reference.

This shortcut is explicitly mentioned in the Common Language Infrastructure Annotated Standard (1st edition from Oct 2003) (on page 491, as a footnote of Table 6-4, "Binary Comparisons or Branch Operations"):

"cgt.un is allowed and verifiable on ObjectRefs (O). This is commonly used when comparing an ObjectRef with null (there is no "compare-not-equal" instruction, which would otherwise be a more obvious solution)."

Repent answered 28/2, 2015 at 12:43 Comment(7)
Excellent answer, just one nit: two's complement is not relevant here. It only matters that signed integers are stored in such a way that non-negative values in int's range have the same representation in int as they do in uint. That's a far weaker requirement than two's complement.Thirddegree
Unsigned types never have any negative numbers, so a comparison operation that compares to zero cannot treat any non-zero number as less than zero. All representations corresponding to the non-negative values of int have already been taken up by the same value in uint, so all representations corresponding to the negative values of int have to correspond to some value of uint greater than 0x7FFFFFFF, but it doesn't really matter which value that is. (Actually, all that's really required is that zero is represented the same way in both int and uint.)Thirddegree
@hvd: Thanks for explaining. You are right, it is not two's complement that matters; it is the requirement that you mentioned and the fact that cgt.un treats an int as an uint without changing the underlying bit pattern. (Imagine that cgt.un would first try to fix underflows by mapping all negative numbers to 0. In that case you obviously couldn't substitute > 0 for != 0.)Repent
Heh, yeah, good point, that's indeed one requirement I forgot to mention. :)Thirddegree
I find it surprising that comparing an object reference to another one using > is verifiable IL. That way one could compare two non-null objects and get a boolean result (which is non-deterministic). That's not a memory-safety issue but it feels like unclean design that is not in the general spirit of safe managed code. This design leaks the fact that object references are implemented as pointers. Seems like a design flaw of the .NET CLI.Cuirassier
@usr: Absolutely! Section III.1.1.4 of the CLI standard says that "Object references (type O) are completely opaque" and that "the only comparison operations permitted are equality and inequality…." Perhaps because object references are not defined in terms of memory addresses, the standard also takes care to conceptually keep the null reference apart from 0 (see e.g. the definitions of ldnull, initobj, and newobj). So the use of cgt.un to compare object references against the null reference appears to contradict section III.1.1.4 in more than one way.Repent
An alternative to ceq; ldc.i4.0; ceq; for the general case of "not-equals" is ceq; ldc.i4.1; xor;. The latter may be more suitable in some cases; be sure to check the release+optimized output of your target JIT to see how inlining affects the native instruction stream.Mcculloch

© 2022 - 2024 — McMap. All rights reserved.