Wie kann man rechenintensiven Code dokumentieren und lehren, der bis zur Unkenntlichkeit optimiert ist?

11

Gelegentlich gibt es 1% des Codes, der rechenintensiv genug ist und die schwerste Art der Optimierung auf niedriger Ebene erfordert. Beispiele sind Videoverarbeitung, Bildverarbeitung und alle Arten der Signalverarbeitung im Allgemeinen.

Ziel ist es, die Optimierungstechniken zu dokumentieren und zu vermitteln, damit der Code nicht unbrauchbar wird und von neueren Entwicklern entfernt werden kann. (*)

(*) Ungeachtet der Möglichkeit, dass die bestimmte Optimierung in einigen unvorhersehbaren zukünftigen CPUs völlig nutzlos ist, so dass der Code trotzdem gelöscht wird.

In Anbetracht der Tatsache, dass Softwareangebote (kommerziell oder Open Source) ihren Wettbewerbsvorteil behalten, indem sie über den schnellsten Code verfügen und die neueste CPU-Architektur verwenden, müssen Softwareentwickler ihren Code häufig optimieren, damit er schneller ausgeführt wird und für eine bestimmte Ausgabe dieselbe Ausgabe erzielt wird Aufgabe, Whlist, die eine kleine Anzahl von Rundungsfehlern toleriert.

In der Regel kann ein Softwareentwickler viele Versionen einer Funktion als Dokumentation jeder durchgeführten Optimierung / Algorithmusumschreibung aufbewahren. Wie stellt man diese Versionen anderen zur Verfügung, um ihre Optimierungstechniken zu studieren?

Verbunden:

rwong
quelle
1
Sie könnten einfach die verschiedenen Versionen im Code behalten, kommentiert, mit vielen Kommentaren, die dem Leser sagen, was los ist.
Mike Dunlavey
1
Und sagen Sie ihnen nicht nur, was der Code tut, sondern warum er so schneller ist. Fügen Sie bei Bedarf Links zu Algorithmen hinzu, entweder Ihre eigenen, wikiähnlichen Dokumente oder Ressourcen, die im Internet verfügbar sind (beachten Sie in diesem Fall nur Link-Rot. Es kann ratsam sein, diese mit einem Link zum Original in Ihr eigenes Dokumentensystem zu kopieren .)
Marjan Venema
1
@ MikeDunlavey: Autsch, bitte kommentiere es nicht aus. Haben Sie einfach mehrere Implementierungen derselben Funktion und rufen Sie die schnellste auf. Auf diese Weise können Sie einfach zu einer anderen Version des Codes wechseln und alle vergleichen.
Sleske
2
@sleske Manchmal kann es langsamer werden, wenn nur mehr Binärcode vorhanden ist.
quant_dev
@quant_dev: Ja, das kann passieren. Ich denke nur, dass es wichtig ist, dass der Code (idealerweise) regelmäßig erstellt und ausgeführt wird, um ihn auf dem neuesten Stand zu halten. Vielleicht nur im Debug-Modus erstellen.
Sleske

Antworten:

10

Kurze Antwort

Halten Sie Optimierungen lokal, machen Sie sie offensichtlich, dokumentieren Sie sie gut und erleichtern Sie den Vergleich der optimierten Versionen untereinander und mit der nicht optimierten Version, sowohl hinsichtlich des Quellcodes als auch der Laufzeitleistung.

Vollständige Antwort

Wenn solche Optimierungen für Ihr Produkt wirklich so wichtig sind , müssen Sie nicht nur wissen, warum die Optimierungen zuvor nützlich waren, sondern auch genügend Informationen bereitstellen, damit Entwickler wissen, ob sie in Zukunft nützlich sein werden.

Im Idealfall müssen Sie Leistungstests in Ihrem Erstellungsprozess verankern, damit Sie herausfinden, wann neue Technologien alte Optimierungen ungültig machen.

Merken:

Die erste Regel der Programmoptimierung: Tun Sie es nicht.

Die zweite Regel der Programmoptimierung (nur für Experten!): Tun Sie es noch nicht. "

- Michael A. Jackson

Um zu wissen, ob jetzt die Zeit gekommen ist, müssen Benchmarking und Tests durchgeführt werden.

Wie Sie bereits erwähnt haben, besteht das größte Problem bei hochoptimiertem Code darin, dass die Wartung schwierig ist. Daher müssen Sie die optimierten Teile so weit wie möglich von den nicht optimierten Teilen trennen. Ob Sie dies durch Verknüpfung zur Kompilierungszeit, virtuelle Laufzeitfunktionsaufrufe oder etwas dazwischen tun, sollte keine Rolle spielen. Was wichtig sein sollte, ist, dass Sie beim Ausführen Ihrer Tests in der Lage sein möchten, alle Versionen zu testen, an denen Sie derzeit interessiert sind.

Ich würde gerne ein System so erstellen, dass die nicht optimierte Basisversion des Produktionscodes immer verwendet werden kann, um die Absicht des Codes zu verstehen , und dann verschiedene optimierte Module daneben erstellen, die die optimierte Version oder die optimierten Versionen enthalten und überall explizit dokumentieren Die optimierte Version unterscheidet sich von der Basislinie. Wenn Sie Ihre Tests (Einheit und Integration) ausführen, führen Sie sie auf der nicht optimierten Version und auf allen aktuell optimierten Modulen aus.

Beispiel

Nehmen wir zum Beispiel an, Sie haben eine Fast Fourier Transform- Funktion. Vielleicht haben Sie eine grundlegende, algorithmische Implementierung in fft.cund Tests in fft_tests.c.

Dann kommt der Pentium und Sie entscheiden sich, die Festkomma-Version fft_mmx.cmithilfe von MMX-Anweisungen zu implementieren . Später kommt das Pentium 3 und Sie beschließen, eine Version hinzuzufügen, die Streaming SIMD Extensions in verwendet fft_sse.c.

Jetzt möchten Sie CUDA hinzufügen , also fügen Sie hinzu fft_cuda.c, stellen jedoch fest, dass mit dem Testdatensatz, den Sie seit Jahren verwenden, die CUDA-Version langsamer ist als die SSE-Version! Sie führen einige Analysen durch und fügen am Ende einen Datensatz hinzu, der 100-mal größer ist, und Sie erhalten die erwartete Beschleunigung. Jetzt wissen Sie jedoch, dass die Einrichtungszeit für die Verwendung der CUDA-Version erheblich ist und dass Sie bei kleinen Datensätzen einen verwenden sollten Algorithmus ohne diese Einrichtungskosten.

In jedem dieser Fälle, in denen Sie denselben Algorithmus implementieren, sollten sich alle gleich verhalten, jedoch auf verschiedenen Architekturen mit unterschiedlicher Effizienz und Geschwindigkeit ausgeführt werden (sofern sie überhaupt ausgeführt werden). Unter dem Gesichtspunkt des Codes können Sie jedoch jedes Paar von Quelldateien vergleichen, um herauszufinden, warum dieselbe Schnittstelle auf unterschiedliche Weise implementiert ist. In der Regel ist es am einfachsten, auf die ursprüngliche, nicht optimierte Version zurückzugreifen.

Gleiches gilt für eine OOP-Implementierung, bei der eine Basisklasse, die den nicht optimierten Algorithmus implementiert, und abgeleitete Klassen unterschiedliche Optimierungen implementieren.

Das Wichtigste ist, die gleichen Dinge zu behalten, die gleich sind , damit die Unterschiede offensichtlich sind .

Mark Booth
quelle
7

Insbesondere da Sie das Beispiel der Video- und Bildverarbeitung verwendet haben, kann der Code als Teil derselben Version beibehalten werden, jedoch je nach Kontext aktiv oder inaktiv.

Während Sie nicht erwähnt haben, gehe ich Chier aus.

Der einfachste Weg im CCode, eine Optimierung durchzuführen (und dies gilt auch, wenn versucht wird, Dinge portabel zu machen), besteht darin, sie beizubehalten

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

Wenn Sie #define OPTIMIZATION_XYZ_ENABLEwährend der Kompilierung in Makefile aktivieren , funktioniert alles entsprechend.

Normalerweise kann das Schneiden einiger Codezeilen in der Mitte von Funktionen unübersichtlich werden, wenn zu viele Funktionen optimiert werden. Daher definiert man in diesem Fall verschiedene Funktionszeiger , um eine bestimmte Funktion auszuführen.

Der Hauptcode wird immer über einen Funktionszeiger wie ausgeführt


   codec->computed_idct(blocks); 

Die Funktionszeiger werden jedoch je nach Beispieltyp definiert (z. B. ist hier die idct-Funktion für unterschiedliche CPU-Architekturen optimiert.



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

Sie sollten libjpeg- Code und libmpeg2- Code sehen und können für solche Techniken ffmpeg sein .

Dipan Mehta
quelle
6

Als Forscher schreibe ich am Ende ziemlich viel vom "Engpass" -Code. Sobald es jedoch in Produktion geht, liegt es in der Verantwortung der Entwickler, es in das Produkt zu integrieren und anschließend zu unterstützen. Wie Sie sich vorstellen können, ist es von größter Bedeutung, klar zu kommunizieren, was und wie das Programm funktionieren soll.

Ich habe festgestellt, dass es drei wesentliche Bestandteile gibt, um diesen Schritt erfolgreich abzuschließen

  1. Der verwendete Algorithmus muss absolut klar sein.
  2. Der Zweck jeder Implementierungslinie muss klar sein.
  3. Abweichungen von den erwarteten Ergebnissen müssen so schnell wie möglich festgestellt werden.

Für den ersten Schritt schreibe ich immer ein kurzes Whitepaper , das den Algorithmus dokumentiert. Ziel ist es, es tatsächlich aufzuschreiben, damit eine andere Person es nur mit dem Whitepaper von Grund auf neu implementieren kann. Wenn es sich um einen bekannten, veröffentlichten Algorithmus handelt, reicht es aus, die Referenzen anzugeben und die Schlüsselgleichungen zu wiederholen. Wenn es sich um eine Originalarbeit handelt, müssen Sie etwas expliziter sein. Hier erfahren Sie, was der Code tun soll .

Die tatsächliche Implementierung, die an die Entwicklung übergeben wird, muss so dokumentiert werden, dass alle Feinheiten explizit dargestellt werden. Wenn Sie Sperren in einer bestimmten Reihenfolge erwerben, um einen Deadlock zu vermeiden, fügen Sie einen Kommentar hinzu. Wenn Sie aufgrund von Cache-Kohärenzproblemen über die Spalten anstatt über die Zeilen einer Matrix iterieren, fügen Sie einen Kommentar hinzu. Wenn Sie etwas tun, das auch nur ein bisschen klug ist, kommentieren Sie es. Wenn Sie garantieren können, dass das Whitepaper und der Code niemals getrennt werden (über VCS oder ein ähnliches System), können Sie auf das Whitepaper zurückgreifen. Das Ergebnis kann leicht über 50% Kommentar sein. Das ist in Ordnung. Hier erfahren Sie, warum der Code das tut, was er tut.

Schließlich müssen Sie in der Lage sein, die Richtigkeit angesichts von Änderungen zu gewährleisten. Glücklicherweise sind wir ein praktisches Tool für automatisierte Test- und kontinuierliche Integrationsplattformen . Diese zeigen Ihnen, was der Code tatsächlich tut .

Meine herzlichste Empfehlung wäre, auf keinen der Schritte zu verzichten. Du wirst sie später brauchen;)

drxzcl
quelle
Vielen Dank für Ihre umfassende Antwort. Ich stimme all Ihren Punkten zu. In Bezug auf automatisierte Tests finde ich, dass es schwierig ist, den numerischen Bereich von Festkomma-Arithmetik und SIMD-Code angemessen abzudecken, was ich zweimal verbrannt habe. Voraussetzungen, die nur in den Kommentaren angegeben wurden (ohne zu verstärkenden Code), wurden nicht immer erfüllt.
Rwong
Der Grund, warum ich Ihre Antwort noch nicht akzeptiert habe, ist, dass ich mehr Anleitung brauche, was "ein kurzes Whitepaper" bedeutet und welche Anstrengungen unternommen werden sollten, um es zu erstellen. Für einige Branchen ist dies Teil des Hauptgeschäftsbereichs, in anderen Branchen müssen jedoch die Kosten berücksichtigt werden, und es sollten gesetzlich verfügbare Abkürzungen verwendet werden.
Rwong
Zuallererst spüre ich Ihren Schmerz in Bezug auf automatisierte Tests, Gleitkomma-Arithmetik und parallelen Code. Ich fürchte, es gibt keine Lösung, die für alle Fälle gültig ist. Normalerweise arbeite ich mit ziemlich liberalen Toleranzen, aber in Ihrer Branche ist das möglicherweise nicht möglich.
drxzcl
2
In der Praxis sieht das Whitepaper oft wie der erste Entwurf eines wissenschaftlichen Papiers aus, ohne die "Fluff" -Teile (keine aussagekräftige Einführung, keine Zusammenfassung, minimale Schlussfolgerungen / Diskussionen und nur die Referenzen, die zum Verständnis erforderlich sind). Ich sehe das Schreiben des Papiers als Bericht und integralen Bestandteil der Algorithmusentwicklung und / oder Algorithmusauswahl. Sie haben diesen Algorithmus implementiert (z. B. spektrale FFT). Was ist es genau? Warum hast du diesen vor den anderen gewählt? Was sind seine Parallelisierungseigenschaften? Der Aufwand sollte in einem angemessenen Verhältnis zur Auswahl- / Entwicklungsarbeit stehen.
drxzcl
5

Ich glaube, dass dies am besten durch ein umfassendes Kommentieren des Codes gelöst werden kann, bis zu dem Punkt, an dem jeder wichtige Codeblock zuvor erklärende Kommentare enthält.

Die Kommentare sollten Zitate zu den Spezifikationen oder zum Hardware-Referenzmaterial enthalten.

Verwenden Sie gegebenenfalls branchenweite Terminologie- und Algorithmusnamen - z. B. "Architektur X generiert CPU-Traps für nicht ausgerichtete Lesevorgänge, sodass dieses Duff-Gerät bis zur nächsten Ausrichtungsgrenze gefüllt wird".

Ich würde die Benennung von Variablen in Ihrem Gesicht verwenden, um sicherzustellen, dass kein Missverständnis darüber besteht, was vor sich geht. Nicht ungarisch, aber Dinge wie "Schritt", um den Abstand in Bytes zwischen zwei vertikalen Pixeln zu beschreiben.

Ich würde dies auch durch ein kurzes, für Menschen lesbares Dokument ergänzen, das übergeordnete Diagramme und Blockdesign enthält.

JBRWilkinson
quelle
1
Die Verwendung einer einheitlichen Terminologie für eine einzelne Sache (z. B. "Schritt" über Begriffe mit ähnlichen Bedeutungen, z. B. "Schritt", "Ausrichtung") im selben Projekt würde helfen. Dies ist etwas schwierig, wenn die Codebasis mehrerer Projekte in ein Projekt integriert wird.
Rwong