Heute schauen wir uns zwei weitere Orte an, an denen pbrt viel Zeit damit verbringt, Szenen aus dem Disney-Cartoon
"Moana" zu analysieren. Mal sehen, ob es hier möglich ist, die Produktivität zu verbessern. Dies schließt mit dem, was in pbrt-v3 ratsam ist. In einem anderen Beitrag werde ich darauf eingehen, wie weit wir gehen können, wenn wir das Verbot von Änderungen aufgeben. In diesem Fall unterscheidet sich der Quellcode zu stark von dem im Buch
Physically Based Rendering beschriebenen System.
Parser-Optimierung
Nach den im
vorherigen Artikel eingeführten Leistungsverbesserungen stieg der Anteil der Zeit, die im pbrt-Parser verbracht wurde und von Anfang an so bedeutend war, natürlich noch mehr an. Derzeit wird der Parser beim Start die meiste Zeit verwendet.
Endlich habe ich meine Kräfte gesammelt und
einen manuell geschriebenen Tokenizer und Parser für pbrt-Szenen implementiert.
Das Format der pbrt-Szenendateien ist ziemlich einfach zu
analysieren : Wenn Sie nicht in Anführungszeichen gesetzte Zeilen berücksichtigen, werden Token durch Leerzeichen getrennt und die Grammatik ist sehr einfach (es ist nie erforderlich, mehr als ein Token nach vorne zu schauen), aber Ihr eigener Parser besteht immer noch aus tausend Codezeilen, die Sie benötigen schreiben und debuggen. Es hat mir geholfen, dass es in vielen Szenen getestet werden konnte; Nachdem ich offensichtliche Störungen behoben hatte, arbeitete ich weiter, bis ich genau die gleichen Bilder wie zuvor rendern konnte: Es sollte keine Pixelunterschiede geben, da der Parser ersetzt wurde. Zu diesem Zeitpunkt war ich mir absolut sicher, dass alles richtig gemacht wurde.
Ich habe versucht, die neue Version so effizient wie möglich zu gestalten, indem ich die Eingabedateien so weit wie möglich
mmap()
std::string_view
und die neue Implementierung von
std::string_view
aus C ++ 17 verwendet habe, um die Erstellung von Kopien von Zeichenfolgen aus dem Inhalt der Datei zu minimieren. Da
strtod()
in früheren Traces viel Zeit in
parseNumber()
, habe ich
parseNumber()
mit besonderer Sorgfalt geschrieben:
parseNumber()
Ganzzahlen und reguläre Ganzzahlen werden separat verarbeitet, und im Standardfall, wenn pbrt für die Verwendung von 32-Bit-Floats kompiliert wird , verwendet
strtof()
anstelle von
strtod()
1 .
Bei der Erstellung einer Implementierung des neuen Parsers hatte ich ein wenig Angst, dass der alte Parser schneller sein könnte: Am Ende wurden Flex und Bison seit vielen Jahren entwickelt und optimiert. Ich konnte nicht im Voraus herausfinden, ob das Schreiben einer neuen Version die ganze Zeit verschwendet würde, bis ich sie fertiggestellt und zum Laufen gebracht habe.
Zu meiner Freude stellte sich heraus, dass unser eigener Parser ein großer Sieg war: Die Verallgemeinerung von Flex und Bison reduzierte die Leistung so sehr, dass die neue Version sie leicht überholte. Dank des neuen Parsers verringerte sich die Startzeit auf 13 Minuten und 21 Sekunden, dh sie beschleunigte sich um das 1,5-fache! Ein zusätzlicher Bonus war, dass es nun möglich war, alle Flex- und Bison-Unterstützung aus dem pbrt-Build-System zu entfernen. Es war schon immer ein Problem, besonders unter Windows, wo die meisten Leute es nicht standardmäßig installiert haben.
Grafikstatusverwaltung
Nachdem der Parser erheblich beschleunigt worden war, tauchte ein neues nerviges Detail auf: Zu diesem Zeitpunkt wurden ungefähr 10% der Einrichtungszeit für die Funktionen
pbrtAttributeBegin()
und
pbrtAttributeEnd()
, und der größte Teil dieser Zeit wurde zugewiesen und dynamischer Speicher freigegeben. Während des ersten Laufs, der 35 Minuten dauerte, dauerten diese Funktionen nur etwa 3% der Ausführungszeit, sodass sie ignoriert werden konnten. Bei der Optimierung ist dies jedoch immer so: Wenn Sie große Probleme beseitigen, werden kleine Probleme wichtiger.
Die Beschreibung der pbrt-Szene basiert auf dem hierarchischen Status der Grafik, der die aktuelle Transformation, das aktuelle Material usw. angibt. Darin können Sie Snapshots des aktuellen Status (
pbrtAttributeBegin()
)
pbrtAttributeBegin()
, Änderungen daran vornehmen, bevor Sie der Szene eine neue Geometrie hinzufügen, und dann zum ursprünglichen Status zurückkehren (
pbrtAttributeEnd()
).
Der Grafikstatus wird in einer Struktur mit einem unerwarteten Namen gespeichert ...
GraphicsState
. Zum Speichern von Kopien von
GraphicsState
Objekten im Stapel gespeicherter Grafikzustände wird
std::vector
. Wenn wir uns die
GraphicsState
Mitglieder ansehen, können wir die Ursache der Probleme annehmen - drei
std::map
, von Namen bis zu Instanzen von Texturen und Materialien:
struct GraphicsState {
Bei der Untersuchung dieser Szenendateien stellte ich fest, dass die meisten Fälle des Speicherns und Wiederherstellens des Grafikstatus in den folgenden Zeilen ausgeführt werden:
AttributeBegin ConcatTransform [0.981262 0.133695 -0.138749 0.000000 -0.067901 0.913846 0.400343 0.000000 0.180319 -0.383420 0.905800 0.000000 11.095301 18.852249 9.481399 1.000000] ObjectInstance "archivebaycedar0001_mod" AttributeEnd
Mit anderen Worten, es aktualisiert die aktuelle Transformation und instanziiert das Objekt.
std::map
Inhalt dieser
std::map
keine Änderungen vorgenommen. Das Erstellen einer vollständigen Kopie davon - Zuweisen von rot-schwarzen Baumknoten, Erhöhen der Referenzanzahl für allgemeine Zeiger, Zuweisen von Speicherplatz und Kopieren von Zeichenfolgen - ist fast immer Zeitverschwendung. All dies wird freigegeben, wenn der vorherige Status der Grafik wiederhergestellt wird.
Ich habe jede dieser Karten durch den Zeiger
std::shared_ptr
um sie zuzuordnen, und den Copy-on-Write-Ansatz implementiert, bei dem das Kopieren innerhalb des Anfangs- / Endblocks eines Attributs nur erfolgt, wenn dessen Inhalt geändert werden muss.
Die Änderung war nicht besonders schwierig, reduzierte jedoch die Startzeit um mehr als eine Minute, was uns 12 Minuten und 20 Sekunden Verarbeitung vor dem Beginn des Renderns ermöglichte - wiederum eine Beschleunigung von 1,08.
Was ist mit der Renderzeit?
Ein aufmerksamer Leser wird feststellen, dass ich bisher nichts über die Renderzeit gesagt habe. Zu meiner Überraschung stellte sich heraus, dass es selbst nach dem Auspacken ziemlich erträglich war: pbrt kann Bilder von Szenen in Filmqualität mit mehreren hundert Samples pro Pixel auf zwölf Prozessorkernen für einen Zeitraum von zwei bis drei Stunden rendern. Zum Beispiel wird dieses Bild, eines der langsamsten, in 2 Stunden 51 Minuten 36 Sekunden gerendert:
Von pbrt-v3 gerenderte Dünen aus Moana mit einer Auflösung von 2048 x 858 bei 256 Abtastungen pro Pixel. Die gesamte Renderzeit auf einer Google Compute Engine-Instanz mit 12 Kernen / 24 Threads mit einer Frequenz von 2 GHz und der neuesten Version von pbrt-v3 betrug 2 Stunden 51 Minuten 36 Sekunden.Meiner Meinung nach scheint dies ein überraschend vernünftiger Indikator zu sein. Ich bin sicher, dass Verbesserungen noch möglich sind, und eine sorgfältige Untersuchung der Orte, an denen die meiste Zeit verbracht wird, wird viele „interessante“ Dinge aufdecken, aber bisher gibt es keine besonderen Gründe dafür.
Bei der Profilerstellung stellte sich heraus, dass ungefähr 60% der Renderzeit am Schnittpunkt von Strahlen mit Objekten verbracht wurden (die meisten Operationen wurden unter Umgehung von BVH durchgeführt), und 25% wurden für die Suche nach ptex-Texturen aufgewendet. Diese Verhältnisse ähneln Indikatoren für einfachere Szenen, so dass hier auf den ersten Blick nichts offensichtlich problematisch ist. (Ich bin jedoch sicher, dass Embree diese Strahlen in etwas kürzerer Zeit verfolgen kann.)
Leider ist die parallele Skalierbarkeit nicht so gut. Normalerweise werden 1400% der CPU-Ressourcen für das Rendern ausgegeben, verglichen mit dem Ideal von 2400% (für 24 virtuelle CPUs in der Google Compute Engine). Es scheint, dass das Problem mit Konflikten während Sperren in ptex zusammenhängt, aber ich habe es noch nicht genauer untersucht. Es ist sehr wahrscheinlich, dass pbrt-v3 die Strahlendifferenz für indirekte Strahlen im Raytracer nicht berechnet. Im Gegenzug erhalten solche Strahlen immer Zugriff auf die detaillierteste MIP-Ebene von Texturen, was für das Zwischenspeichern von Texturen nicht sehr nützlich ist.
Schlussfolgerung (für pbrt-v3)
Nachdem ich die Verwaltung des Grafikstatus korrigiert hatte, stieß ich auf eine Grenze, nach der weitere Fortschritte, ohne wesentliche Änderungen am System vorzunehmen, nicht mehr offensichtlich waren. Der Rest nahm viel Zeit in Anspruch und hatte wenig mit Optimierung zu tun. Daher werde ich zumindest in Bezug auf pbrt-v3 darauf eingehen.
Im Allgemeinen war der Fortschritt ernst: Die Startzeit vor dem Rendern verringerte sich von 35 Minuten auf 12 Minuten und 20 Sekunden, dh die Gesamtbeschleunigung betrug das 2,83-fache. Dank der cleveren Arbeit mit dem Konvertierungscache ist die Speichernutzung von 80 GB auf 69 GB gesunken. Alle diese Änderungen sind jetzt verfügbar, wenn Sie mit der neuesten Version von pbrt-v3 synchronisieren (oder wenn Sie dies in den letzten Monaten getan haben). Und wir werden verstehen, wie viel Müll der
Primitive
Speicher für diese Szene ist. Wir haben herausgefunden, wie weitere 18 GB Speicherplatz gespart werden können, haben ihn jedoch nicht in pbrt-v3 implementiert.
Nach all unseren Optimierungen werden diese 12 Minuten und 20 Sekunden für Folgendes verwendet:
Funktion / Bedienung | Prozentsatz der Laufzeit |
---|
BVH bauen | 34% |
Parsing (außer strtof() ) | 21% |
strtof() | 20% |
Konvertierungscache | 7% |
PLY-Dateien lesen | 6% |
Dynamische Speicherzuordnung | 5% |
Konvertierungsinversion | 2% |
Grafikstatusverwaltung | 2% |
Andere | 3% |
In Zukunft wird die beste Option zur Verbesserung der Leistung ein noch größeres Multithreading der Startphase sein: Fast alles während des Parsens der Szene ist Single-Threaded; Unser natürlichstes erstes Ziel ist der Bau eines BVH. Es wird auch interessant sein, Dinge wie das Lesen von PLY-Dateien und das Generieren von BVH für einzelne Instanzen von Objekten zu analysieren und diese asynchron im Hintergrund auszuführen, während das Parsen im Hauptthread durchgeführt wird.
Irgendwann werde ich sehen, ob es schnellere Implementierungen von
strtof()
; pbrt verwendet nur das, was das System bietet. Sie sollten jedoch bei der Auswahl von Ersatzteilen, die nicht sehr gründlich getestet wurden, vorsichtig sein: Das Parsen von Float-Werten ist einer der Aspekte, bei denen sich der Programmierer absolut sicher sein muss.
Es sieht auch attraktiv aus, die Belastung des Parsers weiter zu reduzieren: Wir haben noch 17 GB Texteingabedateien zum Parsen. Wir können die Unterstützung für die binäre Codierung für pbrt-Eingabedateien hinzufügen (möglicherweise ähnlich
dem RenderMan-Ansatz ), aber ich habe gemischte Gefühle in
Bezug auf diese Idee. Die Möglichkeit, Szenenbeschreibungsdateien in einem Texteditor zu öffnen und zu ändern, ist sehr nützlich, und ich mache mir Sorgen, dass die binäre Codierung manchmal die Schüler verwirrt, die pbrt im Lernprozess verwenden. Dies ist einer der Fälle, in denen sich die richtige Lösung für pbrt von Lösungen für einen kommerziellen Render eines Produktionsniveaus unterscheiden kann.
Es war sehr interessant, all diese Optimierungen im Auge zu behalten und verschiedene Lösungen besser zu verstehen. Es stellte sich heraus, dass pbrt unerwartete Annahmen hat, die die Szene dieser Komplexitätsstufe stören. All dies ist ein großartiges Beispiel dafür, wie wichtig es für eine breite Community von Rendering-Forschern ist, Zugang zu realen Produktionsszenen mit einem hohen Grad an Komplexität zu haben. Ich bedanke mich nochmals bei Disney für die Zeit, die für die Bearbeitung und Veröffentlichung dieser Szene aufgewendet wurde.
Im
nächsten Artikel werden wir uns mit Aspekten befassen, die die Leistung weiter verbessern können, wenn wir pbrt erlauben, radikalere Änderungen vorzunehmen.
Hinweis
- Auf dem Linux-System, auf dem ich getestet habe, ist
strtof()
nicht schneller als strtod()
. Es ist bemerkenswert, dass strtod()
OS X etwa zweimal schneller ist, was völlig unlogisch ist. Aus praktischen Gründen habe ich weiterhin strtof()
.