2017-10-18 2 views
29

Ich habe folgendes Problem:Warum haben Arrays unterschiedlicher Integer-Größen unterschiedliche Leistung?

Die Schreibzeiten auf ein std::array für int8, int16, int32 und int64 mit jeder Größenzunahme verdoppeln. Ich kann solches Verhalten für eine 8-Bit-CPU verstehen, aber nicht für 32/64-Bit.

Warum benötigt ein 32-Bit-System viermal mehr Zeit zum Speichern von 32-Bit-Werten als zum Speichern von 8-Bit-Werten?

Hier ist mein Testcode:

und meine Ergebnisse sind folgende g++ -o array_issue_1 main.cpp -O3 -std=c++14

:

Time of processing int8 array: 9922us. 
Time of processing int16 array: 37717us. 
Time of processing int32 array: 76064us. 
Time of processing int64 array: 146803us. 

Wenn ich mit -O2 kompilieren, dann Ergebnisse

#include <iostream> 
#include <array> 
#include <chrono> 

std::array<std::int8_t, 64 * 1024 * 1024> int8Array; 
std::array<std::int16_t, 64 * 1024 * 1024> int16Array; 
std::array<std::int32_t, 64 * 1024 * 1024> int32Array; 
std::array<std::int64_t, 64 * 1024 * 1024> int64Array; 

void PutZero() 
{ 
    auto point1 = std::chrono::high_resolution_clock::now(); 
    for (auto &v : int8Array) v = 0; 
    auto point2 = std::chrono::high_resolution_clock::now(); 
    for (auto &v : int16Array) v = 0; 
    auto point3 = std::chrono::high_resolution_clock::now(); 
    for (auto &v : int32Array) v = 0; 
    auto point4 = std::chrono::high_resolution_clock::now(); 
    for (auto &v : int64Array) v = 0; 
    auto point5 = std::chrono::high_resolution_clock::now(); 
    std::cout << "Time of processing int8 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point2 - point1)).count() << "us." << std::endl; 
    std::cout << "Time of processing int16 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point3 - point2)).count() << "us." << std::endl; 
    std::cout << "Time of processing int32 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point4 - point3)).count() << "us." << std::endl; 
    std::cout << "Time of processing int64 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point5 - point4)).count() << "us." << std::endl; 
} 

int main() 
{ 
    PutZero(); 
    std::cout << std::endl << "Press enter to exit" << std::endl; 
    std::cin.get(); 
    return 0; 
} 

ich es unter Linux mit kompilieren sind 5 mal schlechter für int8!

Sie können diese Quelle auch in Windows kompilieren. Sie werden eine ähnliche Beziehung zwischen den Ergebnissen erhalten.

Update # 1

Wenn ich mit -O2 kompilieren, dann sind meine Ergebnisse folgende:

Time of processing int8 array: 60182us. 
Time of processing int16 array: 77807us. 
Time of processing int32 array: 114204us. 
Time of processing int64 array: 186664us. 

ich nicht Assembler Ausgabe analysieren. Mein wichtigster Punkt ist, dass ich in C++ effizienten Code schreiben möchte und Dinge wie diese zeigen, dass Dinge wie std::array aus Performanceperspektive herausfordernd und irgendwie kontraintuitiv sein können.

+2

Haben Sie sich den Baugruppenausgang angesehen? –

+0

Ich [schrieb einen Benchmark] (https://github.com/travisdowns/uarch-bench), der unter anderem genau das testet: die Kosten von 'BYTE',' WORD', 'DWORD' und' QWORD' schreiben. Das Ergebnis ist, dass sie alle genau einen Zyklus auf einer modernen Intel-CPU nehmen, außer wenn sie eine Cache-Line-Grenze überschreiten, an welchem ​​Punkt sie zwei Zyklen benötigen. Beachten Sie, dass größere Werte mit höherer Wahrscheinlichkeit eine Cache-Zeilengrenze überschreiten, wenn Sie an bytegranularen Speicherorten nach dem Zufallsprinzip schreiben. In der Praxis wird dieser Benchmark und der meiste Code ausgerichtete Puffer verwenden, so dass Grenzüberschreitungen überhaupt nicht auftreten. – BeeOnRope

+0

Ihr "Hauptpunkt" ist verstümmelt, können Sie es bearbeiten, um klar zu sein? PS: Sie sollten sich keine Sorgen darüber machen, in C++ "effizient" zu sein, außer auf der Ebene der Algorithmen/Bibliothek und der C++ - abstrakten Maschine & ihrer Objekte & Datentypen, bis Sie spezifische Engpässe entdecken und suchen Algorithmus/Datatyp/Objektlevel und dann Allokatoren, alles noch in C++, und wenn Sie sich jemals Gedanken über den Maschinencode machen, sollten Sie das Problem im Maschinencode und nicht in C++ angehen. – philipxy

Antwort

56

Warum benötigt ein 32-Bit-System viermal mehr Zeit zum Speichern von 32-Bit-Werten als zum Speichern von 8-Bit-Werten?

Es tut es nicht. Aber es gibt 3 verschiedene Probleme mit Ihrem Benchmark, die Ihnen diese Ergebnisse liefern.

  1. Sie haben keine Vorverschuldung. Das heißt, Sie haben die Arrays während des Benchmarks mit Fehlern versehen. Diese Seitenfehler zusammen mit der OS-Kernel-Interaktion sind ein dominierender Faktor in der Zeit.
  2. Der Compiler mit -O3 besiegt Ihren Benchmark vollständig, indem er alle Ihre Loops in memset() konvertiert.
  3. Ihr Benchmark ist speichergebunden. Sie messen also die Geschwindigkeit Ihrer Erinnerung anstelle des Kerns.

Problem 1: Die Testdaten werden nicht

Ihre Arrays deklariert, aber nicht verwendet, bevor der Benchmark Prefaulted. Aufgrund der Art und Weise, wie die Kernel- und Speicherzuweisung funktioniert, sind sie noch nicht in den Speicher zugeordnet. Nur wenn du sie anfängst, passiert das. Und wenn dies der Fall ist, verursacht dies eine sehr große Strafe für den Kernel, um die Seite abzubilden.

Dies kann durch Berühren aller Arrays vor dem Benchmark erfolgen.

Nein Pre-Fehlgeschlagene: http://coliru.stacked-crooked.com/a/1df1f3f9de420d18

g++ -O3 -Wall main.cpp && ./a.out 
Time of processing int8 array: 28983us. 
Time of processing int16 array: 57100us. 
Time of processing int32 array: 113361us. 
Time of processing int64 array: 224451us. 

Mit Pre-Fehlgeschlagene: http://coliru.stacked-crooked.com/a/7e62b9c7ca19c128

g++ -O3 -Wall main.cpp && ./a.out 
Time of processing int8 array: 6216us. 
Time of processing int16 array: 12472us. 
Time of processing int32 array: 24961us. 
Time of processing int64 array: 49886us. 

Die Zeiten, mit anderen Worten um etwa einen Faktor 4 fallen, Ihre ursprüngliche Benchmark mehr wurde die Messung des Kernels als der eigentliche Code.


Problem 2: Der Compiler wird dem Sieg über die Benchmark

Der Compiler Ihr Muster des Schreibens Nullen erkennt und vollständig ersetzt alle Schleifen mit Aufrufen an memset(). In der Tat messen Sie Anrufe an memset() mit verschiedenen Größen.

call std::chrono::_V2::system_clock::now() 
    xor esi, esi 
    mov edx, 67108864 
    mov edi, OFFSET FLAT:int8Array 
    mov r14, rax 
    call memset 
    call std::chrono::_V2::system_clock::now() 
    xor esi, esi 
    mov edx, 134217728 
    mov edi, OFFSET FLAT:int16Array 
    mov r13, rax 
    call memset 
    call std::chrono::_V2::system_clock::now() 
    xor esi, esi 
    mov edx, 268435456 
    mov edi, OFFSET FLAT:int32Array 
    mov r12, rax 
    call memset 
    call std::chrono::_V2::system_clock::now() 
    xor esi, esi 
    mov edx, 536870912 
    mov edi, OFFSET FLAT:int64Array 
    mov rbp, rax 
    call memset 
    call std::chrono::_V2::system_clock::now() 

Die Optimierung, die dies -ftree-loop-distribute-patterns tun ist. Selbst wenn Sie das ausschalten, wird Ihnen der Vektorisierer einen ähnlichen Effekt geben.


Mit -O2 sind Vektorisierung und Mustererkennung beide deaktiviert. Der Compiler gibt Ihnen also, was Sie schreiben.

.L4: 
    mov BYTE PTR [rax], 0   ;; <<------ 1 byte at a time 
    add rax, 1 
    cmp rdx, rax 
    jne .L4 
    call std::chrono::_V2::system_clock::now() 
    mov rbp, rax 
    mov eax, OFFSET FLAT:int16Array 
    lea rdx, [rax+134217728] 
.L5: 
    xor ecx, ecx 
    add rax, 2 
    mov WORD PTR [rax-2], cx  ;; <<------ 2 bytes at a time 
    cmp rdx, rax 
    jne .L5 
    call std::chrono::_V2::system_clock::now() 
    mov r12, rax 
    mov eax, OFFSET FLAT:int32Array 
    lea rdx, [rax+268435456] 
.L6: 
    mov DWORD PTR [rax], 0  ;; <<------ 4 bytes at a time 
    add rax, 4 
    cmp rax, rdx 
    jne .L6 
    call std::chrono::_V2::system_clock::now() 
    mov r13, rax 
    mov eax, OFFSET FLAT:int64Array 
    lea rdx, [rax+536870912] 
.L7: 
    mov QWORD PTR [rax], 0  ;; <<------ 8 bytes at a time 
    add rax, 8 
    cmp rdx, rax 
    jne .L7 
    call std::chrono::_V2::system_clock::now() 

Mit -O2: http://coliru.stacked-crooked.com/a/edfdfaaf7ec2882e

g++ -O2 -Wall main.cpp && ./a.out 
Time of processing int8 array: 28414us. 
Time of processing int16 array: 22617us. 
Time of processing int32 array: 32551us. 
Time of processing int64 array: 56591us. 

Nun ist es klar, dass die kleineren Wortgrößen langsamer sind. Aber Sie würden erwarten, dass die Zeiten flach wären, wenn alle Wortgrößen die gleiche Geschwindigkeit hätten. Und der Grund, warum sie nicht sind, ist wegen der Speicherbandbreite.


Problem 3: Speicherbandbreite

Da die Benchmark (wie geschrieben) Nullen nur schreibt, ist es leicht, die Speicherbandbreite für den Kern/System zu sättigen. Der Benchmark wird also davon beeinflusst, wie viel Speicherplatz berührt wird.

Um das zu beheben, müssen wir das Dataset so verkleinern, dass es in den Cache passt. Um dies zu kompensieren, werden die gleichen Daten mehrere Male wiederholt.

std::array<std::int8_t, 512> int8Array; 
std::array<std::int16_t, 512> int16Array; 
std::array<std::int32_t, 512> int32Array; 
std::array<std::int64_t, 512> int64Array; 

... 

auto point1 = std::chrono::high_resolution_clock::now(); 
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int8Array) v = 0; 
auto point2 = std::chrono::high_resolution_clock::now(); 
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int16Array) v = 0; 
auto point3 = std::chrono::high_resolution_clock::now(); 
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int32Array) v = 0; 
auto point4 = std::chrono::high_resolution_clock::now(); 
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int64Array) v = 0; 
auto point5 = std::chrono::high_resolution_clock::now(); 

Jetzt sehen wir, Timings, die viel mehr flach für die verschiedenen Textgrößen sind:

http://coliru.stacked-crooked.com/a/f534f98f6d840c5c

g++ -O2 -Wall main.cpp && ./a.out 
Time of processing int8 array: 20487us. 
Time of processing int16 array: 21965us. 
Time of processing int32 array: 32569us. 
Time of processing int64 array: 26059us. 

Der Grund, warum es nicht ganz flach ist, ist wahrscheinlich, weil es zahlreiche andere Faktoren, die bei den Compiler-Optimierungen eine Rolle spielen. Sie müssen möglicherweise auf das Loop-Enrolling zurückgreifen, um näher heranzukommen.

+0

Das ist hilfreich, aber die Frage fragt warum nimmt 'mov QWORD' doppelt so lange wie' mov DWORD'? – wally

+4

@rex Es tut es nicht. Der einzige Grund, warum das OP dies für richtig hält, ist, dass der Compiler den Benchmark gebrochen hat. Ich denke, ich werde das klären. – Mysticial

+0

Ok, mit '-O2' hast du ähnliche Zeiten für die verschiedenen Wortgrößen gesehen? Ich versuche zu replizieren und sehe einen [Unterschied] (http://coliru.stacked-crooked.com/a/2b9e3e32649afcb5) für die verschiedenen Wortgrößen. Sogar mit "-O2". Ich bekomme auch einen Unterschied (jedes Mal ist das Doppelte der vorherigen) mit MSVC. Die asm-Anweisungen werden als "rep stos byte", "rep stos", "rep stos dword" und "rep stos qword" mit Zeiten von 25344, 47447, 89533 und 178087 gemeldet. Ich versuche also zu verstehen, wie die Antwort lautet könnte sein, dass das nicht wirklich passiert. – wally

Verwandte Themen