Inkrementieren Sie Vektorelemente

In welchem ​​Fall ist das Inkrement von std :: vector- Elementen schneller - wenn sie vom Typ uint8_t oder uint32_t sind ?

Um nicht abstrakt zu argumentieren, betrachten wir zwei spezifische Implementierungen:

void vector8_inc(std::vector<uint8_t>& v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } } void vector32_inc(std::vector<uint32_t>& v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } } 

Versuchen wir zu raten


Es ist einfach, diese Frage mit dem Benchmark zu beantworten, und etwas später werden wir es tun, aber zuerst werden wir versuchen, sie zu erraten (dies wird als "auf Grundprinzipien basierendes Denken" bezeichnet - es klingt wissenschaftlicher).

Zunächst lohnt es sich, eine Frage zu stellen: Wie groß sind diese Vektoren ?

Nun, lasst uns eine Nummer auswählen. Es gebe jeweils 20.000 Elemente.

Weiterhin ist bekannt, dass wir den Intel Skylake-Prozessor testen werden - wir werden die Eigenschaften von Additionsbefehlen für 8-Bit- und 32-Bit- Operanden mit direkter Adressierung sehen. Es stellt sich heraus, dass ihre Hauptanzeigen die gleichen sind: 1 Operation pro Zyklus und eine Verzögerung von 4 Zyklen pro Speicherzugriff (1). In diesem Fall spielt die Verzögerung keine Rolle, da jede Additionsoperation unabhängig ausgeführt wird, so dass die berechnete Geschwindigkeit 1 Element pro Zyklus beträgt, vorausgesetzt, dass der gesamte Rest der Arbeit an der Schleife parallel ausgeführt wird.

Sie können auch feststellen, dass 20.000 Elemente einem 20-KB-Datensatz für die Version mit uint8_t und bis zu 80 KB für die Version mit uint32_t entsprechen . Im ersten Fall passen sie ideal in den L1-Level-Cache moderner x86-basierter Computer und im zweiten nicht. Es stellt sich heraus, dass die 8-Bit-Version durch effizientes Caching einen Vorsprung bekommt.

Schließlich stellen wir fest, dass unsere Aufgabe dem klassischen Fall der automatischen Vektorisierung sehr ähnlich ist: In einer Schleife mit einer bekannten Anzahl von Iterationen wird eine arithmetische Operation für Elemente ausgeführt, die sich nacheinander im Speicher befinden. In diesem Fall sollte die 8-Bit-Version einen enormen Vorteil gegenüber der 32-Bit-Version haben, da eine Vektoroperation viermal so viele Elemente verarbeitet und Intel-Prozessoren im Allgemeinen Vektoroperationen an Einzelbyte-Elementen mit derselben Geschwindigkeit wie über 32 ausführen. Bit-Elemente.

Okay, hör auf zu schimpfen. Es ist Zeit, sich dem Test zuzuwenden.

Benchmark


Ich habe die folgenden Timings für Vektoren mit 20.000 Elementen auf gcc 8 und Clang 8-Compilern mit verschiedenen Optimierungsstufen erhalten:


Es stellt sich heraus, dass die Version mit uint32_t mit Ausnahme der Stufe -O1 schneller als die Version mit uint8_t ist und in einigen Fällen von Bedeutung ist: 5,4-mal auf gcc auf der Stufe -O3 und genau 8-mal auf clang auf beiden Stufen, -O2 und - O3 . Ja, das Inkrement von 32-Bit-Ganzzahlen in std :: vector ist bis zu achtmal schneller als das Inkrement von 8-Bit-Ganzzahlen im gängigen Compiler mit Standardoptimierungseinstellungen.

Wenden wir uns wie üblich der Assembler-Auflistung zu, in der Hoffnung, dass sie Aufschluss darüber gibt, was gerade passiert.

Hier ist eine Auflistung für gcc 8 auf der -O2- Ebene, wobei die 8-Bit-Version „nur“ 1,5-mal langsamer ist als die 32-Bit-Version (2):

8-Bit:

 .L3: inc BYTE PTR [rdx+rax] mov rdx, QWORD PTR [rdi] inc rax mov rcx, QWORD PTR [rdi+8] sub rcx, rdx cmp rax, rcx jb .L3 

32-Bit:
 .L9: inc DWORD PTR [rax] add rax, 4 cmp rax, rdx jne .L9 

Die 32-Bit-Version sieht genauso aus, wie wir es von einer unentwickelten (3) Schleife erwartet hatten: ein Inkrement (4) mit einer Adresse, dann drei Schleifensteuerbefehle: Addiere rax , 4 - ein Inkrement der induktiven Variablen (5) und ein paar cmp- und jne- Befehle um die Bedingungen für das Verlassen der Schleife und den bedingten Sprung darauf zu überprüfen. Alles sieht gut aus - die Bereitstellung würde die Kosten für das Erhöhen des Zählers und das Überprüfen des Zustands kompensieren, und unser Code würde fast die maximal mögliche Geschwindigkeit von 1 Element pro Taktzyklus (6) erreichen, aber für eine Open-Source-Anwendung reicht dies aus. Und was ist mit der 8-Bit-Version? Neben dem Befehl inc mit der Adresse werden zwei weitere Befehle zum Lesen aus dem Speicher ausgeführt sowie der Unterbefehl , der aus dem Nichts stammt.

Hier ist eine Auflistung mit Kommentaren:

8-Bit:

 .L3: inc BYTE PTR [rdx+rax] ;    v[i] mov rdx, QWORD PTR [rdi] ;  v.begin inc rax ; i++ mov rcx, QWORD PTR [rdi+8] ;  v.end sub rcx, rdx ; end - start (.. vector.size()) cmp rax, rcx ; i < size() jb .L3 ; .   i < size() 

Dabei sind vector :: begin und vector :: end die internen Zeiger von std :: vector , mit denen Anfang und Ende der Folge von Elementen innerhalb des dafür ausgewählten Bereichs angegeben werden (7). Dies sind im Wesentlichen dieselben Werte die verwendet werden, um vector :: begin () und vector :: end () zu implementieren (obwohl sie von einem anderen Typ sind). Es stellt sich heraus, dass alle zusätzlichen Befehle nur eine Folge der Berechnung von vector.size () sind . Es scheint nichts Ungewöhnliches? Schließlich wird in der 32-Bit-Version natürlich auch size () berechnet, diese Befehle waren jedoch nicht in dieser Auflistung enthalten. Die Berechnung von size () fand nur einmal statt - außerhalb der Schleife.

Also, was ist los? Die kurze Antwort lautet Pointer Aliasing . Ich werde im Folgenden eine ausführliche Antwort geben.

Ausführliche Antwort


Der Vektor v wird als Referenz an die Funktion übergeben, die tatsächlich ein maskierter Zeiger ist. Der Compiler muss zu den Elementen v :: begin und v :: end des Vektors gehen, um seine Größe size () zu berechnen. In unserem Beispiel wird size () bei jeder Iteration berechnet. Der Compiler ist jedoch nicht verpflichtet, den Quellcode blind zu befolgen: Er kann das Ergebnis des Aufrufs der size () - Funktion durchaus außerhalb der Schleife tragen, aber nur dann, wenn er sicher ist, dass sich die Semantik des Programms nicht ändert . Unter diesem Gesichtspunkt ist das Inkrement v [i] ++ die einzige problematische Stelle in der Schleife. Die Aufzeichnung erfolgt an einer unbekannten Adresse. Kann eine solche Operation den Wert von size () ändern?

Wenn der Datensatz in std :: vector <uint32_t> auftritt (d. H. Durch den Zeiger uint32_t * ), kann er den Wert size () nicht ändern. Das Schreiben in Objekte vom Typ uint32_t kann nur Objekte vom Typ uint32_t ändern, und die zur Berechnung von size () verwendeten Zeiger haben einen anderen Typ (8).

Im Fall von uint8_t lautet die Antwort zumindest auf gängigen Compilern (9): Ja, theoretisch kann sich der Wert von size () ändern , da uint8_t ein Alias ​​für nicht signierte Zeichen und Arrays des Typs " nicht signierte Zeichen" (und " Zeichen" ) ist Alias ​​mit jedem anderen Typ . Dies bedeutet, dass das Schreiben in uint8_t-Zeiger nach Angaben des Compilers den Inhalt des Speichers unbekannter Herkunft an einer beliebigen Adresse ändern kann (10). Daher wird davon ausgegangen, dass jede Inkrementierungsoperation v [i] ++ den size () - Wert ändern kann und daher gezwungen ist, ihn bei jeder Iteration der Schleife neu zu berechnen.

Wir alle wissen, dass das Schreiben in den Speicher, auf den std :: vector zeigt, niemals seine eigene Größe ändert () , da dies bedeuten würde, dass der Vektor selbst irgendwie in seinem eigenen Heap zugeordnet wurde, und das ist praktisch unmöglich und dem Problem von Huhn und Eiern verwandt (11). Dem Compiler ist dies leider nicht bekannt!

Was ist mit dem Rest der Ergebnisse?


Nun, wir haben herausgefunden, warum die Version mit uint8_ etwas langsamer ist als die Version von uint32_t auf gcc auf der -O2- Ebene. Aber warum erklären Sie den enormen Unterschied - bis zu 8-mal - bei clang oder dem gleichen gcc bei -O3 ?

Hier ist alles einfach: Im Fall von uint32_t kann clang die automatische Vektorisierung von Schleifen durchführen:

 .LBB1_6: ; =>This Inner Loop Header: Depth=1 vmovdqu ymm1, ymmword ptr [rax + 4*rdi] vmovdqu ymm2, ymmword ptr [rax + 4*rdi + 32] vmovdqu ymm3, ymmword ptr [rax + 4*rdi + 64] vmovdqu ymm4, ymmword ptr [rax + 4*rdi + 96] vpsubd ymm1, ymm1, ymm0 vpsubd ymm2, ymm2, ymm0 vpsubd ymm3, ymm3, ymm0 vpsubd ymm4, ymm4, ymm0 vmovdqu ymmword ptr [rax + 4*rdi], ymm1 vmovdqu ymmword ptr [rax + 4*rdi + 32], ymm2 vmovdqu ymmword ptr [rax + 4*rdi + 64], ymm3 vmovdqu ymmword ptr [rax + 4*rdi + 96], ymm4 vmovdqu ymm1, ymmword ptr [rax + 4*rdi + 128] vmovdqu ymm2, ymmword ptr [rax + 4*rdi + 160] vmovdqu ymm3, ymmword ptr [rax + 4*rdi + 192] vmovdqu ymm4, ymmword ptr [rax + 4*rdi + 224] vpsubd ymm1, ymm1, ymm0 vpsubd ymm2, ymm2, ymm0 vpsubd ymm3, ymm3, ymm0 vpsubd ymm4, ymm4, ymm0 vmovdqu ymmword ptr [rax + 4*rdi + 128], ymm1 vmovdqu ymmword ptr [rax + 4*rdi + 160], ymm2 vmovdqu ymmword ptr [rax + 4*rdi + 192], ymm3 vmovdqu ymmword ptr [rax + 4*rdi + 224], ymm4 add rdi, 64 add rsi, 2 jne .LBB1_6 

Der Zyklus wurde 8-mal implementiert, und dies ist im Allgemeinen die maximale Leistung, die Sie erhalten können: Ein Vektor (8 Elemente) pro Taktzyklus für den L1-Cache (dies funktioniert nicht mehr, da ein Schreibvorgang pro Taktzyklus begrenzt ist (12)).

Die Vektorisierung wird für uint8_t nicht durchgeführt, da sie durch die Notwendigkeit behindert wird, size () zu berechnen, um die Schleifenbedingung bei jeder Iteration zu überprüfen. Der Grund für die Verzögerung ist immer noch der gleiche, aber die Verzögerung selbst ist viel größer.

Die niedrigsten Timings werden durch die automatische Vektorisierung erklärt: gcc wendet sie nur auf die Ebene -O3 an, und clang gilt standardmäßig sowohl für die Ebene -O2 als auch für die Ebene -O3 . Der Compiler -cc level gcc generiert etwas langsameren Code als clang, da er die autovektorisierte Schleife nicht erweitert.

Korrigieren Sie die Situation


Wir haben herausgefunden, wo das Problem liegt - wie können wir es beheben?

Versuchen wir zunächst eine Möglichkeit, die jedoch nicht funktioniert: Wir schreiben einen idiomatischeren Zyklus, der auf einem Iterator basiert:

 for (auto i = v.begin(); i != v.end(); ++i) { (*i)++; } 

Der Code, den gcc auf der Ebene -O2 generiert, ist etwas besser als die Option mit size () :

 .L17: add BYTE PTR [rax], 1 add rax, 1 cmp QWORD PTR [rdi+8], rax jne .L17 

Aus zwei zusätzlichen Leseoperationen wurde eine, da wir nun mit dem Endzeiger des Vektors vergleichen, anstatt size () neu zu berechnen, wobei der Vektor-Startzeiger vom Endzeiger subtrahiert wird. Nach der Anzahl der Anweisungen hat dieser Code uint32_t eingeholt, da die zusätzliche Leseoperation mit der Vergleichsoperation zusammengeführt wurde. Das Problem ist jedoch nicht behoben und die automatische Vektorisierung ist immer noch nicht verfügbar, sodass uint8_t immer noch deutlich hinter uint32_t zurückliegt - mehr als fünfmal auf gcc und clang auf den Ebenen, auf denen die automatische Vektorisierung bereitgestellt wird.

Versuchen wir etwas anderes. Es wird uns nicht wieder gelingen, oder vielmehr, wir werden eine andere unwirksame Methode finden.

In dieser Version berechnen wir size () nur einmal vor der Schleife und setzen das Ergebnis in eine lokale Variable:

 for (size_t i = 0, s = v.size(); i < s; i++) { v[i]++; } 

Es scheint zu funktionieren? Das Problem war size () , und jetzt haben wir den Compiler angewiesen, das Ergebnis von size () an die lokale Variable s am Anfang der Schleife zu übergeben, und die lokalen Variablen schneiden sich, wie Sie wissen, nicht mit anderen Daten. Wir haben tatsächlich getan, was der Compiler nicht konnte. Und der Code, den es generiert, ist tatsächlich besser (im Vergleich zum Original):

 .L9: mov rdx, QWORD PTR [rdi] add BYTE PTR [rdx+rax], 1 add rax, 1 cmp rax, rcx jne .L9 

Es gibt nur eine zusätzliche Leseoperation und keinen Unterbefehl . Was macht dieser zusätzliche Befehl ( rdx, QWORD PTR [rdi] ), wenn er nicht an der Größenberechnung beteiligt ist? Es liest den data () Zeiger von v !

Der Ausdruck v [i] wird als * (v.data () + i) implementiert, und das von data () zurückgegebene Element ( und tatsächlich ein regulärer Anfangszeiger) wirft das gleiche Problem auf wie size () . Zwar habe ich diesen Vorgang in der Originalversion nicht bemerkt, da er dort "kostenlos" war, da er noch durchgeführt werden musste, um die Größe zu berechnen.

Tragen Sie mit ein wenig mehr, wir haben fast eine Lösung gefunden. Sie müssen nur alle Abhängigkeiten vom Inhalt von std :: vector aus unserer Schleife entfernen. Der einfachste Weg, dies zu tun, besteht darin, unsere Redewendung mit einem Iterator ein wenig zu ändern:

 for (auto i = v.begin(), e = v.end(); i != e; ++i) { (*i)++; } 

Jetzt hat sich alles dramatisch geändert (hier vergleichen wir nur Versionen mit uint8_t - in einem speichern wir das Iteratorende in einer lokalen Variablen vor der Schleife, in dem anderen - nein):


Diese kleine Änderung führte zu einer 20-fachen Geschwindigkeitssteigerung bei Niveaus mit automatischer Vektorisierung. Außerdem hat der Code mit uint8_t nicht nur den Code mit uint32_t eingeholt - er hat ihn mit gcc -O3 fast genau viermal überholt und -O2 und -O3 geklirrt , wie wir zu Beginn erwartet hatten, wobei wir uns auf die Vektorisierung gestützt haben: am Ende genau viermal mehr Elemente können durch eine Vektoroperation verarbeitet werden, und wir benötigen viermal weniger Bandbreite - unabhängig von der Cache-Ebene (13).

Wenn Sie zu diesem Platz lesen, dann müssen Sie sich die ganze Zeit ausgerufen haben:

Aber was ist mit der in C ++ 11 eingeführten for-Schleife mit Band-Traversal ?

Ich beeile mich, Ihnen zu gefallen: es funktioniert! Dies ist in der Tat syntaktischer Zucker, hinter dem sich unsere Version mit einem Iterator in fast derselben Form verbirgt, in der wir den Endzeiger vor dem Beginn der Schleife in einer lokalen Variablen fixiert haben. Also ist seine Geschwindigkeit gleich.

Wenn wir uns plötzlich entschließen, in alte Höhlenzeiten zurückzukehren und eine C-ähnliche Funktion zu schreiben, würde ein solcher Code genauso gut funktionieren:

 void array_inc(uint8_t* a, size_t size) { for (size_t i = 0; i < size; i++) { a[i]++; } } 

Hier werden der Zeiger auf das Array a und die Größenvariable als Wert an die Funktion übergeben, sodass sie nicht wie lokale Variablen durch Schreiben in den Zeiger a (14) geändert werden können. Die Leistung dieses Codes entspricht der der vorherigen Optionen.

Schließlich können Sie auf Compilern, auf denen diese Option verfügbar ist, einen Vektor mit __restrict (15) deklarieren:

 void vector8_inc_restrict(std::vector<uint8_t>& __restrict v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } } 

Das Schlüsselwort __restrict ist nicht Teil des C ++ - Standards, sondern seit C99 Teil des C- Standards (als Einschränkung ). Wenn es als C ++ - Erweiterung im Compiler implementiert ist, folgt es höchstwahrscheinlich der Semantik von C. Natürlich gibt es in C keine Links, sodass Sie den Link zum Vektor mental durch einen Zeiger auf den Vektor ersetzen können.

Beachten Sie, dass restricted keine transitiven Eigenschaften hat : Die Aktion des __restrict-Bezeichners , mit der eine Verknüpfung zu std :: vector deklariert wird, gilt nur für die Elemente des Vektors selbst und nicht für die Heap-Region, auf die v.data () verweist . In unserem Fall ist mehr nicht erforderlich, da es (wie bei lokalen Variablen) ausreicht, den Compiler davon zu überzeugen, dass die Terme selbst, die auf den Anfang und das Ende des Vektors zeigen, sich mit nichts überschneiden. Die Einschränkungsklausel ist jedoch weiterhin relevant, da das Schreiben über v.data () möglicherweise weiterhin dazu führt, dass sich andere Objekte in Ihrer Funktion aufgrund von Aliasing ändern.

Enttäuschung


Hier kommen wir zum letzten - und sehr enttäuschenden - Schluss. Tatsache ist, dass alle oben gezeigten Lösungen nur für diesen speziellen Fall anwendbar sind, wenn der Vektor theoretisch mit sich selbst interferieren kann. Die Lösung bestand darin, die Schleife zu verlassen oder das Ergebnis des Aufrufs von size () oder end () des Vektors zu isolieren und dem Compiler nicht mitzuteilen, dass das Schreiben in die Vektordaten keine Auswirkungen auf andere Daten hat. Ein solcher Code ist mit zunehmender Funktionsgröße nur schwer skalierbar.

Das Aliasing-Problem ist nicht beseitigt, und die Schreibbefehle können immer noch "überall" abgelegt werden - es gibt einfach keine anderen Daten in dieser Funktion, die betroffen sein könnten ... vorerst. Sobald ein neuer Code darin erscheint, wird alles wiederholt. Hier ist ein Beispiel aus der Hand . Wenn Sie in kleinen Schleifen in Arrays von Elementen des Typs uint8_t schreiben, müssen Sie bis zum Ende mit dem Compiler kämpfen (16).

Kommentare


Ich freue mich über jede Rückmeldung. Ich habe noch kein Kommentarsystem (17), daher werden wir wie üblich in diesem Thread auf HackerNews diskutieren .

  1. Wenn Sie hier auf den Speicher zugreifen, wird verstanden, dass die Kette von Abhängigkeiten den Speicher durchläuft: Schreibbefehle an derselben Adresse sollten den zuletzt dort geschriebenen Wert lesen, daher sind solche Operationen abhängig (in der Praxis wird die Umleitung zum Laden (STLF) verwendet, wenn die Aufzeichnung ausreicht oft). Abhängigkeiten des Befehls add beim Speicherzugriff können auf andere Weise auftreten, z. B. durch Berechnung der Adresse. In unserem Fall ist dies jedoch irrelevant.
  2. Hier ist nur ein kleiner Zyklus dargestellt; Der Installationscode ist einfach und funktioniert schnell. Laden Sie den Code auf godbolt hoch , um die vollständige Liste anzuzeigen .
  3. Vielleicht sollte man es einfach "minimiert" nennen? Wie dem auch sei, der gcc-Compiler schleift normalerweise nicht einmal auf den Ebenen -O2 und -O3 herum , außer in besonderen Fällen, in denen die Anzahl der Iterationen gering ist und in der Kompilierungsphase bekannt ist . Aus diesem Grund zeigt gcc im Vergleich zu clang geringere Testergebnisse, spart aber viel an Codegröße. Sie können gcc zwingen, Loops zu entrollen, indem Sie die Profiloptimierung anwenden oder das Flag -funroll-loops aktivieren .
  4. Tatsächlich ist der Befehl inc DWORD PTR [rax] in gcc eine fehlende Optimierung: Es ist fast immer besser, den Befehl add [rax], 1 zu verwenden , da er nur aus 2 kombinierten Mikrooperationen gegenüber 3 für inc besteht . In diesem Fall beträgt der Unterschied nur etwa 6%. Wenn der Zyklus jedoch geringfügig erweitert würde, so dass nur der Aufzeichnungsvorgang wiederholt würde, wäre der Unterschied signifikanter (eine weitere Erweiterung würde keine Rolle mehr spielen, da wir die Grenze von 1 erreichen würden) Aufzeichnungsvorgang pro Zyklus, der nicht von der Gesamtzahl der Mikrooperationen abhängt).
  5. Ich nenne diese Variable induktiv und nicht nur i wie im Quellcode, weil der Compiler die Einheitsoperationen des Inkrements i in 4-Byte-Inkremente des im Rax- Register gespeicherten Zeigers konvertiert und die Schleifenbedingung entsprechend korrigiert hat. In der ursprünglichen Form adressiert unsere Schleife die Elemente des Vektors und inkrementiert nach dieser Konvertierung den Zeiger / Iterator. Dies ist eine Möglichkeit, die Betriebskosten zu senken .
  6. Wenn Sie -funroll-loops aktivieren, beträgt die Geschwindigkeit auf gcc bei einem 8-fachen Rollout 1,08 Takte pro Element. Aber selbst mit diesem Flag wird er die Schleife für die Version mit 8-Bit-Elementen nicht erweitern, sodass die Verzögerung der Geschwindigkeit noch deutlicher wird!
  7. Diese Member haben einen privaten Modifizierer und ihre Namen sind implementierungsabhängig, aber in stdlibc ++ heißen sie nicht wirklich start und finish , wie in gcc. Sie heißen _Vector_base :: _ Vector_impl :: _ M_start und _Vector_base :: _ Vector_impl :: _ M_finish , d.h. Geben Sie die _Vector_impl- Struktur ein, die ein Mitglied von _M_impl (und die einzige) der _Vector_base- Klasse ist und die wiederum die Basisklasse für std :: vector ist . Na gut! Glücklicherweise kann der Compiler mit diesem Stapel von Abstraktionen problemlos umgehen.
  8. Der Standard schreibt nicht vor, wie die internen Typen von std :: vector- Elementen lauten sollen, aber in der libstdc ++ - Bibliothek werden sie einfach als Alloc :: pointer (wobei Alloc der Allokator für den Vektor ist) und für das standardmäßige, std :: zugeteilte Objekt einfach als Alloc :: pointer definiert Zeiger vom Typ T * , d.h. reguläre Zeiger auf ein Objekt - in diesem Fall uint32_t * .
  9. Ich mache diese Reservierung aus einem Grund. Es besteht der Verdacht, dass uint8_t als ein anderer Typ als char , signed char und unsigned char angesehen werden kann . Da Aliasing mit Zeichentypen funktioniert, gilt dies im Prinzip nicht für uint8_t und sollte sich wie jeder andere Nicht-Zeichentyp verhalten. Ich kenne jedoch keinen der Compiler, der das glaubt: In allen ist typedef uint8_t ein Alias ohne Vorzeichen , sodass Compiler den Unterschied zwischen ihnen nicht erkennen, auch wenn sie ihn gerne verwenden würden.
  10. Mit "unbekannter Herkunft" meine ich hier nur, dass der Compiler nicht weiß, wohin der Inhalt des Speichers zeigt oder wie er erschienen ist. Dies umfasst beliebige Zeiger, die an die Funktion übergeben werden, sowie globale und statische Variablen. , , , , , ( - ). , malloc new , , , , : , , . , malloc new .
  11. , std::vector - ? , std::vector<uint8_t> a a.data() placement new b . std::swap(a, b) , – , b ? , b . : - (, ), , .
  12. 8 , .. 32 . , std::vector .
  13. - 4 : , , – . : 8- L1, 32- – L2 , .
  14. , – : . , , «».
  15. v[i] , .
  16. . , «» , uint8_t . , , , uint8_t , . , clang, gcc , , uint8_t . - gcc , . , , - __restrict .
  17. - , , ( Disqus), ( ), .

. : Travis Downs. Incrementing vectors .

Source: https://habr.com/ru/post/de475636/


All Articles