J'ai une méthode qui compare deux ints nullable et affiche le résultat de la comparaison sur la console:
static void TestMethod(int? i1, int? i2)
{
Console.WriteLine(i1 == i2);
}
Et ceci est le résultat de sa décompilation:
private static void TestMethod(int? i1, int? i2)
{
int? nullable = i1;
int? nullable2 = i2;
Console.WriteLine((nullable.GetValueOrDefault() == nullable2.GetValueOrDefault()) & (nullable.HasValue == nullable2.HasValue));
}
Le résultat correspond plus ou moins à ce que j'attendais, mais je me demande pourquoi la version sans court-circuit des opérateurs logiques 'et' (&) (&) est utilisée à la place de la version en court-circuit (&&). Il me semble que ce dernier serait plus efficace - si l’on sait déjà qu’un côté de la comparaison est faux, il n’est pas nécessaire d’évaluer l’autre côté. L'opérateur & est-il nécessaire ici ou s'agit-il simplement d'un détail d'implémentation qui n'est pas assez important pour que l'on s'en occupe?
Le résultat est plus petit que ce à quoi je m'attendais, mais je me demande pourquoi une version sans court-circuit des opérateurs logiques et opérateur (&) est utilisée à la place de la version en court-circuit (&&). Il me semble que ce dernier serait plus efficace - si l’un des côtés de la comparaison est déjà connu, il n’est pas nécessaire d’évaluer l’autre. Y a-t-il une raison qui rend obligatoire l'utilisation de & operator ou s'agit-il simplement de détails de mise en œuvre qui ne sont pas assez importants pour déranger?
C'est une excellente question.
Tout d'abord, j'ai travaillé sur les générateurs de code pour le code d'abaissement nullable antérieur à Roslyn, ainsi que pour la mise en œuvre initiale dans Roslyn. C'est un code compliqué avec beaucoup d'occasions d'erreurs et d'optimisations manquées. J'ai écrit une longue série d'articles de blog sur le fonctionnement de l'optimiseur d'abaissement nullable de Roslyn, qui commence ici:
https://ericlippert.com/2012/12/20//nullable-micro-optimizations-part-one/
Si ce sujet vous intéresse, cette série d'articles vous sera probablement d'une grande aide. La troisième partie est particulièrement pertinente puisqu'elle aborde une question connexe: pour l'arithmétique nullable, générons-nous (x.HasValue & y.HasValue) ? new int?(x.Value + y.Value) : new int?()
ou utilisons-nous &&
ou utilisons-nous GetValueOrDefault
ou quoi? (La réponse est bien sûr que j'ai tout essayé et que j'ai choisi celui qui produit le code le plus petit et le plus rapide.) Toutefois, la série n'envisage pas votre question spécifique ici, qui concerne l'égalité nullable. L'égalité nullable a des règles légèrement différentes de l'arithmétique levée ordinaire.
Bien sûr, je ne suis plus chez Microsoft depuis 2012, et ils l’ont peut-être changé depuis; Je ne saurais pas (MISE À JOUR: en examinant la question liée dans les commentaires ci-dessus, il semble probable que j'ai raté une optimisation lors de ma mise en œuvre initiale en 2011 et que cela ait été corrigé en 2017.)
Pour répondre à votre question précise: le problème avec &&
est qu’il est plus coûteux que &
lorsque le travail effectué du côté droit de l’opérateur est moins cher que le test-and-branch. Test-and-branch n'est pas simplement plus d'instructions. C'est évidemment une branche qui a beaucoup d'effets d'entraînement. Au niveau du processeur, les branches nécessitent une prédiction de branche, et les branches peuvent être mal prédites. Les branches signifient plus de blocs de base et rappelez-vous que l'optimiseur jit s'exécute au moment de l'exécution, ce qui signifie que l'optimiseur jit doit être rapide. Il est permis de dire "trop de blocs de base dans cette méthode, je vais sauter certaines optimisations", et donc peut-être qu'inutilement plus de blocs de base est une mauvaise chose.
Pour résumer, le compilateur C # générera des opérations "et" aussi désireuses si le côté droit n’a pas d’effet secondaire et il estime que l’évaluation de left & right
sera plus rapide et plus courte que celle de left ? right : false
. Souvent, le coût d’évaluer right
est tellement bon marché que le coût de la succursale est plus coûteux que de simplement faire l’arithmétique avide.
Vous pouvez le voir dans des zones autres que l'optimiseur nullable; par exemple, si vous avez
bool x = X();
bool y = Y();
bool z = x && y;
alors cela sera généré en tant que z = x & y
car le compilateur sait qu'il n'y a pas d'opération coûteuse à sauvegarder; Y()
a déjà été appelé.
Si vous examinez le code CIL compilé (Roslyn 2.0), vous pouvez réellement voir (ligne IL_0016) l’instruction br.s
qui exécute un raccourci vers l’appel Console.WriteLine()
si les résultats des méthodes GetValue()
ne sont pas égaux (IL_0013). Je suppose que c'est votre decompiler qui ne se soucie pas de l'afficher correctement en tant que &&
.
IL_0000: nop
IL_0001: ldarg.0
IL_0002: stloc.0
IL_0003: ldarg.1
IL_0004: stloc.1
IL_0005: ldloca.s V_0
IL_0007: call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_000c: ldloca.s V_1
IL_000e: call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_0013: beq.s IL_0018
IL_0015: ldc.i4.0
IL_0016: br.s IL_0028
IL_0018: ldloca.s V_0
IL_001a: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_001f: ldloca.s V_1
IL_0021: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_0026: ceq
IL_0028: call void [mscorlib]System.Console::WriteLine(bool)
IL_002d: nop
IL_002e: ret