Ich habe mich deshalb gefragt, wie wichtig Multithreading im aktuellen Branchenszenario ist.
In leistungskritischen Bereichen, in denen die Leistung nicht von Drittanbieter-Code stammt, sondern von unserem eigenen Code, würde ich Dinge aus der CPU-Perspektive eher in dieser Reihenfolge berücksichtigen (die GPU ist ein Platzhalter, den ich gewonnen habe) geht nicht rein):
- Speichereffizienz (zB Referenzort).
- Algorithmisch
- Multithreading
- SIMD
- Sonstige Optimierungen (statische Verzweigungsvorhersage-Hinweise, zB)
Beachten Sie, dass diese Liste nicht nur von der Wichtigkeit abhängt, sondern auch von vielen anderen Faktoren wie den Auswirkungen auf die Wartung, wie einfach sie sind (wenn nicht, sollten Sie dies im Voraus berücksichtigen), ihren Interaktionen mit anderen auf der Liste usw.
Speichereffizienz
Die meisten sind vielleicht überrascht, dass ich die Speichereffizienz gegenüber der algorithmischen Wahl gewählt habe. Dies liegt daran, dass die Speichereffizienz mit allen vier anderen Elementen in dieser Liste interagiert und die Berücksichtigung häufig eher in der Kategorie "Design" als in der Kategorie "Implementierung" erfolgt. Zugegebenermaßen gibt es hier ein kleines Problem mit Hühnern oder Eiern, da für das Verständnis der Speichereffizienz häufig alle 4 Elemente auf der Liste berücksichtigt werden müssen, während für alle 4 anderen Elemente auch die Speichereffizienz berücksichtigt werden muss. Dennoch ist es das Herzstück von allem.
Wenn wir beispielsweise eine Datenstruktur benötigen, die sequentiellen Zugriff in linearer Zeit und zeitlich konstante Einfügungen nach hinten und nichts anderes für kleine Elemente bietet, wäre die naive Wahl, nach der wir hier greifen, eine verknüpfte Liste. Das ignoriert die Speichereffizienz. Wenn wir die Speichereffizienz in der Mischung berücksichtigen, wählen wir in diesem Szenario am Ende zusammenhängendere Strukturen aus, wie z. B. anwachsende Array-basierte Strukturen oder zusammenhängende Knoten (z. B. einer, der 128 Elemente in einem Knoten speichert), die miteinander verbunden sind, oder zumindest eine verknüpfte Liste, die von einem Pool-Allokator unterstützt wird. Diese haben trotz der gleichen algorithmischen Komplexität einen dramatischen Vorteil. Ebenso wählen wir häufig die schnelle Sortierung eines Arrays anstelle der Sortierung nach Zusammenführung, obwohl die algorithmische Komplexität aufgrund der Speichereffizienz minderwertig ist.
Ebenso können wir kein effizientes Multithreading haben, wenn unsere Speicherzugriffsmuster so detailliert und verstreut sind, dass wir das Ausmaß der falschen Freigabe maximieren und gleichzeitig auf den detailliertesten Codeebenen sperren. Die Speichereffizienz multipliziert also die Effizienz von Multithreading. Dies ist eine Grundvoraussetzung, um das Beste aus den Threads herauszuholen.
Jedes einzelne Element in der Liste hat eine komplexe Interaktion mit Daten, und die Konzentration auf die Darstellung von Daten trägt letztendlich zur Speichereffizienz bei. Jedes einzelne dieser oben genannten Elemente kann mit einer unangemessenen Art der Darstellung oder des Zugriffs auf Daten in Konflikt geraten.
Ein weiterer Grund, warum die Speichereffizienz so wichtig ist, besteht darin, dass sie auf die gesamte Codebasis angewendet werden kann . Im Allgemeinen ist es ein Zeichen dafür, dass man sich einen Profiler zulegen muss, wenn man sich vorstellt, dass sich Ineffizienzen durch kleine Teile der Arbeit hier und da ansammeln. Felder mit geringer Latenz oder solche, die sich mit sehr begrenzter Hardware befassen, werden auch nach der Profilerstellung tatsächlich Sitzungen finden, die keine eindeutigen Hotspots (nur zeitweise über den gesamten Bereich verteilt) in einer Codebasis anzeigen, die bei der Zuweisung, beim Kopieren und bei der Erstellung von Profilen eklatant ineffizient ist Zugriff auf Speicher. In der Regel ist dies das einzige Mal, dass eine gesamte Codebasis Leistungsproblemen ausgesetzt ist, die zu einer Reihe neuer Standards führen können, die in der gesamten Codebasis angewendet werden, und die Speichereffizienz steht häufig im Mittelpunkt.
Algorithmisch
Dies ist so ziemlich selbstverständlich, da die Auswahl in einem Sortieralgorithmus den Unterschied zwischen einer massiven Eingabe, deren Sortierung Monate in Anspruch nimmt, und Sekunden in Anspruch nimmt. Es macht den größten Einfluss von allen, wenn die Wahl zwischen beispielsweise wirklich unterdurchschnittlichen quadratischen oder kubischen Algorithmen und einem linearen oder zwischen linearen und logarithmischen oder konstanten Algorithmus besteht, zumindest bis wir 1.000.000 Kernmaschinen haben (in diesem Fall Speicher) Effizienz würde noch wichtiger werden).
Es steht jedoch nicht ganz oben auf meiner persönlichen Liste, da jeder, der auf seinem Gebiet kompetent ist, wissen würde, wie man eine Beschleunigungsstruktur für das Abtöten von Kegelstümpfen verwendet Ein Radix-Baum für Präfix-basierte Suchanfragen ist Babymaterial. Ohne diese Grundkenntnisse in dem Bereich, in dem wir arbeiten, würde die algorithmische Effizienz sicherlich an die Spitze rücken, aber die algorithmische Effizienz ist oftmals trivial.
Auch das Erfinden neuer Algorithmen kann in einigen Bereichen eine Notwendigkeit sein (z. B. bei der Netzverarbeitung musste ich Hunderte erfinden, da sie entweder vorher nicht existierten oder die Implementierung ähnlicher Funktionen in anderen Produkten proprietäre Geheimnisse waren, die nicht in einem Papier veröffentlicht wurden ). Sobald wir jedoch den Teil zur Problemlösung hinter uns haben und einen Weg finden, die richtigen Ergebnisse zu erzielen, und wenn Effizienz zum Ziel wird, können wir nur noch darüber nachdenken, wie wir mit Daten (Speicher) interagieren. Ohne die Speichereffizienz zu verstehen, kann der neue Algorithmus mit vergeblichen Anstrengungen, ihn zu beschleunigen, unnötig komplex werden, wenn nur die Speichereffizienz berücksichtigt werden muss, um einen einfacheren und eleganteren Algorithmus zu erhalten.
Schließlich liegen Algorithmen eher in der Kategorie "Implementierung" als in der Kategorie "Speichereffizienz". Sie sind im Nachhinein oft einfacher zu verbessern, selbst wenn zunächst ein suboptimaler Algorithmus verwendet wird. Beispielsweise wird ein schlechterer Bildverarbeitungsalgorithmus oft nur an einer lokalen Stelle in der Codebasis implementiert. Es kann später gegen ein besseres ausgetauscht werden. Wenn jedoch alle Bildverarbeitungsalgorithmen an eine Pixel
Schnittstelle gebunden sind, die eine suboptimale Speicherdarstellung aufweist, aber die einzige Möglichkeit zur Korrektur darin besteht, die Darstellung mehrerer Pixel (und nicht eines einzigen) zu ändern, sind wir häufig SOL und müssen die Codebasis in Richtung eines vollständig umschreibenImage
Schnittstelle. Das Gleiche gilt für das Ersetzen eines Sortieralgorithmus - normalerweise handelt es sich um ein Implementierungsdetail, während eine vollständige Änderung der zugrunde liegenden Darstellung der zu sortierenden Daten oder der Art und Weise, wie sie durch Nachrichten geleitet werden, möglicherweise eine Neugestaltung der Schnittstellen erforderlich macht.
Multithreading
Multithreading ist eine schwierige Aufgabe im Hinblick auf die Leistung, da es sich um eine Optimierung auf Mikroebene handelt, die auf Hardwareeigenschaften abzielt, aber unsere Hardware skaliert tatsächlich in diese Richtung. Bereits habe ich Gleichaltrige, die 32 Kerne haben (ich habe nur 4).
Mulithreading gehört jedoch zu den gefährlichsten Mikrooptimierungen, die einem Fachmann wahrscheinlich bekannt sind, wenn der Zweck darin besteht, Software zu beschleunigen. Die Racebedingung ist so ziemlich der tödlichste Fehler, der möglich ist, da sie so unbestimmt ist (möglicherweise wird sie nur einmal alle paar Monate auf dem Computer eines Entwicklers zu einem äußerst ungünstigen Zeitpunkt außerhalb eines Debugging-Kontexts angezeigt, wenn überhaupt). Es hat also die wohl negativste Beeinträchtigung der Wartbarkeit und potenziellen Korrektheit von Code unter all diesen, zumal Fehler im Zusammenhang mit Multithreading selbst bei sorgfältigsten Tests leicht zu übersehen sind.
Trotzdem wird es so wichtig. Auch wenn es angesichts der Anzahl der Kerne, die wir jetzt haben, nicht immer besser ist als die Speichereffizienz (die manchmal die Dinge hundertmal schneller macht), sehen wir immer mehr Kerne. Selbst bei Computern mit 100 Kernen würde ich die Speichereffizienz immer noch ganz oben auf die Liste setzen, da die Thread-Effizienz ohne sie im Allgemeinen nicht möglich ist. Ein Programm kann auf einer solchen Maschine hundert Threads verwenden und trotzdem langsam sein, ohne effiziente Speicherdarstellung und Zugriffsmuster (die mit Sperrmustern verknüpft sind).
SIMD
SIMD ist auch ein bisschen umständlich, da die Register tatsächlich breiter werden, mit Plänen, noch breiter zu werden. Ursprünglich sahen wir 64-Bit-MMX-Register, gefolgt von 128-Bit-XMM-Registern, die 4 SPFP-Operationen parallel ausführen können. Jetzt sehen wir 256-Bit-YMM-Register, die 8 parallel ausführen können. Und es gibt bereits Pläne für 512-Bit-Register, die 16 parallel zulassen würden.
Diese würden mit der Effizienz von Multithreading interagieren und sich vervielfachen. SIMD kann jedoch die Wartbarkeit genauso beeinträchtigen wie Multithreading. Auch wenn Fehler, die mit ihnen zusammenhängen, nicht unbedingt so schwer zu reproduzieren und zu beheben sind wie ein Deadlock oder eine Race-Bedingung, ist die Portabilität umständlich und es muss sichergestellt werden, dass der Code auf jedem Computer ausgeführt werden kann (und die entsprechenden Anweisungen basierend auf den Hardware-Funktionen verwendet werden) peinlich.
Eine andere Sache ist, dass Compiler heutzutage normalerweise keinen fachmännisch geschriebenen SIMD-Code schlagen, aber naive Versuche leicht bestehen. Sie werden möglicherweise so weit verbessert, dass wir sie nicht mehr manuell ausführen müssen oder zumindest nicht mehr so manuell, dass sie inhärenten Code oder reinen Assembler-Code schreiben (vielleicht nur eine kleine menschliche Anleitung).
Auch hier ist SIMD ohne ein für die vektorisierte Verarbeitung effizientes Speicherlayout nutzlos. Am Ende wird nur ein Skalarfeld in ein breites Register geladen, um nur eine Operation auszuführen. Das Herzstück all dieser Elemente ist die Abhängigkeit von Speicherlayouts, um wirklich effizient zu sein.
Andere Optimierungen
Ich würde oft vorschlagen, dass wir heutzutage von "Mikro" sprechen, wenn das Wort andeutet, dass nicht nur der algorithmische Fokus überschritten wird, sondern auch Änderungen, die sich nur geringfügig auf die Leistung auswirken.
Häufig erfordert der Versuch, die Verzweigungsvorhersage zu optimieren, eine Änderung des Algorithmus oder der Speichereffizienz. Wenn dies z. B. nur durch Hinweise versucht wird und der Code für die statische Vorhersage neu angeordnet wird, wird die erstmalige Ausführung dieses Codes tendenziell nur verbessert, was die Auswirkungen in Frage stellt, wenn nicht oft völlig zu vernachlässigen.
Zurück zu Multithreading für Leistung
Wie wichtig ist Multithreading in einem Leistungskontext? Auf meinem 4-Core-Rechner können die Dinge idealerweise etwa fünfmal schneller gemacht werden (was ich mit Hyperthreading erreichen kann). Für meinen Kollegen mit 32 Kernen wäre das wesentlich wichtiger. Und es wird in den kommenden Jahren immer wichtiger.
Also ist es ziemlich wichtig. Es ist jedoch sinnlos, nur eine Reihe von Threads auf das Problem zu werfen, wenn die Speichereffizienz nicht ausreicht, um die sparsame Verwendung von Sperren zu ermöglichen, falsche Freigabe zu reduzieren usw.
Multithreading außerhalb der Leistung
Beim Multithreading geht es nicht immer nur um reine Leistung im Sinne eines einfachen Durchsatzes. Manchmal wird es verwendet, um eine Last sogar auf die möglichen Kosten des Durchsatzes auszugleichen, um die Reaktionsfähigkeit des Benutzers zu verbessern, oder um es dem Benutzer zu ermöglichen, mehr Multitasking auszuführen, ohne darauf zu warten, dass die Dinge abgeschlossen sind (z. B. Fortsetzen des Surfens beim Herunterladen einer Datei).
In diesen Fällen würde ich vorschlagen, dass Multithreading nach oben hin noch weiter ansteigt (möglicherweise sogar über die Speichereffizienz hinaus), da es dann eher um benutzerorientiertes Design geht, als darum, das Beste aus der Hardware herauszuholen. Es wird häufig das Interface-Design und die Art und Weise dominieren, wie wir unsere gesamte Codebasis in solchen Szenarien strukturieren.
Wenn wir nicht einfach eine enge Schleife parallelisieren, die auf eine massive Datenstruktur zugreift, wird Multithreading in die Kategorie "Design" eingestuft, und Design ist immer wichtiger als die Implementierung.
In diesen Fällen würde ich sagen, dass Multithreading im Vorfeld von entscheidender Bedeutung ist, noch mehr als die Speicherdarstellung und der Speicherzugriff.