2013-04-22 16 views
8

Jemand beschloss, einen schnellen Test zu machen, um zu sehen, wie native Client im Vergleich zu Javascript in Bezug auf Geschwindigkeit. Sie haben das getan, indem sie 10 000 000-Quadrat-Quadrat-Berechnungen durchgeführt und die benötigte Zeit gemessen haben. Das Ergebnis mit Javascript: 0,096 Sekunden und mit NaCl: 4,241 Sekunden ... Wie kann das sein? Ist Geschwindigkeit nicht einer der Gründe, NaCl überhaupt zu verwenden? Oder fehlen mir Compiler-Flags oder etwas?Warum ist mein Programm so langsam?

Heres der Code, der ausgeführt wurde:

clock_t t = clock(); 
float result = 0; 
for(int i = 0; i < 10000000; ++i) { 
    result += sqrt(i); 
} 
t = clock() - t;  
float tt = ((float)t)/CLOCKS_PER_SEC; 
pp::Var var_reply = pp::Var(tt); 
PostMessage(var_reply); 

PS: Diese Frage ist eine bearbeitete Version von etwas, das

+9

Warte, du hast die Frage einer Person kopiert und dann selbst, mit der Antwort einer anderen Person, von dieser Mailingliste beantwortet. –

+0

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

+0

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. –

Antwort

19

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.

+1

Es ist wahrscheinlich besser, die Enthüllung an den * Anfang * des Posts zu setzen, wenn man bedenkt, wie lange es ist. –