¿Por qué el compilador de C # traduce esto! = Comparación como si fuera una> comparación?

147

Por pura casualidad descubrí que el compilador de C # activa este método:

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

... en este CIL :

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

... o, si prefiere mirar el código C # descompilado:

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

¿Cómo es que el !=se traduce como " >"?

stakx - ya no contribuye
fuente

Respuestas:

201

Respuesta corta:

No existe una instrucción "comparar-no-igual" en IL, por lo que el !=operador C # no tiene correspondencia exacta y no puede traducirse literalmente.

Sin embargo, existe una instrucción "comparar-igual" ( cequna correspondencia directa con el ==operador), por lo que, en el caso general, x != yse traduce como su equivalente un poco más largo (x == y) == false.

También hay una instrucción "compare-mayor-que" en IL ( cgt) que permite al compilador tomar ciertos atajos (es decir, generar un código IL más corto), uno de ellos es que las comparaciones de desigualdad de objetos contra nulos obj != null, se traducen como si fueran " obj > null".

Vamos a entrar en más detalles.

Si no hay una instrucción "comparar-no-igual" en IL, entonces, ¿cómo el compilador traducirá el siguiente método?

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

Como ya se dijo anteriormente, el compilador convertirá el x != yen (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
}

Resulta que el compilador no siempre produce este patrón bastante largo. Veamos qué sucede cuando reemplazamos ycon la constante 0:

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

La IL producida es algo más corta que en el caso general:

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

El compilador puede aprovechar el hecho de que los enteros con signo se almacenan en el complemento de dos (donde, si los patrones de bits resultantes se interpretan como enteros sin signo, eso es lo que .unsignifica - 0 tiene el valor más pequeño posible), por lo que se traduce x == 0como si fuera unchecked((uint)x) > 0.

Resulta que el compilador puede hacer lo mismo para las comprobaciones de desigualdad contra null:

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

El compilador produce casi el mismo IL que para IsNotZero:

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

Aparentemente, el compilador puede asumir que el patrón de bits de la nullreferencia es el patrón de bits más pequeño posible para cualquier referencia de objeto.

Este acceso directo se menciona explícitamente en el Estándar Anotado de Infraestructura de Lenguaje Común (primera edición de octubre de 2003) (en la página 491, como una nota al pie de la Tabla 6-4, "Comparaciones binarias u operaciones de sucursal"):

" cgt.unestá permitido y es verificable en ObjectRefs (O). Esto se usa comúnmente cuando se compara un ObjectRef con un valor nulo (no existe una instrucción" comparar-no-igual ", que de lo contrario sería una solución más obvia)".

stakx - ya no contribuye
fuente
3
Excelente respuesta, solo una cosa: el complemento de dos no es relevante aquí. Solo importa que los enteros con signo se almacenen de tal manera que los valores no negativos en intel rango tengan la misma representación intque en uint. Ese es un requisito mucho más débil que el complemento de dos.
3
Los tipos sin signo nunca tienen números negativos, por lo que una operación de comparación que se compara con cero no puede tratar ningún número que no sea cero como menor que cero. Todas las representaciones correspondientes a los valores no negativos de intya han sido tomadas por el mismo valor en uint, por lo que todas las representaciones correspondientes a los valores negativos de inttienen que corresponder a un valor uintmayor que 0x7FFFFFFF, pero en realidad no importa qué valor es. (En realidad, todo lo que realmente se requiere es que el cero se represente de la misma manera en ambos inty uint.)
3
@hvd: Gracias por explicarlo. Tienes razón, no es el complemento de dos lo que importa; es el requisito que mencionó y el hecho de que cgt.untrata a an intcomo un uintsin cambiar el patrón de bits subyacente. (Imagínese que cgt.unempezarían por tratar de underflow fijos mediante la asignación de todos los números negativos a 0. En ese caso, es obvio que no puede sustituir > 0a != 0.)
stakx - ya no contribuyendo
2
Encuentro sorprendente que comparar una referencia de objeto con otra utilizando >IL sea verificable. De esa manera, uno podría comparar dos objetos no nulos y obtener un resultado booleano (que no es determinista). Ese no es un problema de seguridad de la memoria, pero se siente como un diseño sucio que no está en el espíritu general de un código administrado seguro. Este diseño filtra el hecho de que las referencias a objetos se implementan como punteros. Parece un defecto de diseño de .NET CLI.
usr
3
@ usr: ¡Absolutamente! La sección III.1.1.4 del estándar CLI dice que "las referencias a objetos (tipo O) son completamente opacas" y que "las únicas operaciones de comparación permitidas son la igualdad y la desigualdad ...". Quizás debido a referencias a objetos son no definen en términos de direcciones de memoria, el estándar también se encarga de mantener conceptualmente la referencia nula aparte de 0 (véase por ejemplo las definiciones de ldnull, initobjy newobj). Por lo tanto, el uso de cgt.uncomparar referencias de objeto contra la referencia nula parece contradecir la sección III.1.1.4 en más de una forma.
stakx - ya no contribuye el