[...] (gewährt in der Mikrosekundenumgebung) [...]
Mikrosekunden summieren sich, wenn wir Millionen bis Milliarden von Dingen durchlaufen. Eine persönliche vtune / Mikrooptimierungssitzung aus C ++ (keine algorithmischen Verbesserungen):
T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds
Alles außer "Multithreading", "SIMD" (handgeschrieben, um den Compiler zu schlagen) und der 4-Valenz-Patch-Optimierung waren Speicheroptimierungen auf Mikroebene. Auch der ursprüngliche Code ab den Anfangszeiten von 32 Sekunden wurde bereits ziemlich stark optimiert (theoretisch optimale algorithmische Komplexität), und dies ist eine kürzlich durchgeführte Sitzung. Die Verarbeitung der Originalversion lange vor dieser letzten Sitzung dauerte mehr als 5 Minuten.
Die Optimierung der Speichereffizienz kann in einem Single-Thread-Kontext häufig von mehreren bis zu Größenordnungen und in Multithread-Kontexten hilfreich sein (die Vorteile eines effizienten Speicher-Rep multiplizieren sich häufig mit mehreren Threads in der Mischung).
Zur Bedeutung der Mikrooptimierung
Ich bin ein wenig aufgeregt über die Idee, dass Mikrooptimierungen Zeitverschwendung sind. Ich bin damit einverstanden, dass es ein guter allgemeiner Rat ist, aber nicht jeder tut es falsch, basierend auf Ahnungen und Aberglauben und nicht auf Messungen. Richtig gemacht, führt dies nicht unbedingt zu einer Mikrowirkung. Wenn wir Intels eigenen Embree (Raytracing-Kernel) nehmen und nur den einfachen skalaren BVH testen, den sie geschrieben haben (kein Ray-Paket, das exponentiell schwerer zu schlagen ist), und dann versuchen, die Leistung dieser Datenstruktur zu übertreffen, kann dies am meisten sein Demütigende Erfahrung, selbst für einen Veteranen, der jahrzehntelang daran gewöhnt war, Code zu profilieren und zu optimieren. Und das alles aufgrund von Mikrooptimierungen. Ihre Lösung kann über hundert Millionen Strahlen pro Sekunde verarbeiten, wenn ich Industrieprofis im Raytracing gesehen habe, die das können. '
Es gibt keine Möglichkeit, eine einfache Implementierung eines BVH mit nur einem algorithmischen Fokus vorzunehmen und mehr als hundert Millionen Primärstrahlschnittpunkte pro Sekunde gegen einen optimierenden Compiler (sogar Intels eigenen ICC) herauszuholen. Ein unkomplizierter erhält oft nicht einmal eine Million Strahlen pro Sekunde. Es sind Lösungen von professioneller Qualität erforderlich, um oft sogar einige Millionen Strahlen pro Sekunde zu erhalten. Es ist eine Mikrooptimierung auf Intel-Ebene erforderlich, um über hundert Millionen Strahlen pro Sekunde zu erhalten.
Algorithmen
Ich denke, Mikrooptimierung ist nicht wichtig, solange die Leistung auf der Ebene von Minuten bis Sekunden, z. B. Stunden bis Minuten, nicht wichtig ist. Wenn wir einen schrecklichen Algorithmus wie die Blasensortierung als Beispiel für eine Masseneingabe verwenden und ihn dann sogar mit einer grundlegenden Implementierung der Zusammenführungssortierung vergleichen, kann die Verarbeitung des ersteren Monate dauern, der letztere möglicherweise 12 Minuten von quadratischer vs linearithmischer Komplexität.
Der Unterschied zwischen Monaten und Minuten wird wahrscheinlich dazu führen, dass die meisten Menschen, auch diejenigen, die nicht in leistungskritischen Bereichen arbeiten, die Ausführungszeit als inakzeptabel betrachten, wenn Benutzer monatelang warten müssen, um ein Ergebnis zu erhalten.
Wenn wir die nicht mikrooptimierte, unkomplizierte Zusammenführungssortierung mit der Quicksortierung vergleichen (die der Zusammenführungssortierung überhaupt nicht algorithmisch überlegen ist und nur Verbesserungen auf Mikroebene für die Referenzlokalität bietet), wird die mikrooptimierte Quicksortierung möglicherweise abgeschlossen 15 Sekunden im Gegensatz zu 12 Minuten. Es kann durchaus akzeptabel sein, Benutzer 12 Minuten warten zu lassen (Kaffeepause).
Ich denke, dieser Unterschied ist für die meisten Menschen zwischen 12 Minuten und 15 Sekunden wahrscheinlich vernachlässigbar, und deshalb wird die Mikrooptimierung oft als nutzlos angesehen, da sie oft nur dem Unterschied zwischen Minuten und Sekunden entspricht und nicht Minuten und Monaten. Der andere Grund, warum ich es für nutzlos halte, ist, dass es oft auf Bereiche angewendet wird, die keine Rolle spielen: ein kleiner Bereich, der nicht einmal kurvenreich und kritisch ist und einen fragwürdigen Unterschied von 1% ergibt (was sehr wohl nur Rauschen sein kann). Aber für Leute, die sich für diese Art von Zeitunterschieden interessieren und bereit sind, sie zu messen und richtig zu machen, lohnt es sich, zumindest die Grundkonzepte der Speicherhierarchie zu beachten (insbesondere die oberen Ebenen in Bezug auf Seitenfehler und Cache-Fehler). .
Java lässt viel Raum für gute Mikrooptimierungen
Puh, sorry - mit dieser Art von Schimpfen beiseite:
Verhindert die "Magie" der JVM den Einfluss eines Programmierers auf Mikrooptimierungen in Java?
Ein bisschen, aber nicht so viel, wie die Leute vielleicht denken, wenn Sie es richtig machen. Wenn Sie beispielsweise Bildverarbeitung in nativem Code mit handgeschriebenem SIMD, Multithreading und Speicheroptimierungen (Zugriffsmuster und möglicherweise sogar Darstellung je nach Bildverarbeitungsalgorithmus) durchführen, können Sie problemlos 32 Millionen Pixel pro Sekunde für 32 Sekunden verarbeiten. Bit-RGBA-Pixel (8-Bit-Farbkanäle) und manchmal sogar Milliarden pro Sekunde.
Es ist unmöglich, in Java irgendwo in die Nähe zu kommen, wenn Sie sagen, dass Sie ein Pixel
Objekt erstellt haben (dies allein würde die Größe eines Pixels von 4 Byte auf 16 auf 64-Bit erhöhen).
Sie könnten jedoch viel näher kommen, wenn Sie das Pixel
Objekt meiden , ein Array von Bytes verwenden und ein Image
Objekt modellieren . Java ist dort immer noch ziemlich kompetent, wenn Sie anfangen, Arrays einfacher alter Daten zu verwenden. Ich habe diese Art von Dingen schon einmal in Java ausprobiert und war ziemlich beeindruckt, vorausgesetzt , Sie erstellen nicht überall ein paar kleine Teeny-Objekte, die viermal größer als normal sind (z. B. Verwendung int
anstelle von Integer
), und beginnen, Bulk-Interfaces wie eine zu modellieren Image
Schnittstelle, nicht Pixel
Schnittstelle. Ich würde sogar sagen, dass Java mit der C ++ - Leistung mithalten kann, wenn Sie einfache alte Daten und keine Objekte (große Arrays von float
z Float
. B. nicht ) durchlaufen .
Vielleicht noch wichtiger als die Speichergrößen ist, dass ein Array von int
eine zusammenhängende Darstellung garantiert. Ein Array von Integer
nicht. Kontiguität ist häufig für die Referenzlokalität wesentlich, da mehrere Elemente (z. ints
B. 16 ) alle in eine einzelne Cache-Zeile passen und möglicherweise vor der Räumung mit effizienten Speicherzugriffsmustern zusammen zugegriffen werden können. In der Zwischenzeit kann eine einzelne Integer
irgendwo im Speicher gestrandet sein, wobei der umgebende Speicher irrelevant ist, nur um diesen Speicherbereich in eine Cache-Zeile zu laden, nur um eine einzelne Ganzzahl vor der Räumung zu verwenden, im Gegensatz zu 16 Ganzzahlen. Auch wenn wir wunderbar Glück und Umgebung hattenIntegers
Wenn alle im Speicher nebeneinander liegen, können wir nur 4 in eine Cache-Zeile einfügen, auf die vor der Räumung zugegriffen werden kann, da Integer
sie viermal größer ist, und das ist im besten Fall.
Und es gibt viele Mikrooptimierungen, da wir unter derselben Speicherarchitektur / -hierarchie vereint sind. Speicherzugriffsmuster spielen keine Rolle, egal welche Sprache Sie verwenden. Konzepte wie das Kacheln / Blockieren von Schleifen werden in C oder C ++ im Allgemeinen weitaus häufiger angewendet, aber sie kommen Java ebenso zugute.
Ich habe kürzlich in C ++ gelesen, dass manchmal die Reihenfolge der Datenelemente Optimierungen liefern kann, [...]
Die Reihenfolge der Datenelemente spielt in Java im Allgemeinen keine Rolle, aber das ist meistens eine gute Sache. In C und C ++ ist es aus ABI-Gründen oft wichtig, die Reihenfolge der Datenelemente beizubehalten, damit Compiler sich nicht damit anlegen. Dort arbeitende menschliche Entwickler müssen darauf achten, ihre Datenelemente in absteigender Reihenfolge (größte bis kleinste) anzuordnen, um zu vermeiden, dass beim Auffüllen Speicherplatz verschwendet wird. Mit Java kann die JIT anscheinend die Mitglieder im laufenden Betrieb für Sie neu anordnen, um eine korrekte Ausrichtung zu gewährleisten und gleichzeitig das Auffüllen zu minimieren. Vorausgesetzt, dies ist der Fall, automatisiert dies etwas, was durchschnittliche C- und C ++ - Programmierer häufig schlecht machen können, und verschwendet auf diese Weise Speicher ( Dies verschwendet nicht nur Speicher, sondern verschwendet häufig Geschwindigkeit, indem der Schritt zwischen AoS-Strukturen unnötig erhöht und mehr Cache-Fehler verursacht werden. Es' Es ist eine sehr roboterhafte Sache, Felder neu anzuordnen, um die Polsterung zu minimieren. Idealerweise beschäftigen sich Menschen damit nicht. Die einzige Zeit, in der die Feldanordnung auf eine Weise von Bedeutung sein kann, bei der ein Mensch die optimale Anordnung kennen muss, ist, wenn das Objekt größer als 64 Byte ist und wir Felder basierend auf dem Zugriffsmuster (nicht optimaler Auffüllung) anordnen - in diesem Fall Dies könnte ein menschlicheres Unterfangen sein (erfordert das Verständnis kritischer Pfade, von denen einige Informationen sind, die ein Compiler möglicherweise nicht vorhersehen kann, ohne zu wissen, was Benutzer mit der Software tun werden).
Wenn nicht, könnten die Leute Beispiele dafür geben, welche Tricks Sie in Java verwenden können (neben einfachen Compiler-Flags).
Der größte Unterschied in Bezug auf eine optimierende Mentalität zwischen Java und C ++ besteht für mich darin, dass Sie in C ++ in einem leistungskritischen Szenario möglicherweise Objekte verwenden können, die ein wenig (winzig) mehr als Java sind. Zum Beispiel kann C ++ eine Ganzzahl ohne jeglichen Overhead in eine Klasse einbinden (überall Benchmarking). Java muss diesen Overhead für Metadatenzeiger + Ausrichtungsauffüllung pro Objekt haben, weshalb er Boolean
größer ist als boolean
(aber im Gegenzug bietet er einheitliche Vorteile der Reflexion und die Möglichkeit, alle Funktionen zu überschreiben, die nicht final
für jedes einzelne UDT markiert sind ).
In C ++ ist es etwas einfacher, die Kontiguität von Speicherlayouts über inhomogene Felder hinweg zu steuern (z. B. Verschachtelung von Floats und Ints in ein Array durch eine Struktur / Klasse), da die räumliche Lokalität häufig verloren geht (oder zumindest die Kontrolle verloren geht). in Java beim Zuweisen von Objekten über den GC.
... aber oft teilen die leistungsstärksten Lösungen diese ohnehin auf und verwenden ein SoA-Zugriffsmuster über zusammenhängende Arrays einfacher alter Daten. Für die Bereiche, in denen Spitzenleistung erforderlich ist, sind die Strategien zur Optimierung des Speicherlayouts zwischen Java und C ++ häufig dieselben. Oft müssen Sie diese winzigen objektorientierten Schnittstellen zugunsten von Schnittstellen im Sammlungsstil abreißen, die beispielsweise Hot / Kaltfeldaufteilung, SoA-Wiederholungen usw. Inhomogene AoSoA-Wiederholungen scheinen in Java unmöglich zu sein (es sei denn, Sie haben nur ein rohes Array von Bytes oder ähnliches verwendet), aber dies ist in seltenen Fällen der Fall, in denen beideSequentielle und Direktzugriffsmuster müssen schnell sein und gleichzeitig eine Mischung von Feldtypen für heiße Felder aufweisen. Für mich ist der größte Teil des Unterschieds in der Optimierungsstrategie (auf der allgemeinen Ebene) zwischen diesen beiden umstritten, wenn Sie nach Spitzenleistungen streben.
Die Unterschiede variieren erheblich, wenn Sie einfach nach einer "guten" Leistung greifen. Wenn Sie nicht so viel mit kleinen Objekten wie Integer
vs. tun int
können, kann dies eher eine PITA sein, insbesondere in Bezug auf die Art und Weise, wie sie mit Generika interagiert . Es ist etwas schwieriger, nur eine generische Datenstruktur als zentrales Optimierungsziel in Java zu erstellen, das für usw. funktioniert int
, float
während diese größeren und teuren UDTs vermieden werden. In den leistungskritischsten Bereichen müssen jedoch häufig eigene Datenstrukturen von Hand gerollt werden ohnehin auf einen ganz bestimmten Zweck abgestimmt, so dass es nur für Code ärgerlich ist, der nach guter Leistung strebt, aber nicht nach Spitzenleistung.
Objekt-Overhead
Beachten Sie, dass der Overhead von Java-Objekten (Metadaten und Verlust der räumlichen Lokalität und vorübergehender Verlust der zeitlichen Lokalität nach einem anfänglichen GC-Zyklus) häufig sehr groß ist für Dinge, die wirklich klein sind (wie int
vs. Integer
) und die in einer Datenstruktur millionenfach gespeichert werden weitgehend zusammenhängend und in sehr engen Schleifen zugänglich. Dieses Thema scheint sehr sensibel zu sein, daher sollte ich klarstellen, dass Sie sich bei großen Objekten wie Bildern keine Gedanken über den Objekt-Overhead machen möchten, sondern nur bei wirklich winzigen Objekten wie einem einzelnen Pixel.
Wenn jemand Zweifel an diesem Teil hat, würde ich vorschlagen, einen Benchmark zwischen der Summierung einer Million Zufallszahlen ints
und einer Million Zufallszahlen Integers
zu erstellen und dies wiederholt zu tun (der Integers
Wille wird nach einem anfänglichen GC-Zyklus im Speicher neu gemischt).
Ultimativer Trick: Schnittstellendesigns, die Raum für Optimierungen lassen
Also der ultimative Java-Trick, wie ich es sehe, wenn Sie es mit einem Ort zu tun haben, der eine schwere Last über kleinen Objekten bewältigt (z. B. a Pixel
, ein 4-Vektor, eine 4x4-Matrix, a Particle
, möglicherweise sogar ein, Account
wenn er nur wenige kleine Objekte hat Felder) besteht darin, die Verwendung von Objekten für diese kleinen Dinge zu vermeiden und Arrays (möglicherweise miteinander verkettet) aus einfachen alten Daten zu verwenden. Die Objekte wurden dann Sammlung Schnittstellen wie Image
, ParticleSystem
, Accounts
, eine Sammlung von Matrizen oder Vektoren, usw. einzelner Index zugegriffen werden kann, zB Dies ist auch einer der ultimative Design - Tricks in C und C ++, da auch ohne dieses Grundobjekt Aufwand und Durch die Modellierung der Schnittstelle auf der Ebene eines einzelnen Partikels werden die effizientesten Lösungen verhindert.