Warum übertreffen Where and Select nur Select?

145

Ich habe eine Klasse wie diese:

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

Tatsächlich ist es viel größer, aber dies schafft das Problem (Verrücktheit) neu.

Ich möchte die Summe von erhalten Value, wo die Instanz gültig ist. Bisher habe ich zwei Lösungen dafür gefunden.

Der erste ist dieser:

int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();

Der zweite ist jedoch folgender:

int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();

Ich möchte die effizienteste Methode erhalten. Zuerst dachte ich, dass der zweite effizienter sein würde. Dann fing der theoretische Teil von mir an zu sagen: "Nun, einer ist O (n + m + m), der andere ist O (n + n). Der erste sollte mit mehr Invaliden besser abschneiden, während der zweite besser abschneiden sollte mit weniger". Ich dachte, dass sie gleich gut abschneiden würden. EDIT: Und dann wies @Martin darauf hin, dass Where und Select kombiniert wurden, also sollte es eigentlich O (m + n) sein. Wenn Sie jedoch nach unten schauen, scheint dies nicht miteinander verbunden zu sein.


Also habe ich es auf die Probe gestellt.

(Es sind mehr als 100 Zeilen, daher dachte ich, es wäre besser, es als Kern zu veröffentlichen.)
Die Ergebnisse waren ... interessant.

Mit 0% Bindungstoleranz:

Die Skalen sprechen für Selectund Whereum ~ 30 Punkte.

How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36

Mit 2% Bindungstoleranz:

Es ist das gleiche, außer dass sie für einige innerhalb von 2% lagen. Ich würde sagen, das ist eine minimale Fehlerquote. Selectund Wherejetzt haben nur noch ~ 20 Punkte Vorsprung.

How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37

Mit 5% Bindungstoleranz:

Dies würde ich als meine maximale Fehlerquote bezeichnen. Es macht es ein bisschen besser für die Select, aber nicht viel.

How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31

Mit 10% Bindungstoleranz:

Dies ist ein Ausweg aus meiner Fehlerquote, aber ich bin immer noch am Ergebnis interessiert. Weil es den Selectund Whereden 20-Punkte-Vorsprung gibt, den es schon eine Weile hat.

How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21

Mit 25% Bindungstoleranz:

Dies ist übrigens Weg aus meiner Fehlermarge, aber ich bin immer noch im Ergebnis interessiert, weil das Selectund Where noch (fast) ihren 20 - Punkte - Vorsprung halten. Es scheint, als würde es in einigen wenigen Punkten übertroffen, und genau das gibt ihm die Führung.

How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0


Nun, ich nehme an, dass der 20 - Punkte - Vorsprung aus der Mitte kam, wo sie beide gebunden sind , zu erhalten , um die gleiche Leistung. Ich könnte versuchen, es zu protokollieren, aber es wäre eine ganze Menge Informationen, die ich aufnehmen müsste. Ein Diagramm wäre besser, denke ich.

Das habe ich also getan.

Select vs Select und Where.

Es zeigt, dass die SelectLinie stabil bleibt (erwartet) und dass die Select + WhereLinie steigt (erwartet). Was mich jedoch verwundert, ist, warum es nicht mit dem Selectbei 50 oder früher übereinstimmt: Tatsächlich hatte ich früher als 50 erwartet, da ein zusätzlicher Enumerator für das Selectund erstellt werden musste Where. Ich meine, das zeigt den 20-Punkte-Vorsprung, aber es erklärt nicht warum. Dies ist wohl der Hauptpunkt meiner Frage.

Warum verhält es sich so? Soll ich ihm vertrauen? Wenn nicht, sollte ich den anderen oder diesen verwenden?


Wie @KingKong in den Kommentaren erwähnt, können Sie auch die SumÜberlastung verwenden, die ein Lambda benötigt. Meine beiden Optionen werden nun dahingehend geändert:

Zuerst:

int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);

Zweite:

int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

Ich werde es etwas kürzer machen, aber:

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

Der zwanzig Punkte Vorsprung ist immer noch da, was bedeutet , es nicht mit dem zu tun hat Whereund SelectKombination von @Marcin in den Kommentaren darauf hingewiesen.

Vielen Dank für das Lesen meiner Textwand! Wenn Sie interessiert sind, finden Sie hier die geänderte Version, die die von Excel aufgenommene CSV protokolliert.

Es ist nicht wahr.
quelle
1
Ich würde sagen, es hängt davon ab, wie teuer die Summe und der Zugang mc.Valuesind.
Medinoc
14
@ It'sNotALie. Where+ Selectverursacht keine zwei getrennten Iterationen über die Eingabesammlung. LINQ to Objects optimiert es in einer Iteration. Lesen Sie mehr auf meinem Blog-Beitrag
MarcinJuraszek
4
Interessant. Lassen Sie mich nur darauf hinweisen, dass eine for-Schleife über ein Array 10x schneller wäre als die beste LINQ-Lösung. Wenn Sie also auf Perf-Jagd gehen, verwenden Sie LINQ überhaupt nicht.
usr
2
Manchmal fragen Leute nach echten Recherchen, dies ist eine Beispielfrage: Ich bin kein C # -Benutzer, der von der Hot-Question-Liste stammt.
Grijesh Chauhan
2
@ WiSaGaN Das ist ein guter Punkt. Wenn dies jedoch auf eine Verzweigung gegenüber einer bedingten Bewegung zurückzuführen ist, erwarten wir mit 50% / 50% den dramatischsten Unterschied. Hier sehen wir die dramatischsten Unterschiede an den Enden, an denen die Verzweigung am vorhersehbarsten ist. Wenn das Wo ein Zweig ist und das Ternäre ein bedingter Zug, dann würden wir erwarten, dass die Wo-Zeiten wieder sinken, wenn alle Elemente gültig sind, aber es kommt nie wieder runter.
John Tseng

Antworten:

131

Selectiteriert einmal über den gesamten Satz und führt für jedes Element eine bedingte Verzweigung (Überprüfung auf Gültigkeit) und eine +Operation durch.

Where+SelectErstellt einen Iterator, der ungültige Elemente überspringt (nicht yield) und +nur die gültigen Elemente ausführt .

Die Kosten für a Selectsind also:

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

Und für Where+Select:

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

Wo:

  • p(valid) ist die Wahrscheinlichkeit, dass ein Element in der Liste gültig ist.
  • cost(check valid) sind die Kosten der Filiale, die die Gültigkeit überprüft
  • cost(yield)sind die Kosten für die Erstellung des neuen Status des whereIterators, der komplexer ist als der einfache Iterator, den die SelectVersion verwendet.

Wie Sie sehen können, nist die SelectVersion für eine bestimmte Konstante, während die Where+SelectVersion eine lineare Gleichung mit p(valid)als Variable ist. Die tatsächlichen Kostenwerte bestimmen den Schnittpunkt der beiden Linien, und da cost(yield)sie sich von diesen unterscheiden können cost(+), schneiden sie sich nicht unbedingt bei p(valid)= 0,5.

Alex
quelle
34
+1 für die einzige Antwort (bis jetzt), die die Frage tatsächlich beantwortet, die Antwort nicht errät und nicht nur "ich auch" generiert! Statistiken.
Binary Worrier
4
Technisch gesehen erstellen LINQ-Methoden Ausdrucksbäume, die anstelle von "Sets" einmal über die gesamte Sammlung ausgeführt werden.
Spoike
Was ist cost(append)? Wirklich gute Antwort, betrachtet es aus einem anderen Blickwinkel als nur Statistiken.
wahr.
5
Whereerstellt nichts, gibt nur jeweils ein Element aus der sourceSequenz zurück, wenn es nur das Prädikat füllt.
MarcinJuraszek
13
@Spoike - Ausdrucksbäume sind hier nicht relevant, da es sich um Linq-to-Objects handelt , nicht um Linq-to-Something-Other (z. B. Entity). Das ist der Unterschied zwischen IEnumerable.Select(IEnumerable, Func)und IQueryable.Select(IQueryable, Expression<Func>). Sie haben Recht, dass LINQ "nichts" tut, bis Sie die Sammlung durchlaufen, was Sie wahrscheinlich gemeint haben.
Kobi
33

Hier finden Sie eine ausführliche Erklärung der Ursachen für die Zeitunterschiede.


Die Sum()Funktion für IEnumerable<int>sieht folgendermaßen aus:

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

In C # foreachist nur syntaktischer Zucker für die .Net-Version eines Iterators (nicht zu verwechseln mit ) . Der obige Code wird also tatsächlich in diesen übersetzt: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;
}

Denken Sie daran, dass die beiden Codezeilen, die Sie vergleichen, die folgenden sind

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

Hier ist der Kicker:

LINQ verwendet die verzögerte Ausführung . Während es so aussieht , result1als würde die Sammlung zweimal durchlaufen , wird sie tatsächlich nur einmal durchlaufen. Die Where()Bedingung wird tatsächlich während Sum()des Aufrufs von angewendet MoveNext() (Dies ist dank der Magie von möglich yield return) .

Dies bedeutet, dass für result1den Code innerhalb der whileSchleife,

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

wird nur einmal für jeden Artikel mit ausgeführt mc.IsValid == true. Im Vergleich dazu result2wird dieser Code für jedes Element in der Sammlung ausgeführt. Deshalb ist result1in der Regel schneller.

(Beachten Sie jedoch, dass das Aufrufen der Where()Bedingung im Inneren MoveNext()immer noch einen geringen Overhead hat. Wenn also die meisten / alle Elemente vorhanden sind mc.IsValid == true, result2ist dies tatsächlich schneller!)


Hoffentlich ist jetzt klar, warum result2es normalerweise langsamer ist. Jetzt möchte ich erklären, warum ich in den Kommentaren angegeben habe, dass diese LINQ-Leistungsvergleiche keine Rolle spielen .

Das Erstellen eines LINQ-Ausdrucks ist billig. Das Aufrufen von Delegatenfunktionen ist günstig. Das Zuweisen und Schleifen eines Iterators ist billig. Aber es ist noch billiger, diese Dinge nicht zu tun. Wenn Sie also feststellen, dass eine LINQ-Anweisung der Engpass in Ihrem Programm ist, wird sie meiner Erfahrung nach ohne LINQ immer schneller als jede der verschiedenen LINQ-Methoden.

Ihr LINQ-Workflow sollte also folgendermaßen aussehen:

  1. Verwenden Sie LINQ überall.
  2. Profil.
  3. Wenn der Profiler angibt, dass LINQ die Ursache für einen Engpass ist, schreiben Sie diesen Code ohne LINQ neu.

Glücklicherweise sind LINQ-Engpässe selten. Engpässe sind selten. Ich habe in den letzten Jahren Hunderte von LINQ-Anweisungen geschrieben und am Ende <1% ersetzt. Und die meisten davon waren auf die schlechte SQL-Optimierung von LINQ2EF zurückzuführen, anstatt auf LINQs Schuld.

Also, wie immer, schreiben Sie klar und vernünftig Code zuerst, und warten , bis nach Ihnen Sorgen über Mikro-Optimierungen profiliert haben.

BlueRaja - Danny Pflughoeft
quelle
3
Kleiner Nachtrag: Die Top-Antwort wurde korrigiert.
wahr.
16

Lustige Sache. Wissen Sie, wie Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)definiert ist? Es verwendet SelectMethode!

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

Eigentlich sollte alles fast gleich funktionieren. Ich habe selbst schnell recherchiert und hier sind die Ergebnisse:

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

Für folgende Implementierungen:

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();

modbedeutet: Jede 1 von modElementen ist ungültig: Für mod == 1jedes Element ist ungültig, für mod == 2ungerade Elemente sind ungültig usw. Die Sammlung enthält 10000000Elemente.

Geben Sie hier die Bildbeschreibung ein

Und Ergebnisse für die Sammlung mit 100000000Gegenständen:

Geben Sie hier die Bildbeschreibung ein

Wie Sie sehen können, Selectund SumErgebnisse sind recht übergreifend konsistente modWerte. Jedoch whereund where+ selectnicht.

MarcinJuraszek
quelle
1
Es ist sehr interessant, dass in Ihren Ergebnissen alle Methoden an derselben Stelle beginnen und voneinander abweichen, während sich die Ergebnisse von It'sNotALie in der Mitte kreuzen.
John Tseng
6

Ich vermute, dass die Version mit Where die Nullen herausfiltert und sie kein Thema für Sum sind (dh Sie führen die Addition nicht aus). Dies ist natürlich eine Vermutung, da ich nicht erklären kann, wie das Ausführen eines zusätzlichen Lambda-Ausdrucks und das Aufrufen mehrerer Methoden eine einfache Addition einer 0 übertrifft.

Ein Freund von mir schlug vor, dass die Tatsache, dass die 0 in der Summe aufgrund von Überlaufprüfungen schwere Leistungseinbußen verursachen könnte. Es wäre interessant zu sehen, wie sich dies im ungeprüften Kontext verhält.

Stilgar
quelle
Einige Tests mit uncheckedmachen es ein kleines bisschen besser für die Select.
wahr.
Kann jemand sagen, ob das Deaktivieren die Methoden betrifft, die im Stack aufgerufen werden, oder nur die Operationen der obersten Ebene?
Stilgar
1
@Stilgar Dies gilt nur für die oberste Ebene.
Branko Dimitrijevic
Vielleicht müssen wir die ungeprüfte Summe implementieren und es auf diese Weise versuchen.
Stilgar
5

Wenn ich das folgende Beispiel durchführe, wird mir klar, dass das einzige Mal, wenn + Select Select übertreffen kann, tatsächlich darin besteht, eine gute Menge (ungefähr die Hälfte in meinen informellen Tests) der potenziellen Elemente in der Liste zu verwerfen. In dem kleinen Beispiel unten erhalte ich ungefähr die gleichen Zahlen aus beiden Proben, wenn das Where ca. 4mil Artikel aus 10mil überspringt. Ich lief in Release und ordnete die Ausführung von where + select vs select mit den gleichen Ergebnissen neu.

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
quelle
Könnte das nicht daran liegen, dass Sie die Zehner in der nicht wegwerfen Select?
wahr.
3
Das Ausführen im Debug ist nutzlos.
MarcinJuraszek
1
@MarcinJuraszek Offensichtlich. Eigentlich wollte ich sagen, dass ich in der Veröffentlichung lief :)
DavidN
@ It'sNotALie Das ist der Punkt. Es scheint mir, dass Where + Select nur dann besser abschneiden kann, wenn Where eine große Menge der summierten Elemente herausfiltert.
DavidN
2
Das ist im Grunde, was meine Frage sagt. Sie binden zu etwa 60%, wie dies bei dieser Probe der Fall ist. Die Frage ist warum, was hier nicht beantwortet wird.
wahr.
4

Wenn Sie Geschwindigkeit brauchen, ist es wahrscheinlich die beste Wahl, nur eine einfache Schleife zu machen. Und dabeifor in der Regel besser als foreach(vorausgesetzt, Ihre Sammlung ist natürlich wahlfrei zugänglich).

Hier sind die Timings, die ich erhalten habe, wenn 10% der Elemente ungültig sind:

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

Und mit 90% ungültigen Elementen:

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

Und hier ist mein Benchmark-Code ...

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);

    }

}

Übrigens stimme ich der Vermutung von Stilgar zu : Die relativen Geschwindigkeiten Ihrer beiden Fälle variieren je nach Prozentsatz der ungültigen Elemente, einfach weil die Anzahl der Sumzu erledigenden Aufgaben im Fall "Wo" unterschiedlich ist.

Branko Dimitrijevic
quelle
1

Anstatt zu versuchen, dies anhand einer Beschreibung zu erklären, werde ich einen mathematischeren Ansatz wählen.

Angesichts des folgenden Codes, der ungefähr annähern sollte, was LINQ intern tut, sind die relativen Kosten wie folgt:
Nur auswählen:Nd + Na
Wo + Auswählen:Nd + Md + Ma

Um herauszufinden, wo sie sich kreuzen werden, müssen wir eine kleine Algebra machen:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)

Dies bedeutet, dass die Kosten eines Delegatenaufrufs ungefähr den Kosten einer Addition entsprechen müssen, damit der Wendepunkt bei 50% liegt. Da wir wissen, dass der tatsächliche Wendepunkt bei etwa 60% lag, können wir rückwärts arbeiten und feststellen, dass die Kosten für einen Delegatenaufruf für @ It'sNotALie tatsächlich etwa 2/3 der Kosten für eine Addition betrugen, was überraschend ist, aber das ist es seine Zahlen sagen.

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
quelle
0

Ich finde es interessant, dass sich MarcinJuraszeks Ergebnis von dem von It'sNotALie unterscheidet. Insbesondere beginnen die Ergebnisse von MarcinJuraszek mit allen vier Implementierungen am selben Ort, während sich die Ergebnisse von It'sNotALie in der Mitte kreuzen. Ich werde anhand der Quelle erklären, wie dies funktioniert.

Nehmen wir an, dass es Gesamtelemente gibt n, undm gültige Elemente gibt.

Die SumFunktion ist ziemlich einfach. Es durchläuft nur den Enumerator: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)

Nehmen wir der Einfachheit halber an, dass die Sammlung eine Liste ist. Sowohl Select als auch WhereSelect erstellen eine WhereSelectListIterator. Dies bedeutet, dass die tatsächlich generierten Iteratoren gleich sind. In beiden Fällen gibt es eine SumSchleife über einen Iterator, die WhereSelectListIterator. Der interessanteste Teil des Iterators ist der MoveNext Methode.

Da die Iteratoren gleich sind, sind die Schleifen gleich. Der einzige Unterschied besteht im Körper der Schleifen.

Der Körper dieser Lambdas hat sehr ähnliche Kosten. Die where-Klausel gibt einen Feldwert zurück, und das ternäre Prädikat gibt auch einen Feldwert zurück. Die select-Klausel gibt einen Feldwert zurück, und die beiden Zweige des ternären Operators geben entweder einen Feldwert oder eine Konstante zurück. Die kombinierte select-Klausel hat den Zweig als ternären Operator, aber WhereSelect verwendet den Zweig in MoveNext.

Alle diese Operationen sind jedoch ziemlich billig. Die bisher teuerste Operation ist die Branche, in der uns eine falsche Vorhersage kosten wird.

Eine weitere teure Operation ist hier die Invoke. Das Aufrufen einer Funktion dauert viel länger als das Hinzufügen eines Werts, wie Branko Dimitrijevic gezeigt hat.

Wiegen ist auch die überprüfte Ansammlung Sum . Wenn der Prozessor kein arithmetisches Überlaufflag hat, kann dies ebenfalls kostspielig sein.

Daher sind die interessanten Kosten: ist:

  1. ( n+ m) * + m* Aufrufenchecked+=
  2. n* Rufe + n* aufchecked+=

Wenn also die Kosten für Invoke viel höher sind als die Kosten für die überprüfte Akkumulation, ist der Fall 2 immer besser. Wenn sie ungefähr gerade sind, sehen wir ein Gleichgewicht, wenn ungefähr die Hälfte der Elemente gültig ist.

Es sieht so aus, als ob auf dem System von MarcinJuraszek geprüft + = vernachlässigbare Kosten verursacht, aber auf den Systemen von It'sNotALie und Branko Dimitrijevic hat geprüftes + = erhebliche Kosten. Es sieht so aus, als wäre es das teuerste auf dem It'sNotALie-System, da der Break-Even-Punkt viel höher ist. Es sieht nicht so aus, als hätte jemand Ergebnisse von einem System veröffentlicht, bei dem die Akkumulation viel mehr kostet als der Aufruf.

John Tseng
quelle
@ It'sNotALie. Ich glaube nicht, dass jemand ein falsches Ergebnis hat. Ich konnte einige Dinge einfach nicht erklären. Ich hatte angenommen, dass die Kosten für Invoke viel höher sind als die für + =, aber es ist denkbar, dass sie je nach Hardwareoptimierung viel näher liegen.
John Tseng