Benötigt LINQ wesentlich mehr Verarbeitungszyklen und Speicher als Dateniterationstechniken auf niedrigerer Ebene?

8

Hintergrund

Ich bin in letzter Zeit dabei, anstrengende Tech-Interviews für Positionen zu ertragen, die den .NET-Stack verwenden, von denen einige dumme Fragen wie diese enthalten , und einige Fragen, die gültiger sind. Ich bin kürzlich auf ein Problem gestoßen, das möglicherweise gültig ist, aber ich möchte mich hier bei der Community erkundigen, um sicherzugehen.

Auf die Frage eines Interviewers, wie ich die Häufigkeit von Wörtern in einem Textdokument zählen und die Ergebnisse bewerten würde, antwortete ich, dass ich dies tun würde

  1. Verwenden Sie ein Stream-Objekt, um die Textdatei als Zeichenfolge zu speichern.
  2. Teilen Sie die Zeichenfolge in ein Array auf Leerzeichen auf, während Sie die Interpunktion ignorieren.
  3. Verwenden Sie LINQ für das Array .GroupBy()und .Count()dann die OrderBy()Anzahl.

Ich habe diese Antwort aus zwei Gründen falsch verstanden:

  1. Das Streamen einer gesamten Textdatei in den Speicher kann katastrophal sein. Was wäre, wenn es eine ganze Enzyklopädie wäre? Stattdessen sollte ich jeweils einen Block streamen und mit dem Erstellen einer Hash-Tabelle beginnen.
  2. LINQ ist zu teuer und erfordert zu viele Verarbeitungszyklen. Ich hätte stattdessen eine Hash-Tabelle erstellen und für jede Iteration nur ein Wort zur Hash-Tabelle hinzufügen sollen, wenn es sonst nicht vorhanden wäre, und dann die Anzahl erhöhen.

Der erste Grund scheint vernünftig. Aber der zweite gibt mir mehr Pause. Ich dachte, dass eines der Verkaufsargumente von LINQ darin besteht, dass es einfach Operationen auf niedrigerer Ebene wie Hash-Tabellen abstrahiert, aber dass es unter dem Schleier immer noch dieselbe Implementierung ist.

Frage

Erfordert LINQ, abgesehen von einigen zusätzlichen Verarbeitungszyklen zum Aufrufen abstrahierter Methoden, erheblich mehr Verarbeitungszyklen, um eine bestimmte Dateniterationsaufgabe auszuführen, als dies bei einer untergeordneten Aufgabe (z. B. beim Erstellen einer Hash-Tabelle) der Fall wäre?

Matt Cashatt
quelle
2
Fragen Sie ihn, was für ein Trottel eine ganze Enzyklopädie in eine einzige Textdatei geschrieben hat?
JeffO
4
Es ist eines dieser Dinge, die gemessen werden sollten. Erstellen Sie 2 oder 3 Implementierungen und zeichnen Sie die Leistung auf. Verallgemeinerungen über LINQ oder Technik X sind nicht sinnvoll. Ich würde sagen, es ist schlecht für den Interviewer, die Verwendung von LINQ als "falsche" Antwort zu deklarieren. Obwohl bei der serverseitigen Verarbeitung mit hoher Last jede Millisekunde zählt.
Lord Tydus
1
Eine schnelle Google-Suche nach "Leistungstests für Objekte und Schleifen" ergab einige Treffer. Einige enthalten Quellcode, den Sie zum Testen selbst verwenden können. Sehen Sie dies , dies und das .
Oded
1
Denken Sie bei Interviews daran, dass es einige C ++ - Programmierer der "alten Schule" gibt, die der Meinung sind, dass Sie das Rad neu erfinden sollten, anstatt die .NET-Bibliotheken zu verwenden. Sie werden auch auf VBs der alten Schule stoßen, die den gesamten Datenzugriffscode von Hand ausführen möchten, anstatt LINQ und EF zu verwenden.
jfrankcarr
1
Oded, diese Beispiele in den von Ihnen angegebenen Links sind sehr falsch. Ich kann nicht auf alle Details in einem Kommentar eingehen, aber nehme den zweiten Link. Es vergleicht "foreach x if x = toFind stop" mit einer Linq-Abfrage, die das Äquivalent von "select * from list where x like toFind" ausführt. Der Unterschied besteht darin, dass der erste Stopp erfolgt, wenn die erste Instanz gefunden wird. Die Linq-Abfrage wird immer wiederholt Bei jedem Eintrag wird eine Sammlung ALLER Elemente zurückgegeben, die dem Suchmuster entsprechen. Sehr verschieden. Das liegt nicht daran, dass LINQ kaputt ist, sondern daran, dass er die falsche Abfrage verwendet hat.
Ian

Antworten:

9

Ich würde sagen, die Hauptschwäche dieser Antwort ist weniger die Verwendung von Linq als vielmehr die Auswahl der spezifischen Operatoren. GroupByNimmt jedes Element und projiziert es auf einen Schlüssel und einen Wert, die in eine Suche einfließen. Mit anderen Worten, jedes Wort fügt der Suche etwas hinzu.

Die naive Implementierung .GroupBy(e => e)speichert eine Kopie jedes Wortes im Quellmaterial, wodurch die endgültige Suche fast so groß ist wie das ursprüngliche Quellmaterial. Selbst wenn wir den Wert mit projizieren, .GroupBy(e => e, e => null)erstellen wir eine große Suche nach kleinen Werten.

Was wir wollen, ist ein Operator, der nur die benötigten Informationen beibehält, dh eine Kopie jedes Wortes und die Anzahl dieses Wortes. Dafür können wir verwenden Aggregate:

words.Aggregate(new Dictionary<string, int>(), (counts, word) => 
{
    int currentCount;
    counts.TryGetValue(word, currentCount);
    counts[word] = currentCount + 1;
    return counts;
} 

Von hier aus gibt es verschiedene Möglichkeiten, dies zu beschleunigen:

  1. Anstatt beim Teilen viele Zeichenfolgen zu erstellen, könnten wir Strukturen weitergeben, die auf die ursprüngliche Zeichenfolge und das Segment verweisen, das das Wort enthält, und das Segment nur dann kopieren, wenn sich herausstellt, dass es sich um einen eindeutigen Schlüssel handelt
  2. Verwenden Sie Parallel Linq, um mehrere Kerne zu aggregieren, und kombinieren Sie dann die Ergebnisse . Dies ist trivial im Vergleich zu der dafür erforderlichen Beinarbeit von Hand.
Chris Pitman
quelle
Alles gute Chris, danke. Ich werde es für eine Weile unterlassen zu akzeptieren, da die Frage allgemeiner ist und im Wesentlichen von Oded in den obigen Kommentaren beantwortet wird. Ich möchte ihm nur die Gelegenheit geben, zuerst die Antwort zu geben. Nochmals vielen Dank für Ihren Einblick, der großartig ist.
Matt Cashatt
6

Ich denke, Sie hatten eine knappe Flucht, der Interviewer wusste nicht wirklich, wovon er sprach. Schlimmer noch, er glaubt, dass es eine "richtige" Antwort gibt. Wenn er jemand wäre, für den Sie arbeiten möchten, würde ich erwarten, dass er Ihre erste Antwort entgegennimmt, herausfindet, warum Sie sie gewählt haben, und Sie dann herausfordert, sie zu verbessern, wenn er Probleme damit findet.

LINQ macht den Menschen Angst, weil es wie Magie aussieht. Es ist eigentlich sehr einfach (so einfach, dass man ein Genie sein müsste, um es zu erfinden)

var result = from item in collection where item=>item.Property > 3 select item;

Ist zusammengestellt in:

IEnumerable<itemType> result = collection.Where(item=>item.property >3);

(Bitte schreien Sie nicht, wenn ich die falsche Syntax habe, es ist nach Mitternacht und ich bin im Bett :))

Wo ist eine Erweiterungsmethode auf IEnumerable, die ein Lambda nimmt. Das Lambda wird einfach (in diesem Fall) zu einem Delegierten zusammengestellt:

bool AMethod(ItemType item)
{
    return item.property >3;
}

Die Where-Methode fügt einfach ALLE Instanzen von Elementen hinzu, bei denen AMethod einer zurückgegebenen Sammlung true zurückgibt.

Es gibt keinen Grund, der langsamer wäre, als einen foreach durchzuführen und alle übereinstimmenden Elemente zu einer Sammlung in dieser Schleife hinzuzufügen. Tatsächlich macht die Where-Erweiterungsmethode wahrscheinlich genau das. Die wahre Magie kommt, wenn Sie eine Alternative injizieren müssen, wo Kriterien.

Wie ich oben in meinem Kommentar erwähnt habe, sind einige der verknüpften Beispiele sehr falsch. Und es ist diese Art von Fehlinformation, die die Probleme verursacht.

Wenn das Interview Ihnen eine Chance gegeben hätte, hätten Sie Folgendes sagen können:

  • LINQ ist leicht zu lesen, insbesondere wenn Sie interessante Projektionen und Gruppierungen einführen. Einfach zu lesender Code ist einfacher zu pflegen, was ein Gewinn ist.

  • Es wäre wirklich einfach, die Leistung zu messen, wenn es sich tatsächlich um einen Engpass handelt, und ihn durch etwas anderes zu ersetzen.

Ian
quelle
Insgesamt stimme ich Ihnen zu, aber das Verhalten der Where-Methode - Where-Methode fügt einer Sammlung nicht alle übereinstimmenden Elemente hinzu. Es speichert die Informationen, die zum Filtern von Elementen im Ausdrucksbaum erforderlich sind. Wenn der zurückgegebene Iterator nicht tatsächlich verwendet wird, erfolgt überhaupt keine Filterung.
Codism
Hervorragender Punkt, das hätte ich erwähnen sollen. In ihrem Beispiel verwenden sie den zurückgegebenen Iterator. Dies war der Wahnsinn ihrer Prüfung. Um den einen gefundenen Wert zu extrahieren (alle Elemente in ihren Testdaten waren eindeutig), hatten sie einen foreach, der die resultierende Aufzählung iterierte, um das Ergebnis anzuzeigen. Natürlich gab es nur ein Ergebnis, so dass nur die eine Antwort gedruckt wurde. Wahnsinn :)
Ian
Obwohl ich LINQ nicht verwendet habe, finde ich es unangenehm, Dinge wie Countfür ein paar enge Szenarien zu optimieren , die mit der Kapselung schlecht funktionieren. Verketten Sie eine Liste mit Millionen Elementen und einen Iterator mit vier Elementen und Countsollten ungefähr 5 Operationen erfordern, aber stattdessen eine Million. Ich wünschte, MS würde IEnhancedEnumeratormit einer int Move(int)Methode definieren , die im Erfolgsfall 0 oder im Fehlerfall den Fehlbetrag zurückgibt ( Move(1000003)bei einer neu erstellten List<T>.EnumeratorListe aus Millionen Elementen würde dies 3 ergeben). Jede Implementierung ...
Supercat
... von IEnumerable<T>könnte in eine Implementierung von eingewickelt werden IEnhancedEnumerator, aber Typen, die IEnhancedEnumeratordirekt implementieren, könnten eine Beschleunigung um Größenordnungen bei vielen Operationen ermöglichen, und selbst Dinge wie die Rückkehr von Appendkönnten die Fähigkeit der Bestandteile zur schnellen Suche aufdecken .
Supercat