Warum ist die Verarbeitung einer sortierten array langsamer als in einem unsortierten array?
Habe ich eine Liste von 500000 zufällig generierte Tuple<long,long,string>
Objekte, auf die trete ich ein einfaches "zwischen" - Suche:
var data = new List<Tuple<long,long,string>>(500000);
...
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);
Wenn ich die Erstellung meiner random array und führe meine Suche nach 100 zufällig generierte Werte x
die Suche wird abgeschlossen in ungefähr vier Sekunden. Die Kenntnis der große Wunder, dass der die Sortierung der Suche, jedoch, ich beschloss, meine Daten Sortieren - zuerst durch Item1
, dann durch Item2
, und schließlich durch Item3
- vor dem ausführen von meinen 100 suchen. Ich erwartete die sortierte version durchführen ein wenig schneller, da der branch prediction: mein denken war, dass, sobald wir zu dem Punkt, wo Item1 == x
alle weiteren Prüfungen t.Item1 <= x
würde sagen der Zweig korrekt als "nicht nehmen", die Beschleunigung der Rute Teil der Suche. Viel zu meiner überraschung, die Suche dauerte doppelt so lange auf ein sortiertes array!
Ich habe versucht, schalten um die Reihenfolge, in der ich lief, meine Experimente und die verschiedenen verwendeten seed für den Zufallszahlengenerator, aber der Effekt ist der gleiche: Suche in einem unsortierten array lief fast doppelt so schnell wie die Suche in den gleichen array, aber sortiert!
Hat jemand eine gute Erklärung für diesen seltsamen Effekt? Der source-code meines tests folgt, ich bin mit .NET 4.0.
private const int TotalCount = 500000;
private const int TotalQueries = 100;
private static long NextLong(Random r) {
var data = new byte[8];
r.NextBytes(data);
return BitConverter.ToInt64(data, 0);
}
private class TupleComparer : IComparer<Tuple<long,long,string>> {
public int Compare(Tuple<long,long,string> x, Tuple<long,long,string> y) {
var res = x.Item1.CompareTo(y.Item1);
if (res != 0) return res;
res = x.Item2.CompareTo(y.Item2);
return (res != 0) ? res : String.CompareOrdinal(x.Item3, y.Item3);
}
}
static void Test(bool doSort) {
var data = new List<Tuple<long,long,string>>(TotalCount);
var random = new Random(1000000007);
var sw = new Stopwatch();
sw.Start();
for (var i = 0 ; i != TotalCount ; i++) {
var a = NextLong(random);
var b = NextLong(random);
if (a > b) {
var tmp = a;
a = b;
b = tmp;
}
var s = string.Format("{0}-{1}", a, b);
data.Add(Tuple.Create(a, b, s));
}
sw.Stop();
if (doSort) {
data.Sort(new TupleComparer());
}
Console.WriteLine("Populated in {0}", sw.Elapsed);
sw.Reset();
var total = 0L;
sw.Start();
for (var i = 0 ; i != TotalQueries ; i++) {
var x = NextLong(random);
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);
total += cnt;
}
sw.Stop();
Console.WriteLine("Found {0} matches in {1} ({2})", total, sw.Elapsed, doSort ? "Sorted" : "Unsorted");
}
static void Main() {
Test(false);
Test(true);
Test(false);
Test(true);
}
Populated in 00:00:01.3176257
Found 15614281 matches in 00:00:04.2463478 (Unsorted)
Populated in 00:00:01.3345087
Found 15614281 matches in 00:00:08.5393730 (Sorted)
Populated in 00:00:01.3665681
Found 15614281 matches in 00:00:04.1796578 (Unsorted)
Populated in 00:00:01.3326378
Found 15614281 matches in 00:00:08.6027886 (Sorted)
- Da der branch prediction :p
- Ich erwartete die sortierte version durchführen ein wenig schneller, da der branch prediction. Meine überlegung war, dass, sobald wir zu dem Punkt, wo
Item1 == x
alle weiteren Prüfungent.Item1 <= x
würde sagen der Zweig korrekt als "nicht nehmen", die Beschleunigung der Rute Teil der Suche. Offensichtlich, dass die Linie des Denkens, hat sich als falsch erwiesen durch die harte Realität 🙂 - Interessanterweise, für
TotalCount
um10,000
oder weniger, die sortiert version nicht schneller (natürlich trivial schneller an diese kleinen zahlen) (zur info, der code könnte wollen, um die ursprüngliche Größevar data List = new List<Tuple<long, long, string>>(500000)
gebunden gegenTotalCount
statt hart-Kodierung der Kapazität) - gute Beobachtung! Ich habe eine Erklärung in meiner Antwort.
- Ich möchte noch hinzufügen, dass die Abschwächung ist insbesondere verbunden mit dem filtern der Liste. Durchführung
data.Where()
zeigt die gleiche Verlangsamung, als irgendetwas anderes, dass er iteriert über die sortierten Liste. Betriebssystem auf die sortierte und unsortierte Listen ohne filter dauert der gleichen Zeit. - Zwar ist es ein wenig aus dem Rahmen der Frage nach dem "warum", es kann sein, erwähnenswert, dass der größte Vorteil von pre-Sortieren der Liste sein sollte, die Sie verwenden können binarysearch-Methode() auf und erreichen O(log n) Leistung auf Ihre Suche.
- Diese Frage ist NICHT eine doppelte eine bestehende Frage hier. nicht zu Stimmen, um es zu schließen, als einen.
- ein Widerspruch stackoverflow.com/q/11227809/992665
- Überhaupt nicht! Die beiden Fragen betrachten wir zwei sehr unterschiedliche Szenarien, ganz natürlich angekommen zu unterschiedlichen Ergebnissen.
- Nicht in Bezug auf Ihre Frage, aber Sie erstellen eine Klasse
TupleComparer
aber das ist völlig unnötig, daComparer<Tuple<long, long, string>>.Default
bereits dieses Verhalten (von derIComparable
Umsetzung vonTuple<,,>
). So können Sie einfachdata.Sort()
ohne Argumente. - stackoverflow.com/questions/11227809/... ich bin überrascht, dass es ein sortiertes array ist schneller
Du musst angemeldet sein, um einen Kommentar abzugeben.
Wenn Sie mithilfe der unsortierten Liste alle Tupel zugegriffen werden, in Speicher-Reihenfolge. Sie zugeordnet wurden nacheinander im RAM. CPUs Liebe den Zugriff auf den Speicher sequentiell, weil Sie spekulativ Anfrage der nächsten cache-Zeile, so wird es immer präsent sein, wenn nötig.
Wenn Sie Sortieren die Liste, die Sie steckte es in zufälliger Reihenfolge, weil Sie Ihre Schlüssel Sortieren nach dem Zufallsprinzip generiert werden. Dies bedeutet, dass die Speicherzugriffe auf Tupel-Mitglieder sind unberechenbar. Die CPU kann prefetch-Speicher und fast jeder Zugriff auf eine Tupel ist ein cache-miss.
Dies ist ein schönes Beispiel für einen spezifischen Vorteil in der GC memory management: Daten, Strukturen, die zugeteilt wurden, zusammen und werden zusammen verwendet, auch sehr schön. Sie haben große Lokalität der Referenz.
Die Strafe aus dem cache findet überwiegt die gespeicherten branch prediction Strafe in diesem Fall.
Versuchen Sie die Umstellung auf eine
struct
-Tupel. Dies wird zur Wiederherstellung der Leistung, weil keine Zeiger zu dereferenzieren, muss auftreten, um Laufzeit für den Zugriff auf Tupel-Mitglieder.Chris Sinclair Hinweise in den Kommentaren, dass "für TotalCount rund 10.000 oder weniger, die sortierte version hat eine schnellere". Dies ist, weil eine kleine Liste passt komplett in den CPU-cache. Den Speicher zugreift, kann unberechenbar sein, aber das Ziel ist immer im cache. Ich glaube, es ist noch eine kleine Strafe, weil sogar ein laden aus dem cache dauert einige Zyklen. Aber das scheint kein problem zu sein, da die CPU jonglieren kann mehrere herausragende Lasten, damit den Durchsatz zu erhöhen. Wenn die CPU trifft eine Wartezeit für Speicher es wird noch Fahrt Voraus in die instruction-stream-Warteschlange, wie viele Speicher-Operationen, wie Sie können. Diese Technik wird verwendet, um zu verstecken Latenz.
Diese Art von Verhalten zeigt, wie schwer es ist, Vorhersagen, die Leistung auf modernen CPUs. Die Tatsache, dass wir nur 2x langsamer beim übergang von sequentiellen zu random-memory-access-sagen Sie mir, wie viel Los ist auf unter der Decke zu verstecken, memory-Latenz. Ein Speicherzugriff kann stall die CPU für 50-200 Zyklen. Angesichts dieser Zahl könnte man erwarten, dass das Programm ein >10x langsamer bei der Einführung von random Speicherzugriffe.
new List<Tuple<long,long,string>>(500000)
one-by-one vor der Prüfung, die neue Liste. In diesem Szenario, die sortiert test ist genauso schnell wie die Liste unsortiert, was mit mit der Begründung, auf diese Antwort.Tuple
struct, und das Programm begann Verhalten, die Art, wie ich vorhergesagt: die sortierte version war ein wenig schneller. Darüber hinaus werden die unsortierte version wurde doppelt so schnell! Also die zahlen mitstruct
sind 2s unsortiert vs. 1,9 s sortiert.std::vector
fast immer besser abschneidet als derstd::list
.std::vector
vsstd::list
ist ein gutes Beispiel.LINQ weiß nicht, ob Sie die Liste sortiert ist oder nicht.
Da die Zählung mit Prädikat-parameter-Erweiterung-Methode für alle IEnumerables, ich denke, es weiß noch nicht einmal, wenn es läuft über die Sammlung mit effizienten random access. So, es wird einfach überprüft, jedes element und jede Usr erklärt, warum die Leistung niedriger.
Auszunutzen performance-Vorteile von sortierten Arrays (z.B. binäre Suche), Sie haben zu tun, ein bisschen mehr coding.
Count
oderWhere
würde "irgendwie" pick up auf die Idee, dass meine Daten sortiert, und führen Sie eine binäre Suche, statt einfach "alles prüfen" suchen. Alle, die ich hatte gehofft, für einige war die Verbesserung aufgrund besserer branch prediction (siehe den link in meiner Frage), aber wie sich herausstellt, ist die Lokalität der Referenz Trumpf branch prediction big time.