C ++: Optimierung der Reihenfolge der Membervariablen?
Las ich in einem blog-post durch ein game-coder für Introversion und er ist eifrig damit beschäftigt, versuchen zu quetschen CPU tick er kann aus dem code. Ein trick, den er erwähnt, off-hand, um
"re-order die member-Variablen einer
Klasse in den meisten verwendet und am wenigsten genutzt wird."
Ich bin nicht vertraut mit C++ noch mit, wie es kompiliert wird, aber ich Frage mich, ob
- Diese Aussage ist korrekt?
- Wie/Warum?
- Betrifft es auch andere (kompiliert/scripting) Sprachen?
Ich bin mir bewusst, dass die Menge an (CPU -) Zeit gespeichert durch diesen trick wären minimal, es ist nicht ein deal-breaker. Aber auf der anderen Seite, in den meisten Funktionen, es wäre ziemlich einfach zu erkennen, welche Variablen die am häufigsten verwendet werden, und anfangen, Code auf diese Weise standardmäßig.
InformationsquelleAutor der Frage DevinB | 2009-05-21
Du musst angemeldet sein, um einen Kommentar abzugeben.
Hier zwei Probleme:
Dem Grund, dass es helfen könnte, ist, dass der Speicher geladen wird in die CPU-Caches in Stücke namens "cache lines". Dies braucht Zeit, und im Allgemeinen die mehr cache-lines geladen für Ihr Objekt, desto länger dauert es. Auch, die mehr andere Sachen geworfen, die aus dem cache Platz zu schaffen, verlangsamt sich die anderen code in einer unvorhersehbaren Art und Weise.
Die Größe einer cache-Zeile hängt von der Prozessor. Wenn es ist groß im Vergleich mit der Größe Ihrer Objekte, dann sehr wenige Objekte gehen, um zu decken eine cache-line-Grenze, so dass die Optimierung des ganzen ist ziemlich irrelevant. Andernfalls, erhalten Sie möglicherweise Weg mit manchmal nur ein Teil des Objekts im cache, und den rest im Hauptspeicher oder L2-cache, vielleicht). Es ist eine gute Sache, wenn Ihre häufigste Operationen (die, die Zugriff auf die Häufig verwendeten Felder) mit so wenig cache wie möglich für das Objekt, so gruppieren Sie diese Felder zusammen gibt Ihnen eine bessere chance, dass dies geschieht.
Dem Allgemeinen Prinzip bezeichnet man als "locality of reference". Je näher zusammen die verschiedenen Speicher-Adressen, die das Programm zugreift, desto besser sind Ihre Chancen auf eine gute cache-Verhalten. Es ist oft schwierig, vorherzusagen, die Leistung im Voraus: verschiedene Prozessor-Modelle die gleiche Architektur, kann sich anders Verhalten, multi-threading bedeutet, dass Sie oft nicht wissen, was in den cache, etc. Aber es ist möglich, zu sprechen über das, was wahrscheinlich zu passieren, die meisten der Zeit. Wenn Sie wollen wissen nichts, Sie haben in der Regel, um Sie zu Messen.
Bitte beachten Sie, dass es gibt einige Probleme hier. Wenn Sie mit CPU-basierte Atomare Operationen (welche die Atomare Datentypen in C++0x in der Regel), dann können Sie feststellen, dass die CPU sperrt die gesamte cache-Zeile, um lock-Bereich. Dann, wenn Sie mehrere Atomare Felder eng zusammen mit verschiedenen threads auf verschiedene Kerne und Betriebssystem auf verschiedenen Feldern in der gleichen Zeit, werden Sie feststellen, dass alle diese atomaren Operationen sind serialisierte, weil Sie alle sperren, die gleiche Position im Speicher auch wenn Sie in Betrieb sind, die auf verschiedenen Feldern. Hatte Sie schon in Betrieb sind auf unterschiedliche cache-Linien, dann würden Sie gearbeitet haben, parallel und schneller laufen. In der Tat, als Glen (über Herb Sutter) weist in seiner Antwort auf eine kohärente-cache-Architektur dies geschieht auch ohne Atomare Operationen, und kann völlig ruinieren Ihre Tag. Also die Lokalität der Referenz ist nicht unbedingt eine gute Sache, wo mehrere Kerne beteiligt sind, auch wenn Sie teilen-cache. Sie können erwarten, dass es auf einem Gelände, das cache-findet in der Regel eine Quelle an Geschwindigkeit verlor, aber schrecklich falsch in Ihrem speziellen Fall.
Nun, ganz abgesehen von der Unterscheidung zwischen den Häufig verwendeten und weniger Häufig verwendeten Felder, je kleiner ein Objekt ist, desto weniger Speicher (und damit weniger cache) befindet. Das ist so ziemlich das gute Nachrichten, zumindest wenn Sie nicht über schwere Konflikte. Die Größe eines Objekts hängt davon ab, die Felder, und auf jegliche Polsterung, die eingesetzt werden muss zwischen Feldern, um sicherzustellen, dass Sie richtig ausgerichtet sind für die Architektur. C++ (manchmal) legt Einschränkungen auf die Reihenfolge die Felder angezeigt werden muss, in einem Objekt, basierend auf der Reihenfolge in der Sie deklariert sind. Dies ist, um low-level-Programmierung zu erleichtern. Also, wenn Ihr Objekt enthält:
dann sind die Chancen dies belegt 16 bytes im Speicher. Die Größe und Ausrichtung der int ist nicht das gleiche auf jeder Plattform, die durch die Art und Weise, aber 4 ist sehr verbreitet und dies ist nur ein Beispiel.
In diesem Fall wird der compiler einfügen 3 bytes Abstand vor dem zweiten int, richtig ausrichten, und 3 bytes Polsterung am Ende. Mit der Größe des Objekts hat, um ein Vielfaches von seiner Ausrichtung, so dass Objekte des gleichen Typs können benachbart im Speicher. Das ist alles ein array ist in C/C++, angrenzende Objekte im Speicher. Hatte das struct worden int, int, char, char, dann das gleiche Objekt gewesen sein könnte 12 bytes, weil der char hat keine Ausrichtung Voraussetzung.
Ich sagte, dass, ob int ist 4 ausgerichtet ist Plattform-abhängig: auf ARM-unbedingt zu sein, da unaligned access " wird eine hardware-exception. Auf x86-Sie können auf ints nicht ausgerichtet, aber es ist in der Regel langsamer und sind IIRC nicht-atomar. Also Compiler in der Regel (immer?) 4-richten Sie int auf x86.
Die Faustregel beim schreiben von code, wenn Sie sich sorgen über die Verpackung, ist ein Blick auf die alignment-Anforderung von jedem Mitglied der Struktur. Dann bestellen Sie die Felder mit der größten ausgerichteten Typen zuerst, dann die nächst kleinste, und so weiter unten, um die Mitglieder mit keine Ausrichtung in Ordnung Voraussetzung. Zum Beispiel, wenn ich bin versucht, zu schreiben portablen code, ich könnte kommen mit diesem:
Wenn Sie nicht wissen, die Ausrichtung in einem Feld, oder Sie schreiben portablen code, aber wollen tun Sie das beste können Sie ohne große Tricks, dann Sie davon ausgehen, dass die Angleichung Voraussetzung ist die größte Anforderung an eine grundlegende Art in der Struktur, und dass die Angleichung Voraussetzung von fundamentaler Typen ist Ihre Größe. Also, wenn deine struct enthält ein uint64_t, oder lange, lange, dann ist die beste Vermutung ist, es ist 8-ausgerichtet. Manchmal werden Sie falsch sein, aber Sie werden Recht viel Zeit.
Beachten Sie, dass die Spiele-Programmierer wie Ihr blogger wissen oft alles über Ihren Prozessor und hardware, und damit Sie nicht haben, zu erraten. Sie wissen, dass die cache-line-Größe, Sie wissen, die Größe und die Ausrichtung jeder Art, und Sie wissen, dass die struct layout-Regeln verwendet, die durch Ihre compiler (für POD und nicht-POD-Typen). Wenn Sie mehrere Plattformen unterstützen, dann können Sie Spezial-Fall ist für jeden, wenn nötig. Sie verbringen auch viel Zeit, darüber nachzudenken, auf welche Objekte in Ihrem Spiel profitieren von performance-Verbesserungen, und mit Profiler, um herauszufinden, wo die wirklichen Engpässe sind. Aber auch so, es ist nicht so eine schlechte Idee, ein paar Faustregeln, die Sie anwenden, ob das Objekt es braucht oder nicht. Solange es nicht der code unklar, "Häufig verwendete Felder an den start des Objekts" und "sort by alignment-Anforderung" sind zwei gute Regeln.
InformationsquelleAutor der Antwort
Je nach der Art von Programm, das Sie ausführen diese Beratung kann zu einem erhöhten Leistung oder es kann auch die Dinge verlangsamen drastisch.
Tun dies in einer multi-threaded-Programm bedeutet, dass Sie gehen, um die Chancen zu vergrößern "false-sharing".
Check-out Herb Sutters Artikel zum Thema hier
Ich habe es schon früher gesagt und ich werde es halten, es zu sagen. Die einzige wirkliche Möglichkeit, um eine wirkliche Leistungssteigerung zu Messen Sie Ihren code, und verwenden Sie die tools zu identifizieren, die echten Flaschenhals statt willkürlich ändern Sachen in Ihrem code-Basis.
InformationsquelleAutor der Antwort Glen
Es ist eine der Möglichkeiten der Optimierung der working set-Größe. Es ist eine gute Artikel von John Robbins wie können Sie die Geschwindigkeit der Anwendungs-performance durch die Optimierung des working set-Größe. Natürlich erfordert eine sorgfältige Auswahl der häufigsten Anwendungsfälle der end-user wird sich voraussichtlich mit der Anwendung.
InformationsquelleAutor der Antwort Canopus
Wir haben leicht unterschiedliche Richtlinien für die Mitglieder hier (ARM-Architektur Ziel, meist THUMB 16-bit-codegen aus verschiedenen Gründen):
"group by "Ausrichtung" ist etwas offensichtlich, und außerhalb des Geltungsbereichs dieser Frage; es vermeidet Polsterung, verbraucht weniger Speicher, etc.
Die zweite Kugel, obwohl, leitet sich von der kleinen 5-bit "immediate" - Feld Größe auf den DAUMEN LDRB (Load Register Byte), LDRH (Load Register Halfword) und LDR - (Load Register) Instruktionen.
5 bit bedeutet offsets von 0-31, die kodiert werden kann. Effektiv, vorausgesetzt, "das" ist praktisch in ein register (was normalerweise der Fall ist):
Wenn Sie außerhalb dieses Bereichs, mehrere Anweisungen generiert werden: entweder eine Sequenz und Fügt mit immediates zu akkumulieren, die entsprechende Adresse in ein register, oder schlimmer noch, eine Last aus dem literal pool am Ende der Funktion.
Wenn wir das tun, schlagen Sie die literal-pool, es tut weh: der literal pool geht durch den d-cache, nicht den i-cache; dies bedeutet, dass mindestens eine cacheline im Wert von Lasten von Arbeitsspeicher für die erste literal-pool zugreifen, und dann eine Vielzahl von möglichen Räumungs-und Entwertung-Probleme zwischen den d-cache und i-cache, wenn das literal pool startet nicht auf seine eigenen cache-Zeile (d.h. wenn der code endet nicht am Ende einer cache-Zeile).
(Wenn ich hatte ein paar Wünsche für den compiler, mit der wir arbeiten, einen Weg zu zwingen, literal pools beginnen, auf cacheline boundaries wäre einer von Ihnen.)
(Unabhängig, eines der Dinge, die wir tun, um zu vermeiden, literal pool-Nutzung ist, halten Sie alle unsere "globals" in einer einzigen Tabelle. Dies bedeutet ein literal pool-lookup für den "GlobalTable", anstatt mehrere lookups für jede Globale. Wenn Sie wirklich clever sind, könnten Sie in der Lage sein, um Ihre GlobalTable in irgendeine Art von Speicher zugegriffen werden kann, ohne das laden einer literal-pool-Eintrag-das war es .sbss?)
InformationsquelleAutor der Antwort leander
Während die Lokalität der Referenz zur Verbesserung des cache-Verhaltens von Daten-Zugriffe ist oft eine relevante überlegung, es gibt ein paar andere Gründe, die zur layout-Kontrolle bei der Optimierung ist erforderlich - vor allem in embedded-Systemen, obwohl die CPUs auf vielen embedded-Systemen nicht auch einen cache.
- Speicher Ausrichtung der Felder in den Strukturen
Ausrichtung sind ziemlich gut verstanden von vielen Programmierer, also werde ich nicht zu sehr ins detail gehen hier.
Auf den meisten CPU-Architekturen, die Felder einer Struktur zugegriffen werden muss bei einer einheitlichen Ausrichtung auf Effizienz. Dies bedeutet, dass, wenn Sie mischen verschiedene Größe der Felder, die der compiler fügen Sie einen Abstand zwischen den Feldern zu halten, die alignment-Anforderungen zu korrigieren. So optimieren Sie den Speicher verwendet, die von einer Struktur es ist wichtig zu beachten Sie dies und legen Sie die Felder so, dass die größten Felder sind, gefolgt von kleineren Bereichen zu halten, die erforderlich Polsterung auf ein minimum. Ist eine Struktur "verpackt" werden " zu verhindern-Polsterung, unaligned Zugriff auf Felder kommt, auf eine hohe Laufzeit Kosten, da der compiler Zugriff auf nicht ausgerichtete Felder mit einer Serie von Zugriffen auf kleinere Teile des Feldes zusammen mit Schichten und Masken zu montieren das Feld Wert in einem register.
- Offset von Häufig verwendeten Felder in einer Struktur
Ein weiterer Aspekt, der wichtig sein kann, die auf vielen embedded-Systemen ist auf die Häufig zugegriffen Felder der zu Beginn einer Struktur.
Einige Architekturen haben eine begrenzte Anzahl von verfügbaren bits in einem Befehl codieren, ein offset zu einem pointer-Zugriff, so dass, wenn Sie auf ein Feld, dessen offset übersteigt die Anzahl der bits, die der compiler verwenden Sie mehrere Anweisungen bilden einen Zeiger auf das Feld. Zum Beispiel die ARM-Thumb-Architektur hat 5 bits zu codieren, die Deklination, so kann er den Zugriff auf ein word-große Feld in einer einzigen Anweisung nur, wenn das Feld innerhalb von 124 bytes von Anfang an. Also, wenn Sie haben eine große Struktur eine Optimierung, die einen embedded engineer vielleicht möchten Sie im Auge zu behalten ist, um Häufig verwendete Felder an den Anfang der Struktur, das layout.
InformationsquelleAutor der Antwort Michael Burr
Gut das erste Mitglied muss nicht ein offset Hinzugefügt, um den Zeiger darauf zugreifen.
InformationsquelleAutor der Antwort Lou Franco
In C#, die Ordnung des Elements wird bestimmt durch den compiler, es sei denn Sie setzen das Attribut [LayoutKind.Sequential/Explicit] das zwingt den compiler, um das Layout der Struktur/Klasse, die Art und Weise Sie es sagen,.
Soweit ich das beurteilen kann, der compiler scheint zu minimieren, Verpacken, während die Ausrichtung der Datentypen auf Ihre Natürliche Reihenfolge (d.h. 4 bytes, int start auf 4-byte-Adressen).
InformationsquelleAutor der Antwort Remi Lemarchand
In der Theorie, könnte es zu reduzieren cache-misses, wenn Sie haben große Objekte. Aber meistens ist es besser, um Mitglieder der Gruppe der gleichen Größe zusammen, so dass Sie schärfere Speicher packen.
InformationsquelleAutor der Antwort Johan Kotlinski
Konzentriere ich mich auf die Leistung, rasche Ausführung der Arbeit, nicht der Speicherverbrauch.
Der compiler ohne Optimierung der Schalter, anzeigen der Variablen-Speicherbereich mit der gleichen Reihenfolge der Deklarationen im code.
Stellen Sie sich
Große Sauerei? ohne die align-Schalter, low-memory-ops. et al,, wir gehen zu müssen, einen unsigned char mit einem 64-bit Wort auf Ihrem DDR3-dimm -, und anderen 64-bit-Wort für die anderen, und doch ist das unvermeidlich, eine für die lange.
So, das ist eine fetch-pro-variable.
Jedoch, packen es, oder neu zu bestellen, wird dazu führen, dass einer hol-und einer UND-Maskierung, um in der Lage sein, die Verwendung der unsigned chars.
Also speed-Weise, die auf einem aktuellen 64-bit-Wort-memory machine, richtet, reorderings, etc, sind no-nos. Ich mache mikrocontroller-Sachen, und da die Unterschiede in verpackt/nicht verpackt sind reallllly bemerkbar (reden über <10MIPS Prozessoren, 8-bit-Wort-Erinnerungen)
Auf der Seite, es ist lange bekannt, dass die engineering-Aufwand zu optimieren, code für andere Leistung als das, was ein guter Algorithmus weist Sie tun, und was der compiler in der Lage ist, zu optimieren, führt Häufig zu brennen Gummi mit keine realen Effekte. Und eine nur-schreiben-Stück syntaxically dubius code.
Den letzten Schritt vorwärts in der Optimierung, die ich sah (uPs, glaube nicht, dass es machbar für PC-Anwendungen), kompilieren Sie Ihr Programm als einzelnes Modul, haben die compiler-Optimierung (viel mehr Allgemeine Ansicht Geschwindigkeit/Zeiger-Auflösung/Speicher packen, etc), und die linker-trash-nicht-genannt-Bibliothek Funktionen, Methoden, etc.
InformationsquelleAutor der Antwort jpinto3912
hmmm, das klingt wie eine höchst zweifelhafte Praxis, warum dann nicht der compiler darum kümmern?
InformationsquelleAutor der Antwort chickeninabiscuit
Ich bezweifle stark, dass hätte keine Auswirkungen in CPU Verbesserungen - vielleicht Lesbarkeit. Optimieren Sie die ausführbare code, wenn die Häufig ausgeführt werden basic-blocks, die ausgeführt werden, innerhalb eines bestimmten Frames im gleichen Satz von Seiten. Dies ist die gleiche Idee, aber würde nicht wissen, wie erstellen von basic-blocks innerhalb des Codes. Meine Vermutung ist, dass der compiler setzt die Funktionen in der Reihenfolge sieht es mit Ihnen auch keine Optimierung hier, so könnten Sie versuchen, und platzieren Sie gemeinsame Funktionalität zusammen.
Versuchen, und führen Sie einen profiler/- Optimierer. Zuerst kompilieren Sie mit einigen profiling-option führen Sie dann das Programm. Sobald die profilierte exe-Datei abgeschlossen ist, wird es dump einige profilierte Informationen. Nehmen Sie dieses dump und führen Sie es durch den Optimierer als Eingabe.
Bin ich nun Weg von dieser Linie der Arbeit für Jahre aber nicht viel geändert hat, wie Sie funktionieren.
InformationsquelleAutor der Antwort AndrewB