HINWEIS im native client mailing list erschienen: Diese Antwort ist eine bearbeitete Version von etwas, das erschien in Die Microbenchmarks sind schwierig: es sei denn, Sie verstehen, was Sie gerade tun. Es ist sehr einfach, Äpfel-zu-Orangen-Vergleiche zu erstellen, die für das Verhalten, das Sie beobachten möchten, nicht relevant sind rve/messen überhaupt.
Ich werde ein wenig mit Ihrem eigenen Beispiel erarbeiten (ich werde NaCl ausschließen und bei den bestehenden, "erprobten und wahren" Technologien bleiben).
Hier ist Ihr Test als native C-Programm:
$ cat test1.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += sqrt(i);
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g %g\n", result, tt);
}
$ gcc -std=c99 -O2 test1.c -lm -o test1
$ ./test1
5.49756e+11 25.43
Ok. Wir können Milliarden Zyklen in 25,43 Sekunden machen. Aber lass uns sehen, was Zeit braucht: wir ersetzen "result + = sqrt (i);" mit "Ergebnis + = i;"
$ cat test2.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += i;
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g %g\n", result, tt);
}
$ gcc -std=c99 -O2 test2.c -lm -o test2
$ ./test2
1.80144e+16 1.21
Wow! 95% der Zeit wurde tatsächlich in der von der CPU bereitgestellten sqrt-Funktion verbracht, alles andere benötigte weniger als 5%. Aber was ist, wenn wir den Code nur ein bisschen ändern: Ersetze "printf ("% g% g \ n ", result, tt);" mit "printf ("% g \ n ", tt);" ?
$ cat test3.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += sqrt(i);
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g\n", tt);
}
$ gcc -std=c99 -O2 test3.c -lm -o test3
$ ./test
1.44
Hmm ... Sieht aus wie jetzt "sqrt" ist fast so schnell wie "+" Wie kann das sein? Wie kann printf den vorherigen Zyklus AT ALL beeinflussen?
Mal sehen:
$ gcc -std=c99 -O2 test1.c -S -o -
...
.L3:
cvtsi2sd %ebp, %xmm1
sqrtsd %xmm1, %xmm0
ucomisd %xmm0, %xmm0
jp .L7
je .L2
.L7:
movapd %xmm1, %xmm0
movss %xmm2, (%rsp)
call sqrt
movss (%rsp), %xmm2
.L2:
unpcklps %xmm2, %xmm2
addl $1, %ebp
cmpl $1000000000, %ebp
cvtps2pd %xmm2, %xmm2
addsd %xmm0, %xmm2
unpcklpd %xmm2, %xmm2
cvtpd2ps %xmm2, %xmm2
jne .L3
...
$ gcc -std=c99 -O2 test3.c -S -o -
...
xorpd %xmm1, %xmm1
...
.L5:
cvtsi2sd %ebp, %xmm0
ucomisd %xmm0, %xmm1
ja .L14
.L10:
addl $1, %ebp
cmpl $1000000000, %ebp
jne .L5
...
.L14:
sqrtsd %xmm0, %xmm2
ucomisd %xmm2, %xmm2
jp .L12
.p2align 4,,2
je .L10
.L12:
movsd %xmm1, (%rsp)
.p2align 4,,5
call sqrt
movsd (%rsp), %xmm1
.p2align 4,,4
jmp .L10
...
Erste Version sqrt tatsächlich fordert Milliarden Mal, aber zweite tut nicht, dass! Stattdessen prüft es, ob die Nummer negativ ist und ruft in diesem Fall nur sqrt auf! Warum? Was versuchen die Compiler (oder vielmehr Compiler-Autoren) hier?
Nun, es ist einfach: Da wir "Ergebnis" in dieser bestimmten Version nicht verwendet haben, kann es sicher "sqrt" Aufruf weglassen ... wenn der Wert nicht negativ ist, ist das! Wenn es dann negativ ist (abhängig von FPU-Flags) kann sqrt verschiedene Dinge tun (unsinniges Ergebnis zurückgeben, Programm abstürzen usw.). Deshalb ist diese Version dutzendfach schneller - berechnet aber keine Quadratwurzeln! Hier
ist letztes Beispiel, das zeigt, wie falsch Microbenchmarks gehen kann:
$ cat test4.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
int result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += 2;
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%d %g\n", result, tt);
}
$ gcc -std=c99 -O2 test4.c -lm -o test4
$ ./test4
2000000000 0
Ausführungszeit ist ... ZERO? Wie kann es sein? Milliarden Berechnungen in weniger als einem Wimpernschlag? Mal sehen:
$ gcc -std=c99 -O2 test1.c -S -o -
...
call clock
movq %rax, %rbx
call clock
subq %rbx, %rax
movl $2000000000, %edx
movl $.LC1, %esi
cvtsi2ssq %rax, %xmm0
movl $1, %edi
movl $1, %eax
divss .LC0(%rip), %xmm0
unpcklps %xmm0, %xmm0
cvtps2pd %xmm0, %xmm0
...
Äh, oh, Zyklus ist vollständig beseitigt!Alle Berechnungen fanden zur Kompilierzeit statt, und um die Verletzung zu beleidigen, wurden beide "Uhr" -Aufrufe vor dem eigentlichen Zyklus ausgeführt!
Was ist, wenn wir es in separate Funktion setzen?
$ cat test5.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int testfunc(int num, int max) {
int result = 0;
for(int i = 0; i < max; ++i) {
result += num;
}
return result;
}
int main() {
clock_t t = clock();
int result = testfunc(2, 1000000000);
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%d %g\n", result, tt);
}
$ gcc -std=c99 -O2 test5.c -lm -o test5
$ ./test5
2000000000 0
Immer noch das gleiche ??? Wie kann das sein?
$ gcc -std=c99 -O2 test5.c -S -o -
...
.globl testfunc
.type testfunc, @function
testfunc:
.LFB16:
.cfi_startproc
xorl %eax, %eax
testl %esi, %esi
jle .L3
movl %esi, %eax
imull %edi, %eax
.L3:
rep
ret
.cfi_endproc
...
Uh-oh: Compiler ist schlau genug, um Zyklus mit einer Multiplikation zu ersetzen!
Nun, wenn Sie NaCl auf der einen Seite und JavaScript auf der anderen Seite hinzufügen, erhalten Sie ein so komplexes System, dass die Ergebnisse buchstäblich unvorhersehbar sind. Das Problem hier ist, dass Sie für Microbenchmark versuchen, Stück Code zu isolieren und dann seine Eigenschaften zu bewerten, aber dann wird Compiler (egal JIT oder AOT) versuchen, Ihre Bemühungen zu vereiteln, weil es versucht, alle nutzlosen Berechnungen zu entfernen von deinem Programm!
Microbenchmarks nützlich, sicher, aber sie sind FORENSIC ANALYSIS-Tool, nicht etwas, das Sie verwenden möchten, um die Geschwindigkeit von zwei verschiedenen Systemen zu vergleichen! Dafür braucht man etwas "Reales" (in gewissem Sinne der Welt: etwas, was nicht durch übereifrigen Compiler in Stücke zerlegt werden kann). Arbeitsaufwand: Besonders beliebt sind Sortieralgorithmen.
Benchmarks, die sqrt verwenden, sind besonders unangenehm, weil sie, wie wir gesehen haben, normalerweise mehr als 90% der Zeit einen einzigen CPU-Befehl ausführen: sqrtsd (fsqrt, wenn es eine 32-Bit-Version ist), die natürlich identisch ist für JavaScript und NaCl. Diese Benchmarks (wenn sie richtig implementiert sind) können als Lackmustest dienen (wenn die Geschwindigkeit einiger Implementierungen zu sehr von dem abweicht, was die einfache native Version zeigt, dann machen Sie etwas falsch), aber sie sind nutzlos, da die Geschwindigkeiten von NaCl, JavaScript, C# verglichen werden. oder Visual Basic.
Warte, du hast die Frage einer Person kopiert und dann selbst, mit der Antwort einer anderen Person, von dieser Mailingliste beantwortet. –
Ja, und beide zugeschrieben. Es schien wie eine Antwort, die geteilt werden sollte. Ich freue mich, wenn die Original-Poster ihre Sachen veröffentlichen wollen. Ich versuche nicht, Kredit zu nehmen, sondern versuche nur eine Antwort zu verbreiten, die ich wirklich informativ fand. – gman
Es ist vollkommen in Ordnung, das zu tun, aber ich denke, du kannst dies als Community-Wiki-Antwort posten, da du nicht der Autor der Antwort bist. –