Pourquoi Where and Select surclasse-t-il simplement Select?

145

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 Selectet 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. Selectet Wheremaintenant 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 Selectet Wherevingt 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 Selectet 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.

Sélectionnez vs Sélectionnez et Où.

Cela montre que la Selectligne reste stable (attendue) et que la Select + Whereligne 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 Selectet 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 Sumla 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 Whereet Selectindiqué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.

It'sNotALie.
la source
1
Je dirais que cela dépend du coût de la somme et de l'accès mc.Value.
Medinoc
14
@ It'sNotALie. Where+ Selectne 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 blog
MarcinJuraszek
4
Intéressant. Permettez-moi de souligner qu'une boucle for sur un tableau serait 10 fois plus rapide que la meilleure solution LINQ. Donc, si vous partez à la recherche de la performance, n'utilisez pas LINQ en premier lieu.
usr
2
Parfois, les gens demandent après de vraies recherches, voici un exemple de question: je ne suis pas un utilisateur C # venu de Hot-question-list.
Grijesh Chauhan
2
@WiSaGaN C'est un bon point. Cependant, si cela est dû à un déménagement de branche vs conditionnel, nous nous attendrions à voir la différence la plus spectaculaire à 50% / 50%. Ici, nous voyons les différences les plus dramatiques aux extrémités, là où la ramification est la plus prévisible. Si le Where est une branche et que le ternaire est un mouvement conditionnel, alors nous nous attendrions à ce que les heures de Where redescendent lorsque tous les éléments sont valides, mais cela ne redescend jamais.
John Tseng

Réponses:

131

Selectitè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+Selectcrée un itérateur qui ignore les éléments non valides (pas yieldeux), en effectuant un +uniquement sur les éléments valides.

Donc, le coût pour a Selectest:

t(s) = n * ( cost(check valid) + cost(+) )

Et pour Where+Select:

t(ws) = n * ( cost(check valid) + p(valid) * (cost(yield) + cost(+)) )

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' whereitérateur, qui est plus complexe que l'itérateur simple Selectutilisé par la version.

Comme vous pouvez le voir, pour une donnée n, la Selectversion est une constante, alors que la Where+Selectversion est une équation linéaire avec p(valid)comme variable. Les valeurs réelles des coûts déterminent le point d'intersection des deux lignes, et comme cost(yield)peuvent être différentes de cost(+), elles ne se croisent pas nécessairement à p(valid)= 0,5.

Alex
la source
34
+1 pour être la seule réponse (jusqu'à présent) qui répond réellement à la question, ne devine pas la réponse et ne génère pas seulement "moi aussi!" statistiques.
Binary Worrier
4
Techniquement, les méthodes LINQ créent des arborescences d'expressions qui sont exécutées une fois sur l'ensemble de la collection au lieu de "ensembles".
Spoike
Quoi cost(append)? Vraiment bonne réponse cependant, regarde-la sous un angle différent plutôt que juste des statistiques.
n'est pasALie.
5
Wherene crée rien, retourne juste un élément à la fois de la sourceséquence si seulement il remplit le prédicat.
MarcinJuraszek
13
@Spoike - Les arborescences d'expressions ne sont pas pertinentes ici, car il s'agit de linq-to-objects , pas de linq-to-something-else (Entity, par exemple). C'est la différence entre IEnumerable.Select(IEnumerable, Func)et IQueryable.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.
Kobi
33

Voici une explication détaillée de ce qui cause les différences de synchronisation.


La Sum()fonction pour IEnumerable<int>ressemble à ceci:

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;
    foreach(int item in source)
    {
        sum += item;
    }
    return sum;
}

En C #, ce foreachn'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>

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;

    IEnumerator<int> iterator = source.GetEnumerator();
    while(iterator.MoveNext())
    {
        int item = iterator.Current;
        sum += item;
    }
    return sum;
}

N'oubliez pas que les deux lignes de code que vous comparez sont les suivantes

int result1 = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
int result2 = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

Maintenant, voici le kicker:

LINQ utilise une exécution différée . Ainsi, bien qu'il puisse sembler qu'il result1répète deux fois la collection, il ne l'itère qu'une seule fois. La Where()condition est effectivement appliquée pendant le Sum(), à l'intérieur de l'appel à MoveNext() (Ceci est possible grâce à la magie de yield return) .

Cela signifie que, pour result1, le code à l'intérieur de la whileboucle,

{
    int item = iterator.Current;
    sum += item;
}

n'est exécuté qu'une seule fois pour chaque élément avec mc.IsValid == true. Par comparaison, result2exécutera ce code pour chaque élément de la collection. C'est pourquoi result1est généralement plus rapide.

(Cependant, notez que l'appel de la Where()condition à l'intérieur a MoveNext()encore une petite surcharge, donc si la plupart / tous les éléments l'ont mc.IsValid == true, ce result2sera en fait plus rapide!)


Espérons que maintenant, il est clair pourquoi result2est 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:

  1. Utilisez LINQ partout.
  2. Profil.
  3. Si le profileur dit que LINQ est la cause d'un goulot d'étranglement, réécrivez ce morceau de code sans LINQ.

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.

BlueRaja - Danny Pflughoeft
la source
3
Petit addendum: la première réponse a été corrigée.
n'est pasALie.
16

Chose amusante. Savez-vous comment est Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)défini? Il utilise la Selectméthode!

public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
{
    return source.Select(selector).Sum();
}

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:

Where -- mod: 1 result: 0, time: 371 ms
WhereSelect -- mod: 1  result: 0, time: 356 ms
Select -- mod: 1  result 0, time: 366 ms
Sum -- mod: 1  result: 0, time: 363 ms
-------------
Where -- mod: 2 result: 4999999, time: 469 ms
WhereSelect -- mod: 2  result: 4999999, time: 429 ms
Select -- mod: 2  result 4999999, time: 362 ms
Sum -- mod: 2  result: 4999999, time: 358 ms
-------------
Where -- mod: 3 result: 9999999, time: 441 ms
WhereSelect -- mod: 3  result: 9999999, time: 452 ms
Select -- mod: 3  result 9999999, time: 371 ms
Sum -- mod: 3  result: 9999999, time: 380 ms
-------------
Where -- mod: 4 result: 7500000, time: 571 ms
WhereSelect -- mod: 4  result: 7500000, time: 501 ms
Select -- mod: 4  result 7500000, time: 406 ms
Sum -- mod: 4  result: 7500000, time: 397 ms
-------------
Where -- mod: 5 result: 7999999, time: 490 ms
WhereSelect -- mod: 5  result: 7999999, time: 477 ms
Select -- mod: 5  result 7999999, time: 397 ms
Sum -- mod: 5  result: 7999999, time: 394 ms
-------------
Where -- mod: 6 result: 9999999, time: 488 ms
WhereSelect -- mod: 6  result: 9999999, time: 480 ms
Select -- mod: 6  result 9999999, time: 391 ms
Sum -- mod: 6  result: 9999999, time: 387 ms
-------------
Where -- mod: 7 result: 8571428, time: 489 ms
WhereSelect -- mod: 7  result: 8571428, time: 486 ms
Select -- mod: 7  result 8571428, time: 384 ms
Sum -- mod: 7  result: 8571428, time: 381 ms
-------------
Where -- mod: 8 result: 8749999, time: 494 ms
WhereSelect -- mod: 8  result: 8749999, time: 488 ms
Select -- mod: 8  result 8749999, time: 386 ms
Sum -- mod: 8  result: 8749999, time: 373 ms
-------------
Where -- mod: 9 result: 9999999, time: 497 ms
WhereSelect -- mod: 9  result: 9999999, time: 494 ms
Select -- mod: 9  result 9999999, time: 386 ms
Sum -- mod: 9  result: 9999999, time: 371 ms

Pour les implémentations suivantes:

result = source.Where(x => x.IsValid).Sum(x => x.Value);
result = source.Select(x => x.IsValid ? x.Value : 0).Sum();
result = source.Sum(x => x.IsValid ? x.Value : 0);
result = source.Where(x => x.IsValid).Select(x => x.Value).Sum();

modsignifie: chaque 1 des modéléments est invalide: pour mod == 1chaque élément est invalide, pour mod == 2les éléments impairs sont invalides, etc. La collection contient des 10000000éléments.

entrez la description de l'image ici

Et résultats pour la collecte avec des 100000000objets:

entrez la description de l'image ici

Comme vous pouvez le voir, Selectet les Sumrésultats sont tout à fait cohérente sur l' ensemble des modvaleurs. Cependant whereet where+ selectne le sont pas.

MarcinJuraszek
la source
1
C'est très intéressant que dans vos résultats, toutes les méthodes commencent au même endroit et divergent, alors que les résultats de It'sNotALie se croisent au milieu.
John Tseng
6

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é.

Stilgar
la source
Certains tests avec en uncheckedfont un tout petit peu meilleur pour le Select.
n'est pasALie.
Quelqu'un peut-il dire que si la case n'est pas cochée, cela affecte les méthodes appelées dans la pile ou uniquement les opérations de niveau supérieur?
Stilgar
1
@Stilgar Cela s'applique uniquement au niveau supérieur.
Branko Dimitrijevic
Alors peut-être que nous devons implémenter Sum non coché et l'essayer de cette façon.
Stilgar
5

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.

static void Main(string[] args)
        {
            int total = 10000000;
            Random r = new Random();
            var list = Enumerable.Range(0, total).Select(i => r.Next(0, 5)).ToList();
            for (int i = 0; i < 4000000; i++)
                list[i] = 10;

            var sw = new Stopwatch();
            sw.Start();

            int sum = 0;

            sum = list.Where(i => i < 10).Select(i => i).Sum();            

            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);

            sw.Reset();
            sw.Start();
            sum = list.Select(i => i).Sum();            

            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds);
        }
DavidN
la source
N'est-ce pas parce que vous ne jetez pas les sous-dix dans le Select?
n'est pasALie.
3
L'exécution en débogage est inutile.
MarcinJuraszek
1
@MarcinJuraszek De toute évidence. Vraiment voulu dire que j'ai couru en sortie :)
DavidN
@ It'sNotALie C'est le point. Il me semble que la seule façon dont Where + Select peut surpasser Select est lorsque Where filtre une grande quantité d'éléments additionnés.
DavidN
2
C'est essentiellement ce que dit ma question. Ils se égalent à environ 60%, comme le fait cet échantillon. La question est de savoir pourquoi, ce qui n'est pas répondu ici.
n'est pasALie.
4

Si vous avez besoin de vitesse, faire une simple boucle est probablement votre meilleur pari. Et faire a fortendance à ê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:

Where + Select + Sum:   257
Select + Sum:           253
foreach:                111
for:                    61

Et avec 90% d'éléments invalides:

Where + Select + Sum:   177
Select + Sum:           247
foreach:                105
for:                    58

Et voici mon code de référence ...

public class MyClass {
    public int Value { get; set; }
    public bool IsValid { get; set; }
}

class Program {

    static void Main(string[] args) {

        const int count = 10000000;
        const int percentageInvalid = 90;

        var rnd = new Random();
        var myCollection = new List<MyClass>(count);
        for (int i = 0; i < count; ++i) {
            myCollection.Add(
                new MyClass {
                    Value = rnd.Next(0, 50),
                    IsValid = rnd.Next(0, 100) > percentageInvalid
                }
            );
        }

        var sw = new Stopwatch();
        sw.Restart();
        int result1 = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
        sw.Stop();
        Console.WriteLine("Where + Select + Sum:\t{0}", sw.ElapsedMilliseconds);

        sw.Restart();
        int result2 = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
        sw.Stop();
        Console.WriteLine("Select + Sum:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result2);

        sw.Restart();
        int result3 = 0;
        foreach (var mc in myCollection) {
            if (mc.IsValid)
                result3 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("foreach:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result3);

        sw.Restart();
        int result4 = 0;
        for (int i = 0; i < myCollection.Count; ++i) {
            var mc = myCollection[i];
            if (mc.IsValid)
                result4 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("for:\t\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result4);

    }

}

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ù».

Branko Dimitrijevic
la source
1

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.

static void Main(string[] args)
{
    var set = Enumerable.Range(1, 10000000)
                        .Select(i => new MyClass {Value = i, IsValid = i%2 == 0})
                        .ToList();

    Func<MyClass, int> select = i => i.IsValid ? i.Value : 0;
    Console.WriteLine(
        Sum(                        // Cost: N additions
            Select(set, select)));  // Cost: N delegate
    // Total cost: N * (delegate + addition) = Nd + Na

    Func<MyClass, bool> where = i => i.IsValid;
    Func<MyClass, int> wSelect = i => i.Value;
    Console.WriteLine(
        Sum(                        // Cost: M additions
            Select(                 // Cost: M delegate
                Where(set, where),  // Cost: N delegate
                wSelect)));
    // Total cost: N * delegate + M * (delegate + addition) = Nd + Md + Ma
}

// Cost: N delegate calls
static IEnumerable<T> Where<T>(IEnumerable<T> set, Func<T, bool> predicate)
{
    foreach (var mc in set)
    {
        if (predicate(mc))
        {
            yield return mc;
        }
    }
}

// Cost: N delegate calls
static IEnumerable<int> Select<T>(IEnumerable<T> set, Func<T, int> selector)
{
    foreach (var mc in set)
    {
        yield return selector(mc);
    }
}

// Cost: N additions
static int Sum(IEnumerable<int> set)
{
    unchecked
    {
        var sum = 0;
        foreach (var i in set)
        {
            sum += i;
        }

        return sum;
    }
}
Jon Norton
la source
0

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 Sumfonction 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 fichierWhereSelectListIterator . Cela signifie que les itérateurs réels générés sont les mêmes. Dans les deux cas, il existe une Sumboucle sur un itérateur, le WhereSelectListIterator. 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:

  1. ( n+ m) * Appeler + m*checked+=
  2. 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.

John Tseng
la source
@ It'sNotALie. Je ne pense pas que quiconque ait un mauvais résultat. Je ne pouvais tout simplement pas expliquer certaines choses. J'avais supposé que le coût d'Invoke était beaucoup plus élevé que celui de + =, mais il est concevable qu'ils puissent être beaucoup plus proches en fonction des optimisations matérielles.
John Tseng