Je suis en train de réviser le chapitre 4 de C # en profondeur qui traite des types nullables, et j'ajoute une section sur l'utilisation de l'opérateur "as", qui vous permet d'écrire:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Je pensais que c'était vraiment bien, et que cela pourrait améliorer les performances par rapport à l'équivalent C # 1, en utilisant "est" suivi d'un cast - après tout, de cette façon, nous n'avons besoin de demander une vérification de type dynamique qu'une seule fois, puis une simple vérification de valeur .
Cependant, cela ne semble pas être le cas. J'ai inclus un exemple d'application de test ci-dessous, qui résume essentiellement tous les entiers dans un tableau d'objets - mais le tableau contient de nombreuses références nulles et des références de chaîne ainsi que des entiers encadrés. Le benchmark mesure le code que vous auriez à utiliser en C # 1, le code utilisant l'opérateur "as", et juste pour lancer une solution LINQ. À mon grand étonnement, le code C # 1 est 20 fois plus rapide dans ce cas - et même le code LINQ (qui je m'attendais à être plus lent, étant donné les itérateurs impliqués) bat le code "as".
L'implémentation .NET des isinst
types nullables est-elle vraiment très lente? Est-ce le supplément unbox.any
qui cause le problème? Y a-t-il une autre explication à cela? Pour le moment, je pense que je vais devoir inclure un avertissement contre l'utilisation de ceci dans des situations sensibles aux performances ...
Résultats:
Distribution: 10000000: 121
Comme: 10000000: 2211
LINQ: 10000000: 2143
Code:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
types nullables. Intéressant, car il ne peut pas être utilisé sur d'autres types de valeur. En fait, plus surprenant.as
essayez de transtyper en un type et s'il échoue, il retourne null. Vous ne pouvez pas définir les types de valeurs sur nullRéponses:
Il est clair que le code machine que le compilateur JIT peut générer pour le premier cas est beaucoup plus efficace. Une règle qui aide vraiment est qu'un objet ne peut être décompressé que vers une variable qui a le même type que la valeur encadrée. Cela permet au compilateur JIT de générer du code très efficace, aucune conversion de valeur ne doit être prise en compte.
Le est opérateur de test est facile, il suffit de vérifier si l'objet est non nul et est du type attendu, ne prend que quelques instructions de code machine. La conversion est également facile, le compilateur JIT connaît l'emplacement des bits de valeur dans l'objet et les utilise directement. Aucune copie ou conversion ne se produit, tout le code machine est en ligne et ne prend qu'une douzaine d'instructions. Cela devait être vraiment efficace dans .NET 1.0 lorsque la boxe était courante.
Casting à int? demande beaucoup plus de travail. La représentation des valeurs de l'entier encadré n'est pas compatible avec la disposition de la mémoire de
Nullable<int>
. Une conversion est requise et le code est délicat en raison des types d'énumération encadrés possibles. Le compilateur JIT génère un appel à une fonction d'assistance CLR nommée JIT_Unbox_Nullable pour effectuer le travail. Il s'agit d'une fonction à usage général pour tout type de valeur, beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût car ce code est enfermé dans mscorwks.dll, mais des centaines d'instructions de code machine sont probables.La méthode d'extension Linq OfType () utilise également l' opérateur is et le transtypage. Il s'agit cependant d'une conversion vers un type générique. Le compilateur JIT génère un appel à une fonction d'assistance, JIT_Unbox () qui peut effectuer une conversion vers un type de valeur arbitraire. Je n'ai pas une grande explication pourquoi c'est aussi lent que le casting
Nullable<int>
, étant donné que moins de travail devrait être nécessaire. Je soupçonne que ngen.exe pourrait causer des problèmes ici.la source
Il me semble que le
isinst
est vraiment très lent sur les types nullables. Dans la méthodeFindSumWithCast
j'ai changéà
ce qui ralentit également considérablement l'exécution. La seule différence en IL que je peux voir est que
est changé en
la source
isinst
est suivi d'un test de nullité puis conditionnellement d' ununbox.any
. Dans le cas annulable, il y a un inconditionnelunbox.any
.isinst
etunbox.any
sont plus lents sur les types nullable.Cela a commencé à l'origine comme un commentaire à l'excellente réponse de Hans Passant, mais il est devenu trop long, donc je veux ajouter quelques bits ici:
Tout d'abord, l'
as
opérateur C # émettra uneisinst
instruction IL (tout comme l'is
opérateur). (Une autre instruction intéressante estcastclass
émise lorsque vous effectuez une conversion directe et que le compilateur sait que la vérification de l'exécution ne peut pas être omise.)Voici ce que
isinst
fait ( ECMA 335 Partition III, 4.6 ):Plus important encore:
Donc, le tueur de performances n'est pas
isinst
dans ce cas, mais en plusunbox.any
. Cela n'était pas clair dans la réponse de Hans, car il ne regardait que le code JITed. En général, le compilateur C # émettra ununbox.any
after aisinst T?
(mais l'omettra au cas où vous le feriezisinst T
, quandT
est un type de référence).Pourquoi ça fait ça?
isinst T?
n'a jamais l'effet qui aurait été évident, c'est-à-dire que vous récupérez aT?
. Au lieu de cela, toutes ces instructions garantissent que vous en avez un"boxed T"
qui peut être déballéT?
. Pour obtenir une réelleT?
, nous avons encore besoin de notre unbox"boxed T"
àT?
, ce qui explique pourquoi le compilateur émet ununbox.any
aprèsisinst
. Si vous y réfléchissez, cela a du sens car le "format de boîte" pourT?
est juste un"boxed T"
et la créationcastclass
et l'isinst
exécution de la boîte seraient incohérentes.Sauvegardant la découverte de Hans avec quelques informations de la norme , voici:
(ECMA 335 Partition III, 4.33):
unbox.any
(ECMA 335 Partition III, 4.32):
unbox
la source
Fait intéressant, j'ai transmis des commentaires sur le soutien de l'opérateur en
dynamic
étant plus lent d'un ordre de grandeurNullable<T>
(similaire à ce premier test ) - je soupçonne pour des raisons très similaires.Je dois aimer
Nullable<T>
. Un autre plaisir est que même si le JIT repère (et supprime)null
pour les structures non nullables, il le fausse pourNullable<T>
:la source
null
pour les structures non nullables"? Voulez-vous dire qu'il remplacenull
par une valeur par défaut ou quelque chose pendant l'exécution?T
etc.). Les exigences de la pile, etc. dépendent des arguments (quantité d'espace de pile pour un local, etc.), vous obtenez donc un JIT pour toute permutation unique impliquant un type de valeur. Cependant, les références sont toutes de la même taille, alors partagez un JIT. Tout en effectuant le JIT de type par valeur, il peut vérifier quelques scénarios évidents et essaie de supprimer le code inaccessible en raison de choses comme des valeurs nulles impossibles. Ce n'est pas parfait, notez. En outre, j'ignore AOT pour ce qui précède.count
variable. L'ajoutConsole.Write(count.ToString()+" ");
après lewatch.Stop();
dans les deux cas ralentit les autres tests d'un peu moins d'un ordre de grandeur, mais le test annulable sans restriction n'est pas modifié. Notez qu'il y a également des changements lorsque vous testez les cas lorsqu'ilnull
est passé, confirmant que le code d'origine ne fait pas vraiment la vérification et l'incrémentation nulles pour les autres tests. LinqpadCeci est le résultat de FindSumWithAsAndHas ci-dessus:
Ceci est le résultat de FindSumWithCast:
Résultats:
À l'aide
as
, il teste d'abord si un objet est une instance de Int32; sous le capot qu'il utiliseisinst Int32
(ce qui est similaire au code manuscrit: si (o est int)). Et en utilisantas
, il déballe également inconditionnellement l'objet. Et c'est un vrai tueur de performances d'appeler une propriété (c'est toujours une fonction sous le capot), IL_0027En utilisant cast, vous testez d'abord si l'objet est un
int
if (o is int)
; sous le capot que cela utiliseisinst Int32
. S'il s'agit d'une instance de int, vous pouvez décompresser la valeur en toute sécurité, IL_002DAutrement dit, voici le pseudo-code d'utilisation de l'
as
approche:Et voici le pseudo-code d'utilisation de l'approche Cast:
Donc, le cast (
(int)a[i]
, bien que la syntaxe ressemble à un cast, mais c'est en fait unboxing, cast et unboxing partagent la même syntaxe, la prochaine fois que je serai pédant avec la bonne terminologie) l'approche est vraiment plus rapide, il vous suffit de déballer une valeur quand un objet est décidément unint
. On ne peut pas dire la même chose en utilisant uneas
approche.la source
Afin de maintenir cette réponse à jour, il convient de mentionner que la plupart des discussions sur cette page sont désormais sans objet avec C # 7.1 et .NET 4.7 qui prend en charge une syntaxe mince qui produit également le meilleur code IL.
L'exemple original de l'OP ...
devient simplement ...
J'ai trouvé qu'une utilisation courante de la nouvelle syntaxe est lorsque vous écrivez un type de valeur .NET (c'est-
struct
à- dire en C # ) qui implémenteIEquatable<MyStruct>
(comme la plupart le devraient). Après avoir implémenté laEquals(MyStruct other)
méthode fortement typée , vous pouvez désormais rediriger avec élégance leEquals(Object obj)
remplacement non typé (hérité deObject
) comme suit:Annexe: Le code IL de
Release
construction pour les deux premiers exemples de fonctions montrés ci-dessus dans cette réponse (respectivement) est donné ici. Bien que le code IL pour la nouvelle syntaxe soit en effet 1 octet plus petit, il gagne généralement gros en faisant zéro appel (contre deux) et en évitant complètement l' opération lorsque cela est possible.unbox
Pour d'autres tests qui corroborent ma remarque sur les performances de la nouvelle syntaxe C # 7 dépassant les options précédemment disponibles, voir ici (en particulier, l'exemple «D»).
la source
Profilage supplémentaire:
Production:
Que pouvons-nous déduire de ces chiffres?
la source
Je n'ai pas le temps de l'essayer, mais vous voudrez peut-être avoir:
comme
Vous créez un nouvel objet à chaque fois, ce qui n'expliquera pas complètement le problème, mais peut y contribuer.
la source
int?
sur la pile à l'aideunbox.any
. Je soupçonne que c'est le problème - je suppose que l'IL fabriqué à la main pourrait battre les deux options ici ... bien qu'il soit également possible que le JIT soit optimisé pour reconnaître le cas is / cast et ne vérifier qu'une seule fois.J'ai essayé la construction de vérification de type exacte
typeof(int) == item.GetType()
, qui fonctionne aussi vite que laitem is int
version et renvoie toujours le nombre (accentuation: même si vous avez écrit unNullable<int>
dans le tableau, vous devrez utilisertypeof(int)
). Vous avez également besoin d'unnull != item
contrôle supplémentaire ici.toutefois
typeof(int?) == item.GetType()
reste rapide (contrairement àitem is int?
), mais renvoie toujours false.Le typeof-construct est à mes yeux le moyen le plus rapide pour la vérification de type exacte , car il utilise le RuntimeTypeHandle. Étant donné que les types exacts dans ce cas ne correspondent pas à nullable, je suppose que vous
is/as
devez effectuer un levage supplémentaire ici pour vous assurer qu'il s'agit bien d'une instance de type Nullable.Et honnêtement: qu'est-ce que vous
is Nullable<xxx> plus HasValue
vous achetez? Rien. Vous pouvez toujours accéder directement au type (valeur) sous-jacent (dans ce cas). Vous obtenez la valeur ou "non, pas une instance du type que vous demandiez". Même si vous avez écrit(int?)null
dans le tableau, la vérification de type renverra false.la source
int?
- si vous encadrez uneint?
valeur, elle se termine par un int encadré ou unenull
référence.Les sorties:
[EDIT: 2010-06-19]
Remarque: Le test précédent a été effectué à l'intérieur de VS, débogage de la configuration, à l'aide de VS2009, à l'aide de Core i7 (machine de développement de l'entreprise).
Ce qui suit a été fait sur ma machine en utilisant Core 2 Duo, en utilisant VS2010
la source