Surprise de Performance avec les types " as " et nullable
Je révise juste 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 chouette, et que cela pourrait améliorer les performances par rapport à L'équivalent C# 1, en utilisant "is" suivi d'un cast - après tout, de cette façon, nous n'avons besoin que de demander une vérification de type dynamique une fois, puis une simple vérification de valeur.
Cela ne semble pas être le cas, cependant. J'ai inclus un échantillon de test app ci - dessous, qui résume essentiellement tous les entiers dans un tableau d'objets-mais le tableau contient beaucoup de références nulles et de références de chaînes ainsi que des entiers en boîte. Le benchmark mesure le code que vous devrez 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 (que je m'attendais à être plus lent, compte tenu des itérateurs impliqués) bat le code "as".
Est le L'implémentation. net de isinst
pour les types nullable est vraiment lente? Est - ce le unbox.any
supplémentaire qui cause le problème? Y a-t-il une autre explication à cela? Pour le moment, il me semble que je vais devoir inclure un avertissement contre l'utilisation de cela dans des situations sensibles aux performances...
Résultats:
Fonte: 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);
}
}
10 ré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 là est qu'un objet ne peut être déballé qu'à une variable qui a le même type que la valeur en boîte. Cela permet au compilateur JIT de générer du code très efficace, aucune conversion de valeur ne doit être considérée.
Leest le test d'opérateur est facile, il suffit de vérifier si l'objet n'est pas null et est du type attendu, ne prend que quelques codes machine instruction. Le cast 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.
Lancer à int? prend beaucoup plus de travail. La représentation de la valeur de l'entier encadré n'est pas compatible avec la disposition de la mémoire de Nullable<int>
. Une conversion est nécessaire et le code est délicat en raison de possibles types d'énumération en boîte. Le compilateur JIT génère un appel à une fonction d'assistance CLR nommée JIT_Unbox_Nullable pour effectuer le travail. C'est une fonction à usage général pour n'importe quel type de valeur, beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût puisque ce code est enfermé dans mscorwks.dll, mais des centaines d'instructions de code machine est probable.
La méthode D'extension Linq OfType() utilise également l'opérateur is et la distribution. Ce est cependant un cast à un type générique. Le compilateur JIT génère un appel à une fonction d'assistance, JIT_Unbox() qui peut effectuer une conversion à un type de valeur arbitraire. Je n'ai pas une bonne explication pourquoi il 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.
Il me semble que le isinst
est vraiment lent sur les types nullables. Dans la méthode FindSumWithCast
j'ai changé
if (o is int)
À
if (o is int?)
Qui ralentit également considérablement l'exécution. La seule différence dans IL que je peux voir est que
isinst [mscorlib]System.Int32
Devient
isinst valuetype [mscorlib]System.Nullable`1<int32>
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'opérateur C# as
émettra une instruction isinst
IL (tout comme l'opérateur is
). (Une autre instruction intéressante est castclass
, émise lorsque vous faites une distribution 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):
Format: isinst typeTok
typeTok est un jeton de métadonnées (un
typeref
,typedef
outypespec
), indiquant la classe désirée.Si typeTok est un non nullable type de valeur ou un paramètre générique, elle est interprétée comme une "boîte" typeTok.
Si typeTok est un type nullable,
Nullable<T>
, il est interprété comme "boxed"T
Le Plus important:
Si le type réel (pas le vérificateur suivi type) de obj est verifier-cessible à le type typeTok alors
isinst
réussit et obj (comme résultat) est retourné à l'identique, bien que la vérification des pistes son type typeTok. Contrairement aux coercions (§1.6) et aux conversions (§3.27),isinst
ne modifie jamais le type réel d'un objet et préserve l'identité de l'objet (voir Partition I).
Donc, le tueur de performance n'est pas isinst
dans ce cas, mais le unbox.any
supplémentaire. Ce N'était pas clair de la réponse de Hans, comme il a regardé le code JITed seulement. En général, le compilateur C# émet un unbox.any
après un isinst T?
(mais l'omettre dans le cas où vous n' isinst T
lorsque T
est un type de référence).
Pourquoi faut-il faire? isinst T?
n'a jamais l'effet qui aurait été évident, c'est-à-dire que vous récupérez un T?
. Au lieu de cela, toutes ces instructions garantissent que vous avez un "boxed T"
qui peut être déballé à T?
. Pour obtenir un T?
réel, nous devons toujours déballer notre "boxed T"
à T?
, qui est pourquoi le compilateur émet un unbox.any
après isinst
. Si vous y réfléchissez, cela a du sens car le "format de boîte" pour T?
est juste un "boxed T"
et faire castclass
et isinst
Effectuer l'unbox serait incohérent.
Sauvegarde de la découverte de Hans avec quelques informations de la norme , voici:
(ECMA 335 Partition III, 4.33): unbox.any
Lorsqu'elle est appliquée à la forme encadrée d'un type de valeur, l'instruction
unbox.any
extrait la valeur contenue dans obj (du typeO
). (Il est équivalent àunbox
suivi deldobj
.) Lorsqu'elle est appliquée à un type de référence, l'instructionunbox.any
a le même effet quecastclass
typeTok.
(ECMA 335 Partition III, 4.32): unbox
Typiquement,
unbox
calcule simplement l'adresse du type de valeur qui est déjà présent à l'intérieur de l'objet encadré. Cette approche n'est pas possible lors du déballage des types de valeur nullable. Parce que les valeursNullable<T>
sont converties enTs
en boîte pendant la boîte opération, une implémentation doit souvent fabriquer un nouveauNullable<T>
sur le tas et calculer l'adresse de l'objet nouvellement alloué.
Fait intéressant, j'ai transmis des commentaires sur le support de l'opérateur via dynamic
étant un ordre de grandeur plus lent pour Nullable<T>
(similaire à ce test précoce) - je soupçonne pour des raisons très similaires.
Je dois aimer Nullable<T>
. Un autre plaisir est que même si le JIT spots (et supprime) null
pour les structures non nullables, il le bore pour Nullable<T>
:
using System;
using System.Diagnostics;
static class Program {
static void Main() {
// JIT
TestUnrestricted<int>(1,5);
TestUnrestricted<string>("abc",5);
TestUnrestricted<int?>(1,5);
TestNullable<int>(1, 5);
const int LOOP = 100000000;
Console.WriteLine(TestUnrestricted<int>(1, LOOP));
Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
Console.WriteLine(TestNullable<int>(1, LOOP));
}
static long TestUnrestricted<T>(T x, int loop) {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
static long TestNullable<T>(T? x, int loop) where T : struct {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
}
Ceci est le résultat de FindSumWithAsAndHas ci-dessus: alt text http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
Ceci est le résultat de FindSumWithCast: alt text http://www.freeimagehosting.net/uploads/ce8a5a3934.png
Résultats:
-
En utilisant
as
, Il teste d'abord si un objet est une instance de Int32; sous le capot, il utiliseisinst Int32
(qui est similaire au code écrit à la main: if (o est int)). Et en utilisantas
, il a également unbox inconditionnellement l'objet. Et c'est un vrai tueur de performance d'appeler une propriété (c'est toujours une fonction sous le capot), IL_0027 -
En utilisant cast, vous testez d'abord si l'objet est un
int
if (o is int)
; sous le capot, cela utiliseisinst Int32
. Si c'est une instance de int, alors vous pouvez décompresser la valeur, IL_002D
En termes simples, c'est le pseudo-code d'utilisation de l'approche as
:
int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
Et c'est le pseudo-code d'utilisation de cast approche:
if (o isinst Int32)
sum += (o unbox Int32)
Donc, le cast ((int)a[i]
, bien 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 suffit de déballer une valeur quand un objet est décidément un int
. La même chose ne peut pas être dite à l'aide d'une approche as
.
Profilage plus loin:
using System;
using System.Diagnostics;
class Program
{
const int Size = 30000000;
static void Main(string[] args)
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i + 1] = "";
values[i + 2] = 1;
}
FindSumWithIsThenCast(values);
FindSumWithAsThenHasThenValue(values);
FindSumWithAsThenHasThenCast(values);
FindSumWithManualAs(values);
FindSumWithAsThenManualHasThenValue(values);
Console.ReadLine();
}
static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += (int)o;
}
}
sw.Stop();
Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithManualAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
bool hasValue = o is int;
int x = hasValue ? (int)o : 0;
if (hasValue)
{
sum += x;
}
}
sw.Stop();
Console.WriteLine("Manual As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenManualHasThenValue(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
}
Sortie:
Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282
, Que pouvons-nous déduire de ces chiffres?
- tout d'abord, l'approche is-then-cast est significativement plus rapide que comme approche. 303 vs 3524
- Deuxièmement,.La valeur est légèrement plus lente que la coulée. 3524 vs 3272
- troisième, .HasValue est légèrement plus lent que l'utilisation de Manual has (c'est-à-dire que l'utilisation de est). 3524 vs 3282
- Quatrièmement, faire une comparaison apple-to-apple(c'est-à-dire à la fois l'attribution de simulé HasValue et la conversion de la Valeur simulée arrive ensemble) entre simulé et réel que approche, nous pouvons voir simulé, qui est encore nettement plus rapide que réel que. 395 vs 3524
- enfin, basé sur la première et la quatrième conclusion, il y a quelque chose qui ne va pas avec as mise en œuvre ^_ ^
Je n'ai pas le temps de l'essayer, mais vous voudrez peut-être avoir:
foreach (object o in values)
{
int? x = o as int?;
Comme
int? x;
foreach (object o in values)
{
x = o as int?;
Vous créez un nouvel objet à chaque fois, ce qui n'expliquera pas complètement le problème, mais peut contribuer.
J'ai essayé la construction de vérification de type exacte
typeof(int) == item.GetType()
, qui fonctionne aussi vite que la version item is int
, et renvoie toujours le nombre (emphase: même si vous avez écrit un Nullable<int>
dans le tableau, vous devrez utiliser typeof(int)
). Vous avez également besoin d'une vérification supplémentaire null != item
ici.
Cependant
typeof(int?) == item.GetType()
reste rapide (contrairement à item is int?
), mais renvoie toujours false.
La typeof-construct est à mes yeux le moyen le plus rapide pour la vérification de type exacte, car elle utilise le Propriétaire du. Puisque les types exacts dans ce cas ne correspondent pas à nullable, je suppose que is/as
doit faire un heavylifting Supplémentaire ici pour s'assurer qu'il s'agit en fait d'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 aller 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 retournera faux.
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);
FindSumWithAsAndHas(values);
FindSumWithAsAndIs(values);
FindSumWithIsThenAs(values);
FindSumWithIsThenConvert(values);
FindSumWithLinq(values);
Console.ReadLine();
}
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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsAndIs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As and Is: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenAs(object[] values)
{
// Apple-to-apple comparison with Cast routine above.
// Using the similar steps in Cast routine above,
// the AS here cannot be slower than Linq.
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int? x = o as int?;
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("Is then As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenConvert(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = Convert.ToInt32(o);
sum += x;
}
}
sw.Stop();
Console.WriteLine("Is then Convert: {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);
}
}
Sorties:
Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811
[MODIFIER: 2010-06-19]
Remarque: le test précédent a été effectué dans VS, configuration debug, en utilisant Vs2009, en utilisant Core i7 (machine de développement d'entreprise).
Ce qui suit a été fait sur ma machine en utilisant Core 2 Duo, en utilisant VS2010
Inside VS, Configuration: Debug
Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018
Outside VS, Configuration: Debug
Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944
Inside VS, Configuration: Release
Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932
Outside VS, Configuration: Release
Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Afin de garder cette réponse à jour, il convient de mentionner que la plupart des discussions sur cette page sont maintenant discutables 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...
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
// ...use x.Value in here
}
Devient simplement...
if (o is int x)
{
// ...use x in here
}
J'ai trouvé qu'une utilisation courante pour la nouvelle syntaxe est lorsque vous écrivez un type de valeur. net (c'est-à-dire struct
dans C#) qui implémente IEquatable<MyStruct>
(comme la plupart). Après avoir implémenté la méthode Equals(MyStruct other)
fortement typée, vous pouvez maintenant rediriger gracieusement la substitution Equals(Object obj)
non typée (héritée de Object
) comme suit:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Annexe: Le Release
construire IL le code pour les deux premiers exemples de fonctions ci-dessus dans cette réponse (respectivement) sont donnés ici. Alors que le code IL pour la nouvelle syntaxe est en effet 1 octet plus petit, il gagne surtout gros en faisant zéro appel (vs deux) et éviter complètement l'opération unbox
lorsque cela est possible.
// static void test1(Object o, ref int y)
// {
// int? x = o as int?;
// if (x.HasValue)
// y = x.Value;
// }
[0] valuetype [mscorlib]Nullable`1<int32> x
ldarg.0
isinst [mscorlib]Nullable`1<int32>
unbox.any [mscorlib]Nullable`1<int32>
stloc.0
ldloca.s x
call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
brfalse.s L_001e
ldarg.1
ldloca.s x
call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
stind.i4
L_001e: ret
// static void test2(Object o, ref int y)
// {
// if (o is int x)
// y = x;
// }
[0] int32 x,
[1] object obj2
ldarg.0
stloc.1
ldloc.1
isinst int32
ldnull
cgt.un
dup
brtrue.s L_0011
ldc.i4.0
br.s L_0017
L_0011: ldloc.1
unbox.any int32
L_0017: stloc.0
brfalse.s L_001d
ldarg.1
ldloc.0
stind.i4
L_001d: ret
Pour d'autres tests qui corroborent ma remarque sur la performance de la nouvelle syntaxe C#7 dépassant les options précédemment disponibles, voir ici (en particulier, exemple 'D').