J'ai une classe, comme celle-ci:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
En fait, c'est beaucoup plus gros, mais cela recrée le problème (bizarrerie).
Je veux obtenir la somme des Value
, où l'instance est valide. Jusqu'à présent, j'ai trouvé deux solutions à cela.
Le premier est celui-ci:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
La deuxième, cependant, est la suivante:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Je veux obtenir la méthode la plus efficace. J'ai d'abord pensé que le second serait plus efficace. Ensuite, la partie théorique de moi a commencé à dire "Eh bien, l'un est O (n + m + m), l'autre est O (n + n). Le premier devrait mieux fonctionner avec plus d'invalides, tandis que le second devrait mieux fonctionner avec moins". Je pensais qu'ils fonctionneraient également. EDIT: Et puis @Martin a souligné que le Where et le Select étaient combinés, donc cela devrait en fait être O (m + n). Cependant, si vous regardez ci-dessous, il semble que cela ne soit pas lié.
Alors je l'ai mis à l'épreuve.
(C'est plus de 100 lignes, alors j'ai pensé qu'il valait mieux l'afficher sous forme de résumé.)
Les résultats étaient ... intéressants.
Avec 0% de tolérance de cravate:
Les échelles sont en faveur de Select
et Where
, d'environ ~ 30 points.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
Avec une tolérance de cravate de 2%:
C'est la même chose, sauf que pour certains, ils se situaient à moins de 2%. Je dirais que c'est une marge d'erreur minimale. Select
et Where
maintenant seulement une avance d'environ 20 points.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
Avec une tolérance de cravate de 5%:
C'est ce que je dirais être ma marge d'erreur maximale. Cela le rend un peu meilleur pour le Select
, mais pas beaucoup.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
Avec 10% de tolérance de cravate:
C'est loin de ma marge d'erreur, mais je suis toujours intéressé par le résultat. Parce qu'il donne Select
et Where
vingt points d' avance , il a eu pendant un certain temps maintenant.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
Avec 25% de tolérance de cravate:
C'est ainsi, chemin de ma marge d'erreur, mais je suis toujours intéressé par le résultat, parce que la Select
et Where
encore (presque) ont conservé leur avance de 20 points. Il semble qu'il le surclasse dans quelques-uns, et c'est ce qui lui donne la tête.
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Maintenant, je devine que la tête de 20 points est venu du milieu, où ils sont tous deux liés à obtenir autour de la même performance. Je pourrais essayer de l'enregistrer, mais ce serait tout un tas d'informations à intégrer. Un graphique serait mieux, je suppose.
C'est donc ce que j'ai fait.
Cela montre que la Select
ligne reste stable (attendue) et que la Select + Where
ligne monte (attendue). Cependant, ce qui me laisse perplexe, c'est pourquoi il ne rencontre pas le Select
à 50 ou avant: en fait, je m'attendais à plus de 50, car un énumérateur supplémentaire devait être créé pour le Select
et Where
. Je veux dire, cela montre l'avance de 20 points, mais cela n'explique pas pourquoi. Tel est, je suppose, le point principal de ma question.
Pourquoi se comporte-t-il ainsi? Dois-je lui faire confiance? Sinon, devrais-je utiliser l'autre ou celui-ci?
Comme @KingKong l'a mentionné dans les commentaires, vous pouvez également utiliser Sum
la surcharge de qui prend un lambda. Donc, mes deux options sont maintenant modifiées pour ceci:
Première:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Seconde:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Je vais le raccourcir un peu, mais:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
L'avance de vingt points est toujours là, ce qui signifie qu'elle n'a pas à voir avec la combinaison Where
et Select
indiquée par @Marcin dans les commentaires.
Merci d'avoir lu mon mur de texte! De plus, si vous êtes intéressé, voici la version modifiée qui enregistre le CSV qu'Excel prend.
mc.Value
.Where
+Select
ne provoque pas deux itérations séparées sur la collection d'entrée. LINQ to Objects l'optimise en une seule itération. Lire la suite sur mon article de blogRéponses:
Select
itère une fois sur l'ensemble de l'ensemble et, pour chaque élément, effectue une branche conditionnelle (vérification de la validité) et une+
opération.Where+Select
crée un itérateur qui ignore les éléments non valides (pasyield
eux), en effectuant un+
uniquement sur les éléments valides.Donc, le coût pour a
Select
est:Et pour
Where+Select
:Où:
p(valid)
est la probabilité qu'un élément de la liste soit valide.cost(check valid)
est le coût de la succursale qui vérifie la validitécost(yield)
est le coût de construction du nouvel état de l'where
itérateur, qui est plus complexe que l'itérateur simpleSelect
utilisé par la version.Comme vous pouvez le voir, pour une donnée
n
, laSelect
version est une constante, alors que laWhere+Select
version est une équation linéaire avecp(valid)
comme variable. Les valeurs réelles des coûts déterminent le point d'intersection des deux lignes, et commecost(yield)
peuvent être différentes decost(+)
, elles ne se croisent pas nécessairement àp(valid)
= 0,5.la source
cost(append)
? Vraiment bonne réponse cependant, regarde-la sous un angle différent plutôt que juste des statistiques.Where
ne crée rien, retourne juste un élément à la fois de lasource
séquence si seulement il remplit le prédicat.IEnumerable.Select(IEnumerable, Func)
etIQueryable.Select(IQueryable, Expression<Func>)
. Vous avez raison de dire que LINQ ne fait «rien» tant que vous ne parcourez pas la collection, ce que vous vouliez probablement dire.Voici une explication détaillée de ce qui cause les différences de synchronisation.
La
Sum()
fonction pourIEnumerable<int>
ressemble à ceci:En C #, ce
foreach
n'est que du sucre syntaxique pour la version .Net d'un itérateur, (à ne pas confondre avec ) . Donc, le code ci-dessus est en fait traduit en ceci:IEnumerator<T>
IEnumerable<T>
N'oubliez pas que les deux lignes de code que vous comparez sont les suivantes
Maintenant, voici le kicker:
LINQ utilise une exécution différée . Ainsi, bien qu'il puisse sembler qu'il
result1
répète deux fois la collection, il ne l'itère qu'une seule fois. LaWhere()
condition est effectivement appliquée pendant leSum()
, à l'intérieur de l'appel àMoveNext()
(Ceci est possible grâce à la magie deyield return
) .Cela signifie que, pour
result1
, le code à l'intérieur de lawhile
boucle,n'est exécuté qu'une seule fois pour chaque élément avec
mc.IsValid == true
. Par comparaison,result2
exécutera ce code pour chaque élément de la collection. C'est pourquoiresult1
est généralement plus rapide.(Cependant, notez que l'appel de la
Where()
condition à l'intérieur aMoveNext()
encore une petite surcharge, donc si la plupart / tous les éléments l'ontmc.IsValid == true
, ceresult2
sera en fait plus rapide!)Espérons que maintenant, il est clair pourquoi
result2
est généralement plus lent. Maintenant, j'aimerais expliquer pourquoi j'ai déclaré dans les commentaires que ces comparaisons de performances LINQ n'ont pas d'importance .Créer une expression LINQ est bon marché. L'appel de fonctions de délégué est bon marché. L'allocation et le bouclage d'un itérateur sont bon marché. Mais c'est encore moins cher de ne pas faire ces choses. Ainsi, si vous trouvez qu'une instruction LINQ est le goulot d'étranglement dans votre programme, d'après mon expérience, la réécrire sans LINQ la rendra toujours plus rapide que l'une des différentes méthodes LINQ.
Ainsi, votre flux de travail LINQ devrait ressembler à ceci:
Heureusement, les goulots d'étranglement LINQ sont rares. Heck, les goulots d'étranglement sont rares. J'ai écrit des centaines d'instructions LINQ ces dernières années et j'ai fini par remplacer <1%. Et la plupart de ces problèmes étaient dus à la mauvaise optimisation SQL de LINQ2EF , plutôt qu'à la faute de LINQ.
Donc, comme toujours, écrivez d'abord un code clair et sensé, et attendez après avoir profilé pour vous soucier des micro-optimisations.
la source
Chose amusante. Savez-vous comment est
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
défini? Il utilise laSelect
méthode!Donc en fait, tout devrait fonctionner à peu près de la même manière. J'ai fait des recherches rapides par moi-même, et voici les résultats:
Pour les implémentations suivantes:
mod
signifie: chaque 1 desmod
éléments est invalide: pourmod == 1
chaque élément est invalide, pourmod == 2
les éléments impairs sont invalides, etc. La collection contient des10000000
éléments.Et résultats pour la collecte avec des
100000000
objets:Comme vous pouvez le voir,
Select
et lesSum
résultats sont tout à fait cohérente sur l' ensemble desmod
valeurs. Cependantwhere
etwhere
+select
ne le sont pas.la source
Je suppose que la version avec Where filtre les 0 et ils ne sont pas un sujet pour Sum (c'est-à-dire que vous n'exécutez pas l'ajout). C'est bien sûr une supposition car je ne peux pas expliquer comment l'exécution d'une expression lambda supplémentaire et l'appel de plusieurs méthodes surpassent une simple addition d'un 0.
Un de mes amis a suggéré que le fait que le 0 dans la somme puisse entraîner de graves pénalités de performances en raison des contrôles de dépassement de capacité. Il serait intéressant de voir comment cela fonctionnerait dans un contexte non contrôlé.
la source
unchecked
font un tout petit peu meilleur pour leSelect
.En exécutant l'exemple suivant, il devient clair pour moi que le seul moment où Where + Select peut surpasser Select est en fait lorsqu'il rejette une bonne quantité (environ la moitié dans mes tests informels) des éléments potentiels de la liste. Dans le petit exemple ci-dessous, j'obtiens à peu près les mêmes nombres des deux échantillons lorsque le Where saute environ 4 mil éléments sur 10 mil. J'ai couru dans la version et réorganisé l'exécution de where + select vs select avec les mêmes résultats.
la source
Select
?Si vous avez besoin de vitesse, faire une simple boucle est probablement votre meilleur pari. Et faire a
for
tendance à être mieux queforeach
(en supposant que votre collection soit à accès aléatoire, bien sûr).Voici les horaires que j'ai obtenus avec 10% d'éléments invalides:
Et avec 90% d'éléments invalides:
Et voici mon code de référence ...
BTW, je suis d'accord avec la supposition de Stilgar : les vitesses relatives de vos deux cas varient en fonction du pourcentage d'éléments invalides, simplement parce que la quantité de travail
Sum
à faire varie dans le cas «Où».la source
Plutôt que d'essayer d'expliquer via la description, je vais adopter une approche plus mathématique.
Étant donné le code ci-dessous qui devrait approximer ce que LINQ fait en interne, les coûts relatifs sont les suivants:
Sélectionnez uniquement:
Nd + Na
Où + Sélectionnez:
Nd + Md + Ma
Pour déterminer le point où ils vont se croiser, nous devons faire un peu d'algèbre:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
Cela signifie que pour que le point d'inflexion soit à 50%, le coût d'une invocation de délégué doit être à peu près le même que le coût d'un ajout. Puisque nous savons que le point d'inflexion réel était d'environ 60%, nous pouvons travailler à rebours et déterminer que le coût d'une invocation de délégué pour @ It'sNotALie était en fait d'environ 2/3 du coût d'un ajout, ce qui est surprenant, mais c'est ce que ses chiffres disent.
la source
Je trouve intéressant que le résultat de MarcinJuraszek soit différent de celui d'It'sNotALie. En particulier, les résultats de MarcinJuraszek commencent avec les quatre implémentations au même endroit, tandis que les résultats d'It'sNotALie se croisent vers le milieu. Je vais vous expliquer comment cela fonctionne à partir de la source.
Supposons qu'il y ait des
n
éléments totaux, etm
éléments valides.La
Sum
fonction est assez simple. Il parcourt simplement l'énumérateur: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Pour simplifier, supposons que la collection est une liste. Les deux Select et WhereSelect créeront un fichier
WhereSelectListIterator
. Cela signifie que les itérateurs réels générés sont les mêmes. Dans les deux cas, il existe uneSum
boucle sur un itérateur, leWhereSelectListIterator
. La partie la plus intéressante de l'itérateur est le MoveNext méthode .Puisque les itérateurs sont les mêmes, les boucles sont les mêmes. La seule différence réside dans le corps des boucles.
Le corps de ces lambdas a un coût très similaire. La clause where renvoie une valeur de champ et le prédicat ternaire renvoie également une valeur de champ. La clause select renvoie une valeur de champ et les deux branches de l'opérateur ternaire renvoient soit une valeur de champ, soit une constante. La clause de sélection combinée a la branche comme opérateur ternaire, mais WhereSelect utilise la branche dans
MoveNext
.Cependant, toutes ces opérations sont assez bon marché. L'opération la plus coûteuse à ce jour est la succursale, où une mauvaise prédiction nous coûtera.
Une autre opération coûteuse ici est le
Invoke
. L'appel d'une fonction prend un peu plus de temps que l'ajout d'une valeur, comme l'a montré Branko Dimitrijevic.L'accumulation vérifiée est également prise en compte
Sum
. Si le processeur n'a pas d'indicateur de débordement arithmétique, cela peut également être coûteux à vérifier.Par conséquent, les coûts intéressants sont: est:
n
+m
) * Appeler +m
*checked+=
n
* Invoquer +n
*checked+=
Ainsi, si le coût d'Invoke est beaucoup plus élevé que le coût de l'accumulation vérifiée, alors le cas 2 est toujours meilleur. S'ils sont à peu près égaux, nous verrons un équilibre lorsque la moitié environ des éléments sont valides.
On dirait que sur le système de MarcinJuraszek, coché + = a un coût négligeable, mais sur les systèmes de It'sNotALie et Branko Dimitrijevic, coché + = a des coûts importants. Il semble que ce soit le plus cher du système It'sNotALie puisque le seuil de rentabilité est beaucoup plus élevé. Il ne semble pas que quiconque ait publié les résultats d'un système où l'accumulation coûte beaucoup plus cher que l'invocation.
la source