Je suis en train de réviser le chapitre 4 de C # in Depth qui traite des types nullables, et j'ajoute une section sur l'utilisation de l'opérateur "en tant que", 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 génial et que cela pouvait améliorer les performances par rapport à l'équivalent C # 1, en utilisant "est" suivi d'un transt - après tout, de cette façon, nous n'avons à demander qu'un contrôle de type dynamique une fois, puis un contrôle de valeur simple. .
Cela ne semble pas être le cas, cependant. J'ai inclus un exemple d'application de test ci-dessous, qui additionne en gros tous les entiers d'un tableau d'objets, mais ce tableau contient de nombreuses références nulles et des chaînes, ainsi que des entiers encadrés. Le benchmark mesure le code que vous devez utiliser en C # 1, le code utilisant l'opérateur "en tant que", 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 "en".
L'implémentation .NET de isinst
pour les types nullables est-elle vraiment lente? Est-ce le supplément unbox.any
qui cause le problème? Y a-t-il une autre explication à cela? Pour le moment, j'ai l'impression qu'il va falloir inclure un avertissement contre l'utilisation de ce logiciel dans des situations sensibles aux performances ...
Résultats:
Cast: 10000000: 121
As: 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);
}
}
Clairement, 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, c'est qu'un objet ne peut être décompressé que dans une variable du même type que la valeur encadrée. Cela permet au compilateur JIT de générer un code très efficace, aucune conversion de valeur n’est à envisager.
Le test de l'opérateur is est simple. Il suffit de vérifier si l'objet n'est pas nul et s'il est du type attendu, il 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? prend beaucoup plus de travail. La représentation de la valeur de l’entier encadré n’est pas compatible avec la structure de la mémoire de Nullable<int>
. Une conversion est nécessaire et le code est délicat en raison des types d’énumération en boîte possibles. Le compilateur JIT génère un appel à une fonction d'assistance CLR appelée JIT_Unbox_Nullable pour que le travail soit effectué. C'est une fonction d'usage général pour tout type de valeur, il y a beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d’estimer le coût puisque ce code est verrouillé 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 la conversion. Il s’agit toutefois 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 un transtypage en un type de valeur arbitraire. Je n'ai pas une bonne explication pourquoi c'est aussi lent que le casting pour 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 très lent sur les types nullables. Dans la méthode FindSumWithCast
j'ai changé
if (o is int)
à
if (o is int?)
ce qui ralentit également considérablement l'exécution. La seule différence dans IL que je peux voir est que
isinst [mscorlib]System.Int32
se change en
isinst valuetype [mscorlib]System.Nullable`1<int32>
Cela a commencé par être un commentaire sur l'excellente réponse de Hans Passant, mais cela a pris trop de temps, je veux donc ajouter quelques éléments ici:
Tout d'abord, l'opérateur C # as
émettra une instruction isinst
IL (l'opérateur is
en fera de même). (Une autre instruction intéressante est castclass
, émise lorsque vous effectuez une diffusion directe et que le compilateur sait que la vérification à l'exécution ne peut pas être omise.)
Voici ce que fait isinst
( ECMA 335 Partition III, 4.6 ):
Format: isinst typeTok
typeTok est un jeton de métadonnées (un
typeref
,typedef
outypespec
), indiquant la classe souhaitée.Si typeTok est un type de valeur non nullable ou un type de paramètre générique, il est interprété comme "encadré" typeTok.
Si typeTok est un type nullable,
Nullable<T>
, il est interprété comme "encadré"T
Le plus important:
Si le type actuel (et non le type suivi par le vérificateur) de obj est vérificateur-assignable-à le type typeTok alors
isinst
réussit et = obj (comme résultat) est renvoyé sous forme inchangée pendant que la vérification en indique le type typeTok. Contrairement aux coercitions (§1.6) et aux conversions (§3.27),isinst
ne change jamais le type réel d'un objet et préserve l'identité de l'objet (voir Partition I).
Donc, le tueur à la performance n'est pas isinst
dans ce cas, mais le unbox.any
. Cela ne ressortait pas clairement de la réponse de Hans, qui ne regardait que le code JITed. En général, le compilateur C # émettra un unbox.any
après un isinst T?
_ (mais l'omettra si vous le faites isinst T
, lorsque T
est un type de référence).
Pourquoi ça fait ça? isinst T?
n’a jamais eu l’effet qui aurait été évident, c’est-à-dire que vous récupérerez 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 réel T?
, nous devons encore déballer notre "boxed T"
à T?
, raison pour laquelle 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?
n'est qu'un "boxed T"
et en faisant castclass
et isinst
effectuer la décompression serait incohérent.
Sauvegarde de la découverte de Hans avec des informations provenant du standard , voici ce qui se passe:
(ECMA 335 Partition III, 4.33): unbox.any
Appliqué à la forme encadrée d'un type de valeur, le
unbox.any
instruction extrait la valeur contenue dans obj (de typeO
). (Cela équivaut àunbox
suivi deldobj
.) Lorsqu'il est appliqué à un type de référence, leunbox.any
instruction a le même effet quecastclass
typeTok.
(ECMA 335 Partition III, 4.32): unbox
Généralement,
unbox
calcule simplement l'adresse du type de valeur déjà présent à l'intérieur de l'objet encadré. Cette approche n'est pas possible lorsque les types de valeur nullable ont été désencaissés. CarNullable<T>
Les valeurs sont converties enTs
en boîte lors de l’utilisation de la boîte, une implémentation doit souvent fabriquer un nouveauNullable<T>
sur le tas et calculez l'adresse du nouvel objet alloué.
Fait intéressant, j’ai transmis des commentaires sur le support des opérateurs via dynamic
étant un ordre de grandeur plus lent pour Nullable<T>
(semblable à ce test précoce ) - Je soupçonne pour des raisons très similaires.
Tu dois aimer Nullable<T>
. Une autre option amusante est que même si le JIT repère (et supprime) null
pour les structures non nullables, il le bloque 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:
Ceci est le résultat de FindSumWithCast:
Résultats:
En utilisant as
, il vérifie d’abord si un objet est une instance de Int32; sous le capot, il utilise isinst Int32
(ce qui est similaire au code écrit à la main: if (o est int)). Et en utilisant as
, il décompactera également l’objet de manière inconditionnelle. Et c’est un vrai tueur de performance que d’appeler une propriété (c’est toujours une fonction cachée), IL_0027
En utilisant cast, vous testez d'abord si l'objet est un int
if (o is int)
; sous le capot, cela utilise isinst Int32
. S'il s'agit d'une instance de int, vous pouvez alors décompresser la valeur en toute sécurité, IL_002D
En termes simples, c’est le pseudo-code de l’utilisation de l’approche as
:
int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
Et voici le pseudo-code de l'utilisation de l'approche cast:
if (o isinst Int32)
sum += (o unbox Int32)
Ainsi, la distribution ((int)a[i]
, La syntaxe ressemble à une distribution, mais en réalité, elle est unboxing, la distribution et la déballage partagent la même syntaxe, la prochaine fois que je serai pédant avec la bonne terminologie), l'approche est vraiment plus rapide, Nécessaire uniquement pour décompresser une valeur lorsqu'un objet est décidément un int
. On ne peut pas dire la même chose en utilisant une approche as
.
Profiler 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?
Afin de garder cette réponse à jour, il est utile de mentionner que l'essentiel de la discussion sur cette page est maintenant sans objet avec C # 7.1 et . NET 4.7 qui prend en charge une syntaxe fine qui produit également le meilleur code IL.
Exemple original du PO ...
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 constaté qu'une utilisation courante de la nouvelle syntaxe est lorsque vous écrivez un type de valeur .NET (c'est-à-dire struct
in C # ) qui implémente IEquatable<MyStruct>
(Comme la plupart devraient le faire). Après avoir implémenté la méthode Equals(MyStruct other)
fortement typée, vous pouvez désormais rediriger gracieusement la Equals(Object obj)
non typée _ (annulée de (héritée de Object
) vers elle comme suit:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Annexe: Le Release
build [~ # ~] il [~ # ~] les codes pour les deux premiers exemples de fonctions indiqués ci-dessus dans cette réponse (respectivement) sont donnés ici. Bien que le code IL de la nouvelle syntaxe soit effectivement inférieur de 1 octet, il gagne généralement gros en effectuant zéro appel (contre deux) et en évitant 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 des tests supplémentaires 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, exemple 'D').
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 retourne toujours le nombre (en italique: même si vous écrivez un Nullable<int>
dans le tableau, vous devrez utiliser typeof(int)
). Vous avez également besoin d'un chèque supplémentaire de null != item
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 du type exact, car il utilise le RuntimeTypeHandle. Puisque les types exacts dans ce cas ne correspondent pas à nullable, mon hypothèse est que is/as
Doit faire plus de poids lourd ici pour s’assurer qu’il s’agit bien d’une instance de type Nullable.
Et honnêtement: qu'est-ce que votre is Nullable<xxx> plus HasValue
Vous achète? Rien. Vous pouvez toujours aller directement au type sous-jacent (valeur) (dans ce cas). Vous obtenez soit la valeur, soit "non, pas une instance du type que vous demandiez". Même si vous avez écrit (int?)null
Dans le tableau, la vérification du type renverra false.
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 à chaque fois un nouvel objet, ce qui n'explique pas complètement le problème, mais peut y contribuer.
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);
}
}
Les 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
[EDIT: 2010-06-19]
Remarque: Le test précédent avait été effectué dans VS, débogage de la configuration, à l'aide de VS2009, à l'aide de Core i7 (machine de développement de la société).
Les opérations suivantes ont été effectuées sur ma machine avec Core 2 Duo, avec 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