Performance surprise “avec” et des types nullables
Je suis juste une révision du chapitre 4 de C# en Profondeur qui traite avec des types nullables, et je vais ajouter une section sur l'utilisation de l' "que" de l'opérateur, qui 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 qu'elle pourrait améliorer les performances sur le C# 1 équivalent, à l'aide de "est", suivi par un casting - après tout, de cette façon, nous avons seulement besoin de demander la dynamique de la vérification de type à la fois, et puis une simple vérification de valeur.
Cela ne semble pas être le cas, cependant. J'ai inclus un exemple de test d'application ci-dessous, qui, fondamentalement, résume tous les entiers dans un tableau d'objets - mais le tableau contient beaucoup de références nulles et de la chaîne de références ainsi que des entiers en boîte. L'indice de référence des mesures le code que vous auriez à utiliser en C# 1, le code en utilisant le "comme" de l'opérateur, et juste pour le plaisir d'un LINQ solution. À mon grand étonnement, le C# 1 du code est 20 fois plus rapide dans ce cas - et même le code LINQ (que je devrais devrait être plus lente, compte tenu de la itérateurs impliqués) bat le "comme" de code.
Est la .NET de la mise en œuvre de isinst
pour les types nullables vraiment lent? Est-ce la supplémentaires unbox.any
que les causes du problème? Est-il une autre explication pour cela? Au moment où il se sent comme je vais avoir à inclure une mise en garde contre l'utilisation de cette performance des situations délicates...
Résultats:
Cast: 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);
}
}
Je suis juste curieux, avez-vous testé avec le CLR 4.0 ainsi?
Bon point. Fera à un certain point (même si ce n'est pas dans VS pour le moment 🙂 @divo: Oui, et c'est pire de tous. Mais alors que dans la version bêta, donc il peut y avoir beaucoup de débogage de code.
Aujourd'hui, j'ai appris que vous pouvez utiliser
as
sur les types nullables. Intéressant, car il ne peut pas être utilisé sur d'autres types de valeur. En fait, le plus surprenant.il est parfaitement logique pour elle de ne pas travailler sur des types de valeur. Pensez-y,
as
tente de jeter un type et si elle échoue, elle renvoie null. Vous ne pouvez pas définir des types de valeur à null
OriginalL'auteur Jon Skeet | 2009-10-17
Vous devez vous connecter pour publier un commentaire.
Clairement le code de l'ordinateur, le compilateur JIT peut générer, pour le premier cas est beaucoup plus efficace. Une règle qui aide vraiment il n'y a qu'un objet ne peut être unboxed à une variable qui a le même type que la boîte de la valeur. Qui permet au compilateur JIT pour générer très efficace de code, pas de valeur conversions doivent être considérés.
La est opérateur de test est facile, il suffit de vérifier si l'objet n'est pas nulle et est du type attendu, mais prend un peu de code machine des instructions. Le casting est également facile, le compilateur JIT connaît l'emplacement de la valeur des bits dans l'objet et utilise directement. Aucune copie ou la conversion se produit, tous les code machine est en ligne et qu'il faut, mais une douzaine d'instructions. Ce devait être vraiment efficace .NET 1.0 lors de la boxe était commun.
La conversion en int? prend beaucoup plus de travail. La valeur de la représentation de la boîte contenant l'entier n'est pas compatible avec la disposition de la mémoire de
Nullable<int>
. Une conversion est nécessaire, et le code est délicate en raison d'une possible boxed types enum. Le compilateur JIT génère un appel à un CLR fonction d'assistance nommé JIT_Unbox_Nullable pour faire le travail. C'est un objectif général de la fonction pour n'importe quel type de la valeur, beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût étant donné que ce code est enfermé à l'intérieur mscorwks.dll mais des centaines de machine instructions de code est probable.Linq OfType() la méthode d'extension utilise également le est de l'opérateur et de la fonte. C'est cependant un casting pour un type générique. Le compilateur JIT génère un appel à une fonction d'assistance, JIT_Unbox() qui peut effectuer un cast à une valeur arbitraire de type. Je n'ai pas de grande explication de pourquoi il est aussi lent que la fonte de
Nullable<int>
, étant donné que moins de travail devrait être nécessaire. Je soupçonne que ngen.exe peut causer des problèmes ici.OriginalL'auteur Hans Passant
Il me semble que la
isinst
est vraiment lent sur les types nullables. Dans la méthodeFindSumWithCast
j'ai changéà
qui a également ralentit considérablement l'exécution. La seule differenc dans IL je peux voir, c'est que
est changé à
isinst
est suivie par un test de nullité et ensuite conditionnellement ununbox.any
. Dans le nullable cas, il y a un inconditionnelunbox.any
.Oui, s'avère
isinst
etunbox.any
sont plus lentes sur les types nullables.Vous pouvez consulter ma réponse quant à savoir pourquoi le cast est nécessaire. (Je sais c'est vieux, mais je viens de découvrir ce q et pensé que je devrais donner mon 2c de ce que je sais sur le CLR).
OriginalL'auteur Dirk Vollmar
À l'origine commencé comme un Commentaire de Hans Passant de réponse excellent, mais c'était trop long donc je veux ajouter quelques morceaux ici:
Tout d'abord, le C#
as
opérateur émet unisinst
instruction IL (le fait de lais
opérateur). (Un autre élément intéressant de l'instruction estcastclass
, rependu, quand tu fais un direct en fonte et le compilateur sait que le contrôle d'exécution ne peut pas être omis.)Voici ce que
isinst
n' (ECMA 335 Partition III, 4.6):Le plus important:
Donc, la performance de tueur n'est pas
isinst
dans ce cas, mais le supplément deunbox.any
. Ce n'était pas clair à partir de Hans réponse, alors qu'il regardait la JITed code uniquement. En général, le compilateur C# émet ununbox.any
après unisinst T?
(mais l'omettre dans le cas où vous neisinst T
, quandT
est un type de référence).Pourquoi faut-il faire?
isinst T?
n'a jamais pour effet qu'aurait été évidente, à savoir que vous obtenez en retour unT?
. Au lieu de cela, l'ensemble de ces instructions, assurez-vous que vous avez un"boxed T"
qui peut être unboxed àT?
. Pour obtenir une réelleT?
, nous avons encore besoin de unbox notre"boxed T"
àT?
, c'est pourquoi le compilateur émet ununbox.any
aprèsisinst
. Si vous pensez à ce sujet, cela fait sens car la zone "format" pourT?
est juste un"boxed T"
et de fairecastclass
etisinst
effectuer le unbox serait incompatible.La sauvegarde de Hans trouver des informations à partir de la standard, ici, il va:
(ECMA 335 Partition III, 4.33):
unbox.any
(ECMA 335 Partition III, 4.32):
unbox
OriginalL'auteur Johannes Rudolph
Fait intéressant, je suis passé sur les commentaires à propos de soutien aux opérateurs via
dynamic
étant un ordre de grandeur plus lent pourNullable<T>
(similaire à ce début de test) - je soupçonne, pour des raisons très semblables.Gotta love
Nullable<T>
. Un autre plaisir est que même si l'équipe des taches (et supprime)null
pour non nullable les structures, il borks pourNullable<T>
:Par conséquent, certains obscur code dans MiscUtil/Opérateur ;-p
Si aucune autre bonne a sortir de tout cela, il m'a conduit à inclure des avertissements pour les deux, mon code et🙂
Je sais que c'est une vieille question, mais pourriez-vous expliquer ce que vous entendez par "le JIT spots (et supprime)
null
pour non nullable les structures"? Voulez-vous dire qu'il remplacenull
avec une valeur par défaut ou quelque chose au cours de l'exécution?une méthode générique peut être utilisé lors de l'exécution avec le nombre de permutations de génériques de paramètres (
T
etc). La pile etc exigences dépendent de la args (quantité d'espace de pile pour un local, etc), de sorte que vous obtenez un JIT pour toute permutation impliquant un type de valeur. Toutefois, les références sont toutes de la même taille afin de partager un JIT. Tout en faisant de la p-valeur de type JIT, elle peut vérifier pour quelques scénarios, et essaie à l'accise code inaccessible à cause de choses comme l'impossible, les valeurs null. Il n'est pas parfait, note. Aussi, je suis ignorant AOT pour la ci-dessus.OriginalL'auteur Marc Gravell
C'est le résultat de FindSumWithAsAndHas ci-dessus: le texte d'alt http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
C'est le résultat de FindSumWithCast: le texte d'alt http://www.freeimagehosting.net/uploads/ce8a5a3934.png
Résultats:
À l'aide de
as
, tester d'abord si un objet est une instance de Int32; sous le capot, c'est à l'aide deisinst Int32
(qui est similaire à la main le code écrit par: si (o est de type int) ). Et à l'aide deas
, elle aussi inconditionnellement unbox l'objet. Et c'est une véritable performance-killer à l'appel d'un bien(c'est encore une fonction sous le capot), IL_0027À l'aide de fonte, vous vérifiez d'abord si l'objet est une
int
if (o is int)
; sous le capot, c'est à l'aide deisinst Int32
. Si il est une instance de type int, alors vous pouvez en toute sécurité unbox la valeur, IL_002DSimplement, c'est le pseudo-code de l'aide
as
approche:Et c'est le pseudo-code de l'aide en fonte approche:
De sorte que le cast (
(int)a[i]
, eh bien, la syntaxe ressemble à un casting, mais c'est en fait l'unboxing, la distribution et l'unboxing de partager la même syntaxe, la prochaine fois, je vais être pédant avec le bouton droit de la terminologie), dont l'approche est vraiment plus rapide, vous avez seulement besoin de unbox une valeur lorsqu'un objet est décidément uneint
. La même chose ne peut pas être dit à l'aide d'unas
approche.OriginalL'auteur Michael Buen
Profilage plus loin:
De sortie:
Que pouvons-nous déduire de ces chiffres?
la mise en œuvre ^_^
OriginalL'auteur
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'est pas complètement expliquer le problème, mais peut contribuer.
Non, j'ai couru, et il est légèrement plus lent.
La déclaration d'une variable dans un endroit différent de n'affecte que le code généré de façon significative lorsque la variable est capturé (à quel point il affecte la valeur de la sémantique) dans mon expérience. Notez que ce n'est pas la création d'un nouvel objet sur le tas, même si c'est certainement la création d'une nouvelle instance de
int?
sur la pile à l'aideunbox.any
. Je pense que c'est la question - ma conjecture est que fabriqués à la main IL pourrait battre les deux options ici... mais il est également possible que le JIT est optimisé pour reconnaître de l'est/cast cas et seule case à la fois.est/cast est une cible facile pour l'optimisation, c'est comme une fâcheusement idiome commun.
Les variables locales sont alloués sur la pile lorsque la pile cadre de la méthode est créée, donc où vous déclarer la variable dans la méthode ne fait aucune différence du tout. (Sauf si c'est dans une fermeture bien sûr, mais ce n'est pas le cas ici.)
OriginalL'auteur James Black
J'ai essayé le type exact de vérifier construire
typeof(int) == item.GetType()
, qui effectue aussi vite que leitem is int
version, et renvoie toujours la numéro (attention: même si vous avez écrit unNullable<int>
dans le tableau, vous devez utilisertypeof(int)
). Vous avez également besoin d'un supplément denull != item
vérifier ici.Cependant
typeof(int?) == item.GetType()
reste rapide (contrairement àitem is int?
), mais renvoie toujours false.La typeof-construire, c'est à mes yeux le moyen le plus rapide pour exacte vérification de type, comme il utilise le Propriétaire. Depuis les types exacts dans ce cas, ne correspondent pas avec les valeurs null, ma conjecture est,
is/as
avoir à faire de nouvelles heavylifting ici à s'assurer qu'il est en fait une instance d'un type Nullable.Et honnêtement: qu'est-ce que votre
is Nullable<xxx> plus HasValue
vous acheter? Rien. Vous pouvez toujours vous rendre directement à la sous-jacent (valeur) type (dans ce cas). Vous obtenez soit la valeur ou "non, pas un exemple du type demandaient". Même si vous avez écrit(int?)null
de la matrice, la vérification de type retourne la valeur false.int?
- si vous zone uneint?
valeur il termine aussi un coffret de type int ou unnull
de référence.OriginalL'auteur dalo
Afin de garder cette réponse jusqu'à ce jour, il convient de mentionner que la plupart de la discussion sur cette page est maintenant sans objet maintenant avec C# 7.1 et .NET de 4,7 qui prend en charge un slim syntaxe qui produit aussi de la meilleure IL code.
L'OP de l'exemple original...
devient tout simplement...
J'ai trouvé que l'utilisation la plus courante pour la nouvelle syntaxe est lorsque vous écrivez un .NET type de valeur (c'est à dire
struct
dans C#) qui implémenteIEquatable<MyStruct>
(comme la plupart). Après la mise en œuvre fortement typéesEquals(MyStruct other)
méthode, vous pouvez maintenant gracieusement rediriger le non typéeEquals(Object obj)
remplacer (héritée deObject
) comme suit:Annexe: La
Release
construire IL code pour les deux premiers exemples de fonctions ci-dessus dans cette réponse (respectivement) sont donnés ici. Alors que la IL code pour la nouvelle syntaxe est, en effet, 1 octet pour les plus petits, il est principalement gagne gros nul appels (contre deux) et en évitant lesunbox
opération tout à fait quand possible.Pour plus de tests, ce qui justifie ma remarque à propos de la performance de la nouvelle C#7 syntaxe dépassant précédemment, les options disponibles, voir ici (en particulier, par exemple "D").
OriginalL'auteur Glenn Slayden
Sorties:
[EDIT: 2010-06-19]
Remarque: test Précédent a été fait à l'intérieur de VS, la configuration de débogage, à l'aide de VS2009, à l'aide de Core i7(société de développement de la machine).
Ce qui suit a été fait sur ma machine à l'aide des Core 2 Duo, à l'aide de VS2010
Je suis en utilisant .NET Framework 3.5
Avez-vous été courir un unoptimised construire, ou en cours d'exécution dans le débogueur?
unoptimized construire, sous débogueur
Droit - j'ai tendance à les performances d'affichage des résultats sous un débogueur comme largement hors de propos 🙂
OriginalL'auteur Michael Buen