Ich bin gerade auf etwas gestoßen. Zuerst dachte ich, es könnte sich um eine Fehlvorhersage für Zweige handeln, wie dies in diesem Fall der Fall ist , aber ich kann nicht erklären, warum eine Fehlvorhersage für Zweige dieses Verhalten verursachen sollte.
Ich habe zwei Versionen von Bubble Sort in Java implementiert und einige Leistungstests durchgeführt:
import java.util.Random;
public class BubbleSortAnnomaly {
public static void main(String... args) {
final int ARRAY_SIZE = Integer.parseInt(args[0]);
final int LIMIT = Integer.parseInt(args[1]);
final int RUNS = Integer.parseInt(args[2]);
int[] a = new int[ARRAY_SIZE];
int[] b = new int[ARRAY_SIZE];
Random r = new Random();
for (int run = 0; RUNS > run; ++run) {
for (int i = 0; i < ARRAY_SIZE; i++) {
a[i] = r.nextInt(LIMIT);
b[i] = a[i];
}
System.out.print("Sorting with sortA: ");
long start = System.nanoTime();
int swaps = bubbleSortA(a);
System.out.println( (System.nanoTime() - start) + " ns. "
+ "It used " + swaps + " swaps.");
System.out.print("Sorting with sortB: ");
start = System.nanoTime();
swaps = bubbleSortB(b);
System.out.println( (System.nanoTime() - start) + " ns. "
+ "It used " + swaps + " swaps.");
}
}
public static int bubbleSortA(int[] a) {
int counter = 0;
for (int i = a.length - 1; i >= 0; --i) {
for (int j = 0; j < i; ++j) {
if (a[j] > a[j + 1]) {
swap(a, j, j + 1);
++counter;
}
}
}
return (counter);
}
public static int bubbleSortB(int[] a) {
int counter = 0;
for (int i = a.length - 1; i >= 0; --i) {
for (int j = 0; j < i; ++j) {
if (a[j] >= a[j + 1]) {
swap(a, j, j + 1);
++counter;
}
}
}
return (counter);
}
private static void swap(int[] a, int j, int i) {
int h = a[i];
a[i] = a[j];
a[j] = h;
}
}
Wie wir sehen können, ist der einzige Unterschied zwischen diesen beiden Sortierverfahren die >
vs. >=
. Wenn man das Programm mit ausführt java BubbleSortAnnomaly 50000 10 10
, würde man offensichtlich erwarten, dass dies sortB
langsamer ist als sortA
weil es mehr swap(...)
s ausführen muss . Aber ich habe die folgende (oder ähnliche) Ausgabe auf drei verschiedenen Maschinen erhalten:
Sorting with sortA: 4.214 seconds. It used 564960211 swaps.
Sorting with sortB: 2.278 seconds. It used 1249750569 swaps.
Sorting with sortA: 4.199 seconds. It used 563355818 swaps.
Sorting with sortB: 2.254 seconds. It used 1249750348 swaps.
Sorting with sortA: 4.189 seconds. It used 560825110 swaps.
Sorting with sortB: 2.264 seconds. It used 1249749572 swaps.
Sorting with sortA: 4.17 seconds. It used 561924561 swaps.
Sorting with sortB: 2.256 seconds. It used 1249749766 swaps.
Sorting with sortA: 4.198 seconds. It used 562613693 swaps.
Sorting with sortB: 2.266 seconds. It used 1249749880 swaps.
Sorting with sortA: 4.19 seconds. It used 561658723 swaps.
Sorting with sortB: 2.281 seconds. It used 1249751070 swaps.
Sorting with sortA: 4.193 seconds. It used 564986461 swaps.
Sorting with sortB: 2.266 seconds. It used 1249749681 swaps.
Sorting with sortA: 4.203 seconds. It used 562526980 swaps.
Sorting with sortB: 2.27 seconds. It used 1249749609 swaps.
Sorting with sortA: 4.176 seconds. It used 561070571 swaps.
Sorting with sortB: 2.241 seconds. It used 1249749831 swaps.
Sorting with sortA: 4.191 seconds. It used 559883210 swaps.
Sorting with sortB: 2.257 seconds. It used 1249749371 swaps.
Wenn ich den Parameter LIMIT
auf zB 50000
( java BubbleSortAnnomaly 50000 50000 10
) setze , erhalte ich die erwarteten Ergebnisse:
Sorting with sortA: 3.983 seconds. It used 625941897 swaps.
Sorting with sortB: 4.658 seconds. It used 789391382 swaps.
Ich habe das Programm nach C ++ portiert, um festzustellen, ob dieses Problem Java-spezifisch ist. Hier ist der C ++ - Code.
#include <cstdlib>
#include <iostream>
#include <omp.h>
#ifndef ARRAY_SIZE
#define ARRAY_SIZE 50000
#endif
#ifndef LIMIT
#define LIMIT 10
#endif
#ifndef RUNS
#define RUNS 10
#endif
void swap(int * a, int i, int j)
{
int h = a[i];
a[i] = a[j];
a[j] = h;
}
int bubbleSortA(int * a)
{
const int LAST = ARRAY_SIZE - 1;
int counter = 0;
for (int i = LAST; 0 < i; --i)
{
for (int j = 0; j < i; ++j)
{
int next = j + 1;
if (a[j] > a[next])
{
swap(a, j, next);
++counter;
}
}
}
return (counter);
}
int bubbleSortB(int * a)
{
const int LAST = ARRAY_SIZE - 1;
int counter = 0;
for (int i = LAST; 0 < i; --i)
{
for (int j = 0; j < i; ++j)
{
int next = j + 1;
if (a[j] >= a[next])
{
swap(a, j, next);
++counter;
}
}
}
return (counter);
}
int main()
{
int * a = (int *) malloc(ARRAY_SIZE * sizeof(int));
int * b = (int *) malloc(ARRAY_SIZE * sizeof(int));
for (int run = 0; RUNS > run; ++run)
{
for (int idx = 0; ARRAY_SIZE > idx; ++idx)
{
a[idx] = std::rand() % LIMIT;
b[idx] = a[idx];
}
std::cout << "Sorting with sortA: ";
double start = omp_get_wtime();
int swaps = bubbleSortA(a);
std::cout << (omp_get_wtime() - start) << " seconds. It used " << swaps
<< " swaps." << std::endl;
std::cout << "Sorting with sortB: ";
start = omp_get_wtime();
swaps = bubbleSortB(b);
std::cout << (omp_get_wtime() - start) << " seconds. It used " << swaps
<< " swaps." << std::endl;
}
free(a);
free(b);
return (0);
}
Dieses Programm zeigt das gleiche Verhalten. Kann jemand erklären, was genau hier los ist?
sortB
Erstes Ausführen und dann sortA
nichts ändert die Ergebnisse.
quelle
>
vs>=
hat nur geringe Auswirkungen. Um wirklich aussagekräftige Zahlen für Zeiten zu erhalten, müssen Sie viele verschiedene Sequenzen und Durchschnittswerte messen-DRUNS=XXX
(C ++, Compiler-Direktive) ausführen . Und die Ergebnisse sind reproduzierbar.a
in beiden Läufen sortiert (und berührt nichtb
).Antworten:
Ich denke, es kann tatsächlich an der Verzweigungsvorhersage liegen. Wenn Sie die Anzahl der Swaps im Vergleich zur Anzahl der inneren Sortieriterationen zählen, finden Sie:
Limit = 10
Limit = 50000
In dem
Limit == 10
Fall wird der Swap also 99,98% der Zeit in der B-Sortierung durchgeführt, was offensichtlich für den Verzweigungsprädiktor günstig ist. In demLimit == 50000
Fall, dass der Swap nur zufällig zu 68% getroffen wird, ist der Verzweigungsprädiktor weniger vorteilhaft.quelle
LIMIT >= ARRAY_SIZE
Sie einen Testfall durchführen können, in dem das Array aus eindeutigen Zahlen besteht. Zum Beispiel, wenna[i] = ARRAY_SIZE - i
Sie einen Swap für jede Schleife und identische Zeiten für die A / B-Sortierungen erhalten.Ich denke, dies kann in der Tat durch eine falsche Vorhersage der Branche erklärt werden.
Betrachten Sie zum Beispiel LIMIT = 11 und
sortB
. Bei der ersten Iteration der äußeren Schleife stößt sie sehr schnell auf eines der Elemente gleich 10. Es wirda[j]=10
also definitiva[j]
so sein>=a[next]
, da es keine Elemente gibt, die größer als 10 sind. Daher wird ein Tausch durchgeführt , dann machen Sie nur einen Schrittj
, um dasa[j]=10
wieder zu finden (der gleiche getauschte Wert). So wird es wieder seina[j]>=a[next]
, und so eins. Jeder Vergleich mit Ausnahme einiger am Anfang wird wahr sein. Ebenso wird es bei den nächsten Iterationen der äußeren Schleife ausgeführt.Nicht dasselbe für
sortA
. Es wird ungefähr auf die gleiche Weise beginnen, stolperna[j]=10
, einige Swaps auf ähnliche Weise durchführen, aber nur bis zu einem Punkt, an dem es auch findeta[next]=10
. Dann ist die Bedingung falsch und es wird kein Tausch durchgeführt.a[next]=10
Und so weiter: Jedes Mal, wenn es stolpert , ist die Bedingung falsch und es werden keine Swaps durchgeführt. Daher ist diese Bedingung 10 Mal von 11 wahr (Wertea[next]
von 0 bis 9) und in 1 Fall von 11 falsch. Nichts Seltsames, dass die Verzweigungsvorhersage fehlschlägt.quelle
Unter Verwendung des mit dem
perf stat
Befehl bereitgestellten C ++ - Codes (Zeitzählung entfernt) erhielt ich Ergebnisse, die die Brach-Miss-Theorie bestätigen.Mit
Limit = 10
profitiert BubbleSortB stark von der Verzweigungsvorhersage (0,01% Fehler), aber mit derLimit = 50000
Verzweigungsvorhersage schlägt die Vorhersage noch mehr fehl (mit 15,65% Fehlern) als bei BubbleSortA (12,69% bzw. 12,76% Fehler).BubbleSortA Limit = 10:
Performance counter stats for './bubbleA.out': 46670.947364 task-clock # 0.998 CPUs utilized 73 context-switches # 0.000 M/sec 28 CPU-migrations # 0.000 M/sec 379 page-faults # 0.000 M/sec 117,298,787,242 cycles # 2.513 GHz 117,471,719,598 instructions # 1.00 insns per cycle 25,104,504,912 branches # 537.904 M/sec 3,185,376,029 branch-misses # 12.69% of all branches 46.779031563 seconds time elapsed
BubbleSortA Limit = 50000:
Performance counter stats for './bubbleA.out': 46023.785539 task-clock # 0.998 CPUs utilized 59 context-switches # 0.000 M/sec 8 CPU-migrations # 0.000 M/sec 379 page-faults # 0.000 M/sec 118,261,821,200 cycles # 2.570 GHz 119,230,362,230 instructions # 1.01 insns per cycle 25,089,204,844 branches # 545.136 M/sec 3,200,514,556 branch-misses # 12.76% of all branches 46.126274884 seconds time elapsed
BubbleSortB Limit = 10:
Performance counter stats for './bubbleB.out': 26091.323705 task-clock # 0.998 CPUs utilized 28 context-switches # 0.000 M/sec 2 CPU-migrations # 0.000 M/sec 379 page-faults # 0.000 M/sec 64,822,368,062 cycles # 2.484 GHz 137,780,774,165 instructions # 2.13 insns per cycle 25,052,329,633 branches # 960.179 M/sec 3,019,138 branch-misses # 0.01% of all branches 26.149447493 seconds time elapsed
BubbleSortB-Limit = 50000:
Performance counter stats for './bubbleB.out': 51644.210268 task-clock # 0.983 CPUs utilized 2,138 context-switches # 0.000 M/sec 69 CPU-migrations # 0.000 M/sec 378 page-faults # 0.000 M/sec 144,600,738,759 cycles # 2.800 GHz 124,273,104,207 instructions # 0.86 insns per cycle 25,104,320,436 branches # 486.101 M/sec 3,929,572,460 branch-misses # 15.65% of all branches 52.511233236 seconds time elapsed
quelle
Erstens bin ich mir über C ++ nicht sicher, aber ich kann etwas über Java sprechen.
Hier ist ein Code,
public class Example { public static boolean less(final int a, final int b) { return a < b; } public static boolean lessOrEqual(final int a, final int b) { return a <= b; } }
Wenn
javap -c
ich darauf laufe, bekomme ich den Bytecodepublic class Example { public Example(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static boolean less(int, int); Code: 0: iload_0 1: iload_1 2: if_icmpge 7 5: iconst_1 6: ireturn 7: iconst_0 8: ireturn public static boolean lessOrEqual(int, int); Code: 0: iload_0 1: iload_1 2: if_icmpgt 7 5: iconst_1 6: ireturn 7: iconst_0 8: ireturn }
Sie werden feststellen, dass der einzige Unterschied
if_icmpge
(wenn der Vergleich größer / gleich ist) gegenüberif_icmpgt
(wenn der Vergleich größer als) ist.Alles oben Genannte ist eine Tatsache, der Rest ist meine beste Vermutung, wie
if_icmpge
undif_icmpgt
basierend auf einem College-Kurs, den ich in Assemblersprache belegt habe, gehandhabt wird. Um eine bessere Antwort zu erhalten, sollten Sie nachschlagen, wie Ihre JVM damit umgeht. Ich vermute, dass C ++ auch bis zu einer ähnlichen Operation kompiliert wird.Die Art und Weise Computer vergleichen Zahlen ist eine von dem anderen subtrahiert und prüfen , ob diese Zahl 0 ist oder nicht, so dass , wenn tun ,
a < b
wenn Subtraktionenb
ausa
und sieht , wenn das Ergebnis kleiner als 0 durch das Vorzeichen des Wertes Prüfung (b - a < 0
). Um diesa <= b
zu tun, muss ein zusätzlicher Schritt ausgeführt und 1 (b - a - 1 < 0
) subtrahiert werden .Normalerweise ist dies ein sehr winziger Unterschied, aber das ist nicht irgendein Code, diese Blase Art ist freaking! O (n ^ 2) ist die durchschnittliche Häufigkeit, mit der wir diesen speziellen Vergleich durchführen, da er sich in der innersten Schleife befindet.
Ja, es kann etwas mit der Vorhersage von Zweigen zu tun haben. Ich bin mir nicht sicher, ich bin kein Experte in diesem Bereich, aber ich denke, dass dies auch eine nicht unbedeutende Rolle spielen kann.
quelle
<
, schneller zu sein als<=
. Prozessoranweisungen werden diskretisiert; Jeder Befehl muss eine ganzzahlige Anzahl von Taktzyklen benötigen - es wird keine "Zeit gespart", es sei denn, Sie können eine ganze Uhr herausdrücken. Siehe stackoverflow.com/a/12135533cmp
gefolgt von ajl
genau so lange, wie es eine erfolgreiche Verzweigungsvorhersage zulässt,cmp
gefolgt von einemjle
. stackoverflow.com/questions/12135518/is-faster-than enthält weitere Details.jle
? Oder vielleicht habe ich diese falsche Annahme auch irgendwo gehört. Ich bin mir nicht 100% sicher, woher ich das jetzt habe, da ich es wirklich in Betracht ziehe. Ich nehme theoretisch an, dass die Interpretation der JVM eines bestimmten Betriebssystems / einer bestimmten Architektur einen Unterschied machen könnte, aber ich würde jetzt annehmen, dass alle dies in einem einzigen Zyklus tun.bl
als auchble
Anweisungen, was für mich völlig überraschend ist.