Pourquoi le compilateur C # traduit-il cette comparaison! = Comme s'il s'agissait d'une> comparaison?

147

J'ai découvert par pur hasard que le compilateur C # tourne cette méthode:

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

… Dans ce CIL :

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

… Ou, si vous préférez regarder du code C # décompilé:

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

Comment se fait-il que le !=soit traduit par " >"?

stakx - ne contribue plus
la source

Réponses:

201

Réponse courte:

Il n'y a pas d'instruction "compare-not-equal" dans IL, donc l' !=opérateur C # n'a pas de correspondance exacte et ne peut pas être traduit littéralement.

Il existe cependant une instruction "comparer-égal" ( ceq, une correspondance directe avec l' ==opérateur), donc dans le cas général, x != yse traduit comme son équivalent légèrement plus long (x == y) == false.

Il existe également une instruction "comparer plus grand que" dans IL ( cgt) qui permet au compilateur de prendre certains raccourcis (c'est-à-dire générer du code IL plus court), l'un étant que les comparaisons d'inégalité des objets contre null,, obj != nullse traduisent comme si elles étaient " obj > null".

Entrons dans un peu plus de détails.

S'il n'y a pas d'instruction "compare-not-equal" dans IL, comment la méthode suivante sera-t-elle traduite par le compilateur?

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

Comme déjà dit ci-dessus, le compilateur transformera le 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
}

Il s'avère que le compilateur ne produit pas toujours ce modèle assez long. Voyons ce qui se passe lorsque nous remplaçons ypar la constante 0:

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

L'IL produite est un peu plus courte que dans le cas général:

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

Le compilateur peut tirer parti du fait que les entiers signés sont stockés dans le complément à deux (où, si les modèles de bits résultants sont interprétés comme des entiers non signés - c'est ce que .unsignifie - 0 a la plus petite valeur possible), donc il se traduit x == 0comme s'il était unchecked((uint)x) > 0.

Il s'avère que le compilateur peut faire la même chose pour les vérifications d'inégalité contre null:

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

Le compilateur produit presque le même IL que pour IsNotZero:

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

Apparemment, le compilateur est autorisé à supposer que le modèle de bits de la nullréférence est le plus petit modèle de bits possible pour toute référence d'objet.

Ce raccourci est explicitement mentionné dans le Common Language Infrastructure Annotated Standard (1ère édition d'octobre 2003) (à la page 491, en tant que note de bas de page du tableau 6-4, «Comparaisons binaires ou opérations de succursales»):

" cgt.unest autorisé et vérifiable sur ObjectRefs (O). Ceci est couramment utilisé lors de la comparaison d'un ObjectRef avec null (il n'y a pas d'instruction" compare-not-equal ", ce qui serait autrement une solution plus évidente)."

stakx - ne contribue plus
la source
3
Excellente réponse, juste un nit: le complément à deux n'est pas pertinent ici. Il importe seulement que les entiers signés soient stockés de telle manière que les valeurs non négatives dans intla plage de s aient la même représentation dans intque dans uint. C'est une exigence bien plus faible que le complément à deux.
3
Les types non signés n'ont jamais de nombres négatifs, donc une opération de comparaison qui compare à zéro ne peut pas traiter un nombre différent de zéro comme inférieur à zéro. Toutes les représentations correspondant aux valeurs non négatives de intont déjà été reprises par la même valeur dans uint, donc toutes les représentations correspondant aux valeurs négatives de intdoivent correspondre à une valeur uintsupérieure à 0x7FFFFFFF, mais peu importe quelle valeur est. (En fait, tout ce qui est vraiment nécessaire, c'est que zéro soit représenté de la même manière dans les deux intet uint.)
3
@hvd: Merci d'avoir expliqué. Vous avez raison, ce n'est pas le complément à deux qui compte; c'est l'exigence que vous avez mentionnée et le fait de cgt.untraiter un intcomme un uintsans changer le motif de bits sous-jacent. (Imaginez que cgt.unfaudrait d' abord essayer de résoudre sousverses en cartographiant tous les numéros négatifs à 0. Dans ce cas , vous ne pouvait évidemment pas se substituer > 0à != 0.)
stakx - ne cotisez
2
Je trouve surprenant que comparer une référence d'objet à une autre en utilisant >est IL vérifiable. De cette façon, on pourrait comparer deux objets non nuls et obtenir un résultat booléen (qui n'est pas déterministe). Ce n'est pas un problème de sécurité de la mémoire, mais cela ressemble à une conception impure qui n'est pas dans l'esprit général du code géré sécurisé. Cette conception fuit le fait que les références d'objet sont implémentées en tant que pointeurs. Cela ressemble à un défaut de conception de la CLI .NET.
usr
3
@usr: Absolument! La section III.1.1.4 de la norme CLI dit que «les références d'objet (type O) sont complètement opaques» et que «les seules opérations de comparaison autorisées sont l'égalité et l'inégalité…». Peut-être parce que les références d'objet sont pas définis en termes d'adresses de mémoire, la norme prend soin de garder conceptuellement aussi la référence nulle part de 0 (voir par exemple les définitions de ldnull, initobjet newobj). Ainsi, l'utilisation de cgt.unpour comparer les références d'objet à la référence nulle semble contredire la section III.1.1.4 de plus d'une manière.
stakx - ne contribue plus