Muss Code mit niedriger Latenz manchmal „hässlich“ sein?


21

(Dies richtet sich vor allem an Personen mit spezifischen Kenntnissen über Systeme mit geringer Latenz, um zu vermeiden, dass Personen nur mit unbegründeten Meinungen antworten.)

Glauben Sie, dass es einen Kompromiss zwischen dem Schreiben von "nettem" objektorientiertem Code und dem Schreiben von sehr schnellem Code mit niedriger Latenz gibt? Vermeiden Sie beispielsweise virtuelle Funktionen in C ++ / den Overhead von Polymorphismus usw. Schreiben Sie Code neu, der böse aussieht, aber sehr schnell ist usw.?

Es liegt auf der Hand - wen interessiert es, ob es hässlich aussieht (solange es wartbar ist) - wenn Sie Geschwindigkeit brauchen, brauchen Sie Geschwindigkeit?

Es würde mich interessieren, von Leuten zu hören, die in solchen Bereichen gearbeitet haben.


1
@ user997112: Der genaue Grund ist selbsterklärend. Es heißt: "Wir erwarten, dass die Antworten durch Fakten, Referenzen oder spezifisches Fachwissen gestützt werden, aber diese Frage wird wahrscheinlich Debatten, Argumente, Abstimmungen oder erweiterte Diskussionen hervorrufen . Das bedeutet nicht unbedingt, dass sie korrekt sind, aber das war der Abschluss Grund von allen drei engen Wählern gewählt
Robert Harvey

Anekdotisch würde ich sagen, dass der Grund, warum diese Frage enge Stimmen anzieht, darin besteht, dass sie möglicherweise als dünn verhüllte Beschimpfung wahrgenommen wird (obwohl ich dies nicht glaube).
Robert Harvey

8
Ich werde meinen Hals rausstrecken: Ich habe die dritte Stimme zum Abschluss als "nicht konstruktiv" abgegeben, weil ich denke, dass der Fragesteller seine eigene Frage ziemlich genau beantwortet. "Schöner" Code, der nicht schnell genug für die Ausführung des Auftrags ist, hat die Latenzzeitanforderungen nicht erfüllt. "Hässlicher" Code, der schnell genug läuft, kann durch eine gute Dokumentation besser gewartet werden. Wie Sie Schönheit oder Hässlichkeit messen, ist ein Thema für eine andere Frage.
Blrfl

1
Der Quellcode für Disruptor von LMAX ist nicht zu hässlich. Es gibt einige "zur Hölle mit Javas Sicherheitsmodell" -Teile (unsichere Klasse) und einige hardwarespezifische Modifikationen (Cache-Zeilen-gepolsterte Variablen), aber es ist sehr lesbar, IMO.
James

5
@ Carson63000, user1598390 und wer auch immer interessiert ist: Wenn die Frage geschlossen ist, können Sie sich gerne über die Schließung auf unserer Meta-Site erkundigen. Es macht wenig Sinn, eine Schließung in Kommentaren zu diskutieren, insbesondere eine Schließung, die nicht stattgefunden hat . Denken Sie auch daran, dass jede geschlossene Frage wieder geöffnet werden kann, es ist nicht das Ende der Welt. Außer natürlich, wenn die Mayas Recht hatten. In diesem Fall war es schön, euch alle zu kennen!
Yannis

Antworten:


31

Glauben Sie, dass es einen Kompromiss zwischen dem Schreiben von "nettem" objektorientiertem Code und dem Schreiben von Code mit sehr geringer Latenz gibt?

Ja.

Deshalb gibt es den Ausdruck "vorzeitige Optimierung". Entwickler sollen gezwungen werden, ihre Leistung zu messen und nur den Code zu optimieren, der einen Unterschied in der Leistung bewirkt , und gleichzeitig ihre Anwendungsarchitektur von Anfang an sinnvoll zu gestalten, damit sie nicht unter hoher Last herunterfällt.

Auf diese Weise können Sie Ihren hübschen, gut strukturierten, objektorientierten Code so weit wie möglich beibehalten und nur mit hässlichem Code die kleinen Teile optimieren, die wichtig sind.


15
"Lass es funktionieren, dann mach es schnell". Diese Antwort deckt so ziemlich alles ab, was ich beim Lesen der Frage zu sagen glaubte.
Carson63000

13
Ich werde hinzufügen "Measure, rate nicht"
Martijn Verburg

1
Ich denke, dass es sich lohnt, einige davon in die grundlegende Vermeidung von Arbeit zu stecken, solange dies nicht zu Lasten der Lesbarkeit geht. Wenn Sie die Dinge übersichtlich und lesbar halten und nur die offensichtlichen Dinge tun, die sie tun müssen, gewinnen Sie wie andere Entwickler auf lange Sicht indirekt darüber, wie es funktioniert.
Erik Reppen

1
Bei "vorzeitiger Optimierung" - das gilt auch dann, wenn der optimierte Code genauso "schön" ist wie der nicht optimierte Code. Es geht darum, keine Zeit mit dem Streben nach Geschwindigkeit zu verschwenden oder mit dem, was Sie nicht erreichen müssen. Tatsächlich geht es bei der Optimierung nicht immer um Geschwindigkeit, und möglicherweise gibt es eine unnötige Optimierung für "Schönheit". Ihr Code muss kein großartiges Kunstwerk sein, um lesbar und wartbar zu sein.
Steve314

Ich zweite @ Steve314. Ich bin der Leistungsführer für ein Produkt und finde häufig massiv überkomplizierten Code, dessen Ursprung ich auf eine Art Leistungsoptimierung zurückführen kann. Die Vereinfachung dieses Codes zeigt häufig eine erhebliche Leistungsverbesserung. Ein solches Beispiel wurde zu einer 5-fachen Leistungsverbesserung, als ich es vereinfachte (Nettoreduktion von Tausenden von Codezeilen). Es ist klar, dass sich niemand die Zeit genommen hat, um tatsächlich zu messen, und einfach eine vorzeitige Optimierung dessen durchgeführt hat, was sie für wahrscheinlich langsamen Code hielten .
Brandon

5

Ja, das Beispiel, das ich gebe, ist nicht C ++ gegen Java, sondern Assembly gegen COBOL, wie ich es kenne.

Beide Sprachen sind sehr schnell, aber selbst COBOL enthält beim Kompilieren viel mehr Anweisungen, die in den Befehlssatz eingefügt werden und nicht unbedingt vorhanden sein müssen, anstatt diese Anweisungen selbst in Assembly zu schreiben.

Dieselbe Idee kann direkt auf Ihre Frage angewendet werden, wie man "hässlich aussehenden Code" schreibt, anstatt Vererbung / Polymorphismus in C ++ zu verwenden. Ich glaube, es ist notwendig, hässlich aussehenden Code zu schreiben. Wenn der Endbenutzer Transaktionszeiträume von weniger als einer Sekunde benötigt, ist es unsere Aufgabe als Programmierer, ihnen das zu geben, egal wie es passiert.

Ungeachtet dessen erhöht die großzügige Verwendung von Kommentaren die Funktionalität und Wartbarkeit des Programmierers erheblich, unabhängig davon, wie hässlich der Code ist.


3

Ja, es gibt einen Kompromiss. Damit meine ich, dass Code, der schneller und hässlicher ist, nicht besser benötigt wird - die quantitativen Vorteile von "schnellem Code" müssen gegen die Wartungskomplexität der Codeänderungen abgewogen werden, die erforderlich sind, um diese Geschwindigkeit zu erreichen.

Der Kompromiss ergibt sich aus den Geschäftskosten. Code, der komplexer ist, erfordert erfahrenere Programmierer (und Programmierer mit genaueren Fähigkeiten, z. B. mit CPU-Architektur- und Designkenntnissen), die mehr Zeit benötigen, um den Code zu lesen und zu verstehen und Fehler zu beheben. Die Geschäftskosten für die Entwicklung und Pflege eines solchen Codes könnten im Bereich von 10x - 100x gegenüber normal geschriebenem Code liegen.

Diese Wartungskosten sind in einigen Branchen gerechtfertigt , in denen Kunden bereit sind, für sehr schnelle Software eine sehr hohe Prämie zu zahlen.

Einige Geschwindigkeitsoptimierungen erzielen einen besseren Return-on-Investment (ROI) als andere. Einige Optimierungstechniken können nämlich mit geringerem Einfluss auf die Codewartbarkeit (Beibehaltung der Struktur auf höherer Ebene und der Lesbarkeit auf niedrigerer Ebene) im Vergleich zu normal geschriebenem Code angewendet werden.

Daher sollte ein Geschäftsinhaber:

  • Schauen Sie sich die Kosten und Vorteile an,
  • Machen Sie Messungen und Berechnungen
    • Lassen Sie den Programmierer die Programmgeschwindigkeit messen
    • Lassen Sie den Programmierer die für die Optimierung erforderliche Entwicklungszeit einschätzen
    • Schätzen Sie selbst, wie hoch der Umsatz mit schnellerer Software ist
    • Lassen Sie Software-Architekten oder QS-Manager die Nachteile einer verringerten Intuitivität und Lesbarkeit des Quellcodes qualitativ einschätzen
  • Und priorisieren Sie die schwachen Früchte der Softwareoptimierung.

Diese Kompromisse sind sehr spezifisch für die Umstände.

Diese können ohne die Beteiligung von Managern und Produktverantwortlichen nicht optimal entschieden werden.

Diese sind sehr plattformspezifisch. Beispielsweise haben Desktop- und mobile CPUs unterschiedliche Überlegungen. Server- und Clientanwendungen haben ebenfalls unterschiedliche Überlegungen.


Ja, es stimmt im Allgemeinen, dass schnellerer Code anders aussieht als normal geschriebener Code. Jeder andere Code benötigt mehr Zeit zum Lesen. Ob dies Hässlichkeit impliziert, ist in den Augen des Betrachters.

Die Techniken, mit denen ich etwas Erfahrung habe, sind: (ohne zu versuchen, ein bestimmtes Maß an Fachwissen zu beanspruchen) Kurzvektoroptimierung (SIMD), fein abgestimmte Aufgabenparallelität, Speicherzuweisung und Wiederverwendung von Objekten.

SIMD hat in der Regel schwerwiegende Auswirkungen auf die Lesbarkeit auf niedriger Ebene, obwohl normalerweise keine strukturellen Änderungen auf höherer Ebene erforderlich sind (vorausgesetzt, die API wurde unter Berücksichtigung der Vermeidung von Engpässen entwickelt).

Einige Algorithmen lassen sich leicht in SIMD umwandeln (die peinlich vektorisierbaren). Einige Algorithmen erfordern zur Verwendung von SIMD mehr Neuanordnungen bei der Berechnung. In extremen Fällen wie der Wellenfront-SIMD-Parallelität müssen völlig neue Algorithmen (und patentierbare Implementierungen) geschrieben werden, um die Vorteile nutzen zu können.

Für eine fein abgestimmte Task-Parallelisierung müssen Algorithmen in Datenflussdiagramme umgeordnet und wiederholt eine funktionale (rechnerische) Zerlegung auf den Algorithmus angewendet werden, bis kein weiterer Margenvorteil mehr erzielt werden kann. Dekomponierte Stufen sind typischerweise mit einem Fortsetzungsstil verkettet, einem Konzept, das aus der funktionalen Programmierung entlehnt ist.

Durch funktionale (rechnerische) Zerlegung müssen Algorithmen, die normalerweise in einer linearen und konzeptionell klaren Reihenfolge geschrieben wurden (Codezeilen, die in derselben Reihenfolge wie sie geschrieben wurden, ausführbar sind), in Fragmente zerlegt und in mehrere Funktionen aufgeteilt werden oder Klassen. (Siehe Objektivierung des Algorithmus weiter unten.) Diese Änderung wird andere Programmierer, die mit dem Zerlegungsentwurfsprozess, der zu diesem Code geführt hat, nicht vertraut sind, erheblich behindern.

Um einen solchen Code wartbar zu machen, müssen die Autoren dieses Codes ausführliche Dokumentationen des Algorithmus schreiben - weit über die Art von Code-Kommentaren oder UML-Diagrammen für normal geschriebenen Code hinaus. Dies ähnelt der Art und Weise, wie Forscher ihre wissenschaftlichen Arbeiten verfassen.


Nein, schneller Code muss nicht im Widerspruch zur Objektorientierung stehen.

Anders ausgedrückt ist es möglich, sehr schnelle Software zu implementieren, die immer noch objektorientiert ist. In Richtung des unteren Endes dieser Implementierung (auf der Ebene der Schrauben und Muttern, auf der der Großteil der Berechnung stattfindet) kann der Objektentwurf erheblich von den Entwürfen abweichen, die aus dem objektorientierten Entwurf (OOD) erhalten wurden. Das Design der unteren Ebene ist auf die Objektivierung von Algorithmen ausgerichtet.

Einige Vorteile der objektorientierten Programmierung (OOP), wie beispielsweise die Verkapselung, der Polymorphismus und die Komposition, lassen sich immer noch aus der Objektivierung von Algorithmen auf niedriger Ebene ziehen. Dies ist die Hauptbegründung für die Verwendung von OOP auf dieser Ebene.

Die meisten Vorteile des objektorientierten Designs (OOD) gehen verloren. Am wichtigsten ist, dass das Low-Level-Design nicht intuitiv ist. Ein Programmierkollege kann nicht lernen, mit dem Code der niedrigeren Ebene zu arbeiten, ohne vorher vollständig zu verstehen, wie der Algorithmus transformiert und zerlegt wurde, und dieses Verständnis ist aus dem resultierenden Code nicht erhältlich.


2

Ja, manchmal muss Code "hässlich" sein, damit er in der erforderlichen Zeit funktioniert. Der gesamte Code muss jedoch nicht hässlich sein. Die Leistung sollte zuvor getestet und profiliert werden, um die Teile des Codes zu finden, die "hässlich" sein müssen, und diese Abschnitte sollten mit einem Kommentar versehen werden, damit zukünftige Entwickler wissen, was absichtlich hässlich ist und was nur Faulheit ist. Wenn jemand eine Menge schlecht gestalteten Codes schreibt, der Leistungsgründe geltend macht, lassen Sie ihn dies beweisen.

Geschwindigkeit ist genauso wichtig wie jede andere Anforderung eines Programms. Wenn eine Lenkwaffe falsch korrigiert wird, ist dies gleichbedeutend mit der Bereitstellung der richtigen Korrektur nach dem Aufprall. Die Wartbarkeit ist für den Arbeitscode immer von untergeordneter Bedeutung.


2

Einige der Studien, von denen ich Auszüge gesehen habe, weisen darauf hin, dass sauberer, einfach zu lesender Code oft schneller ist als komplexerer, schwer zu lesender Code. Dies liegt zum Teil an der Art und Weise, wie Optimierer entworfen wurden. Sie sind in der Regel viel besser darin, eine Variable in einem Register zu optimieren, als dies mit einem Zwischenergebnis einer Berechnung zu tun. Lange Sequenzen von Zuweisungen unter Verwendung eines einzelnen Operators, die zum Endergebnis führen, können möglicherweise besser optimiert werden als eine lange komplizierte Gleichung. Neuere Optimierer haben möglicherweise den Unterschied zwischen sauberem und kompliziertem Code verringert, aber ich bezweifle, dass sie ihn beseitigt haben.

Andere Optimierungen wie das Abrollen der Schleife können bei Bedarf auf saubere Weise hinzugefügt werden.

Jede Optimierung, die zur Verbesserung der Leistung hinzugefügt wird, sollte mit einem entsprechenden Kommentar versehen werden. Dies sollte eine Erklärung enthalten, dass es als Optimierung hinzugefügt wurde, vorzugsweise mit Leistungsmessungen vor und nach.

Ich habe festgestellt, dass die 80/20-Regel für den von mir optimierten Code gilt. Als Faustregel optimiere ich nichts, was nicht mindestens 80% der Zeit in Anspruch nimmt. Ich strebe dann eine 10-fache Leistungssteigerung an (und erreiche dies normalerweise auch). Dies verbessert die Leistung um das Vierfache. Die meisten Optimierungen, die ich implementiert habe, haben den Code nicht wesentlich weniger "schön" gemacht. Ihr Kilometerstand kann variieren.


2

Wenn Sie mit hässlich meinen, dass es auf der Ebene, auf der andere Entwickler es wiederverwenden oder verstehen müssen, schwierig zu lesen / zu verstehen ist, dann würde ich sagen, dass eleganter, einfach zu lesender Code Sie letztendlich fast immer einbezieht Leistungsgewinn auf lange Sicht in einer App, die Sie pflegen müssen.

Ansonsten gibt es manchmal genug von einem Leistungsgewinn, um es wert zu sein, ihn in eine schöne Box mit einem Killer-Interface zu stecken, aber meiner Erfahrung nach ist dies ein ziemlich seltenes Dilemma.

Denken Sie dabei an grundlegende Arbeitsvermeidung. Bewahren Sie die arkanen Tricks für den Fall auf, dass sich tatsächlich ein Leistungsproblem ergibt. Und wenn Sie etwas schreiben müssen, das jemand nur verstehen kann, wenn er mit der spezifischen Optimierung vertraut ist, tun Sie, was Sie können, um das Hässliche zumindest aus der Sicht der Wiederverwendung Ihres Codes leicht verständlich zu machen. Code, der nur selten eine schlechte Leistung erbringt, tut dies, weil die Entwickler übermäßig genau darüber nachdachten, was der nächste Typ erben würde. Wenn jedoch häufige Änderungen die einzige Konstante einer App sind (nach meiner Erfahrung die meisten Web-Apps), ist dies starrer / unflexibler Code Schwierig zu modifizieren ist praktisch, in Panik zu versetzen, damit überall in Ihrer Codebasis etwas auftaucht. Sauber und schlank ist auf lange Sicht besser für die Leistung.


Ich möchte zwei Änderungen vorschlagen: (1) Es gibt Orte, an denen Geschwindigkeit erforderlich ist. An diesen Stellen ist es meiner Meinung nach sinnvoller, die Benutzeroberfläche verständlicher zu gestalten, als die Implementierung verständlicher zu gestalten, da letztere möglicherweise sehr viel schwieriger ist. (2) "Code, der nur selten eine miserable Leistung erbringt ...", was ich gerne mit "Eine starke Betonung auf Code-Eleganz und -Einfachheit ist selten die Ursache für eine miserable Leistung. Ersteres ist umso wichtiger, wenn häufige Änderungen vorgenommen werden werden erwartet, ... "
rwong

Die Implementierung war eine schlechte Wortwahl in einer OOPish-Konversation. Ich meinte es in Bezug auf die Benutzerfreundlichkeit und bearbeitete. # 2, ich habe gerade einen Satz hinzugefügt, um festzustellen, dass 2 im Wesentlichen der Punkt ist, den ich ansprach.
Erik Reppen

1

Komplex und hässlich sind nicht dasselbe. Code, der viele Sonderfälle aufweist, so optimiert ist, dass er den letzten Tropfen an Leistung herausholt, und der auf den ersten Blick wie ein Gewirr von Verbindungen und Abhängigkeiten aussieht, kann in der Tat sehr sorgfältig ausgearbeitet und sehr schön sein, wenn Sie ihn erst einmal verstanden haben. In der Tat, wenn die Leistung (gemessen an der Latenz oder etwas anderem) wichtig genug ist, um sehr komplexen Code zu rechtfertigen, muss der Code gut gestaltet sein. Wenn nicht, können Sie nicht sicher sein, dass all diese Komplexität wirklich besser ist als eine einfachere Lösung.

Hässlicher Code ist für mich schlampiger, schlecht durchdachter und / oder unnötig komplizierter Code . Ich glaube nicht, dass Sie eine dieser Funktionen in Code wollen, die funktionieren muss.


1

Glauben Sie, dass es einen Kompromiss zwischen dem Schreiben von "nettem" objektorientiertem Code und dem Schreiben von sehr schnellem Code mit niedriger Latenz gibt? Vermeiden Sie beispielsweise virtuelle Funktionen in C ++ / den Overhead von Polymorphismus usw. Schreiben Sie Code neu, der böse aussieht, aber sehr schnell ist usw.?

Ich arbeite in einem Bereich, der sich ein bisschen mehr auf den Durchsatz als auf die Latenz konzentriert, aber sehr leistungskritisch ist, und ich würde "sorta" sagen .

Ein Problem ist jedoch, dass so viele Menschen völlig falsche Vorstellungen von Leistung haben. Neulinge bekommen oft fast alles falsch und ihr gesamtes konzeptionelles Modell der "Rechenkosten" muss überarbeitet werden, wobei nur die algorithmische Komplexität das Einzige ist, was sie richtig machen können. Fortgeschrittene machen eine Menge falsch. Experten verstehen manche Dinge falsch.

Das Messen mit präzisen Tools, die Metriken wie Cache-Fehler und Verzweigungsfehlervorhersagen liefern können, hält alle Personen mit unterschiedlichem Fachwissen auf diesem Gebiet in Schach.

Messen ist auch das, was darauf hinweist, nicht zu optimieren . Experten verbringen oft weniger Zeit mit der Optimierung als Anfänger, da sie echte gemessene Hotspots optimieren und nicht versuchen, wilde Stiche im Dunkeln zu optimieren, basierend auf der Vermutung, was langsam sein könnte (was in extremer Form zu einer Mikrooptimierung verleiten könnte) etwa jede zweite Zeile in der Codebasis).

Auf Leistung ausgelegt

Abgesehen davon kommt der Schlüssel zum Entwerfen für Leistung vom Entwurfsteil , wie im Schnittstellendesign. Eines der Probleme mit der Unerfahrenheit besteht darin, dass es in der Regel zu einer frühen Verschiebung der absoluten Implementierungsmetriken kommt, z. B. zu den Kosten eines indirekten Funktionsaufrufs in einem verallgemeinerten Kontext, als wären es die Kosten (die vom Standpunkt eines Optimierers aus auf den ersten Blick besser zu verstehen sind) ist eher ein Gesichtspunkt als ein Verzweigungsgesichtspunkt) ein Grund, dies in der gesamten Codebasis zu vermeiden.

Die Kosten sind relativ . Während ein indirekter Funktionsaufruf Kosten verursacht, sind z. B. alle Kosten relativ. Wenn Sie diese Kosten einmal bezahlen, um eine Funktion aufzurufen, die Millionen von Elementen durchläuft, ist die Sorge um diese Kosten, als würden Sie stundenlang um ein paar Cent für den Kauf eines Milliarden-Dollar-Produkts feilschen war ein Cent zu teuer.

Gröberes Schnittstellendesign

Der Interface Design Aspekt der Leistung sucht oft früher auf diese Kosten zu einer gröberen Ebene zu schieben. Anstatt zum Beispiel die Laufzeitabstraktionskosten für ein einzelnes Partikel zu zahlen, können wir diese Kosten auf die Ebene des Partikelsystems / Emitters verschieben und ein Partikel effektiv in ein Implementierungsdetail und / oder einfach in Rohdaten dieser Partikelsammlung rendern.

Das objektorientierte Design muss also nicht mit dem Leistungsentwurf unvereinbar sein (ob Latenz oder Durchsatz), aber es kann Versuchungen in einer Sprache geben, die sich darauf konzentriert, immer winziger werdende granulare Objekte zu modellieren, und das neueste Optimierungsprogramm kann dies nicht Hilfe. Es ist nicht möglich, eine Klasse, die einen einzelnen Punkt darstellt, auf eine Weise zusammenzufassen, die eine effiziente SoA-Darstellung für die Speicherzugriffsmuster der Software ergibt. Eine Sammlung von Punkten mit einem auf der Grobheitsebene modellierten Schnittstellendesign bietet diese Möglichkeit und ermöglicht es, bei Bedarf immer optimalere Lösungen zu finden. Ein solches Design wurde für Massenspeicher * entwickelt.

* Beachten Sie hier den Fokus auf Speicher und nicht auf Daten , da das Arbeiten in leistungskritischen Bereichen über einen längeren Zeitraum die Ansicht über Datentypen und Datenstrukturen und deren Verbindung zum Speicher verändert. Ein binärer Suchbaum geht nicht mehr nur um die logarithmische Komplexität in solchen Fällen, wie möglicherweise disparaten und Cache-unfreundlichen Speicherabschnitten für Baumknoten, es sei denn, dies wird von einem festen Allokator unterstützt. Die Ansicht schließt die algorithmische Komplexität nicht aus, sieht sie jedoch nicht mehr unabhängig von Speicherlayouts. Man fängt auch an, Iterationen der Arbeit als Iterationen des Speicherzugriffs zu betrachten. *

Viele leistungskritische Designs können tatsächlich sehr gut mit der Vorstellung von High-Level-Schnittstellendesigns kompatibel sein, die für den Menschen leicht zu verstehen und zu verwenden sind. Der Unterschied besteht darin, dass es sich bei "High-Level" in diesem Kontext um eine Massenaggregation des Speichers handelt, eine Schnittstelle, die für potenziell große Datensammlungen modelliert ist, und eine Implementierung unter der Haube, die unter Umständen recht Low-Level ist. Eine visuelle Analogie könnte ein Auto sein, das wirklich komfortabel und einfach zu fahren und zu handhaben ist und bei hoher Schallgeschwindigkeit sehr sicher ist. Wenn Sie jedoch die Motorhaube öffnen, sind darin kleine feuerspeiende Dämonen.

Mit einem gröberen Design können effizientere Sperrmuster und die Ausnutzung der Parallelität im Code auf einfachere Weise bereitgestellt werden (Multithreading ist ein umfassendes Thema, das ich hier überspringen werde).

Speicherpool

Ein kritischer Aspekt der Programmierung mit niedriger Latenz wird wahrscheinlich eine sehr explizite Steuerung des Speichers sein, um die Referenzlokalität sowie die allgemeine Geschwindigkeit der Zuweisung und Freigabe des Speichers zu verbessern. In einem benutzerdefinierten Allokator-Pooling-Speicher spiegelt sich die gleiche Art von Design-Denkweise wider, die wir beschrieben haben. Es ist für Bulk konzipiert ; es ist grob ausgelegt. Es reserviert Speicher in großen Blöcken und bündelt den bereits zugewiesenen Speicher in kleinen Blöcken.

Die Idee ist genau die gleiche, kostspielige Dinge (z. B. das Zuweisen eines Speicherabschnitts zu einem Allzweck-Allokator) einer immer gröberen Ebene zuzuweisen. Ein Speicherpool ist für den Umgang mit Massenspeicher konzipiert .

Typ Systeme Speicher trennen

Eine der Schwierigkeiten beim granularen objektorientierten Design in jeder Sprache ist, dass es oft viele kleine benutzerdefinierte Typen und Datenstrukturen einführen möchte. Diese Typen können dann in kleinen Blöcken zugewiesen werden, wenn sie dynamisch zugewiesen werden.

Ein gängiges Beispiel in C ++ wäre, wenn Polymorphismus erforderlich ist und die natürliche Versuchung darin besteht, jede Instanz einer Unterklasse einem Allzweck-Speicherzuweiser zuzuweisen.

Dies führt letztendlich dazu, dass möglicherweise zusammenhängende Speicherlayouts in kleine, kleinteilige Bits und Teile aufgeteilt werden, die über den Adressierungsbereich verstreut sind, was zu mehr Seitenfehlern und Cache-Fehlern führt.

In Bereichen mit der geringsten Latenz, ohne Stottern und mit deterministischer Reaktion können sich Hotspots nicht immer auf einen einzigen Engpass beschränken, in dem sich tatsächlich winzige Ineffizienzen "ansammeln" (etwas, das sich viele Menschen vorstellen) Wenn ein Profiler falsch vorgeht, um sie in Schach zu halten, kann es in latenzbedingten Fällen tatsächlich zu einigen seltenen Fällen kommen, in denen sich geringfügige Ineffizienzen ansammeln. Und viele der häufigsten Gründe für eine solche Anhäufung können folgende sein: die übermäßige Zuweisung von winzigen Erinnerungsstücken überall.

In Sprachen wie Java, kann es hilfreich sein, mehr Anordnungen von einfachen alten Datentypen , wenn möglich , bottlenecky Bereichen (Bereiche verarbeitet in engen Schleifen), wie eine Reihe von verwenden int(aber immer noch hinter einer sperrigen High-Level - Schnittstelle) anstelle von, sagen sie , eines ArrayListvon benutzerdefinierten IntegerObjekten. Dies vermeidet die Speichersegregation, die typischerweise mit letzterer einhergeht. In C ++ müssen wir die Struktur nicht so stark herabsetzen, wenn unsere Speicherzuweisungsmuster effizient sind, da benutzerdefinierte Typen dort und sogar im Kontext eines generischen Containers zusammenhängend zugewiesen werden können.

Speicher wieder zusammenfügen

Eine Lösung besteht darin, einen benutzerdefinierten Allokator für homogene Datentypen und möglicherweise sogar für homogene Datentypen zu finden. Wenn winzige Datentypen und Datenstrukturen auf Bits und Bytes im Speicher abgeflacht werden, erhalten sie einen homogenen Charakter (wenn auch mit einigen unterschiedlichen Ausrichtungsanforderungen). Wenn wir sie nicht aus einer speicherorientierten Sicht betrachten, möchte das Typensystem der Programmiersprachen potenziell zusammenhängende Speicherbereiche in kleine, verstreute Teile aufteilen.

Der Stapel verwendet diesen speicherorientierten Fokus, um dies zu vermeiden und möglicherweise eine gemischte Kombination von benutzerdefinierten Typinstanzen darin zu speichern. Wenn möglich, ist es eine gute Idee, den Stapel stärker zu nutzen, da sich der obere Rand fast immer in einer Cache-Zeile befindet. Wir können jedoch auch Speicherzuordnungen entwerfen, die einige dieser Merkmale ohne ein LIFO-Muster imitieren und Speicher über verschiedene Datentypen hinweg in zusammenhängende Typen zusammenfassen Chunks auch für komplexere Speicherzuordnungen und Freigabemuster.

Moderne Hardware ist so konzipiert, dass sie bei der Verarbeitung zusammenhängender Speicherblöcke (z. B. wiederholter Zugriff auf dieselbe Cache-Zeile, dieselbe Seite) ihren Höchststand erreicht. Das Schlüsselwort dort ist Kontiguität, da dies nur dann von Vorteil ist, wenn Umgebungsdaten von Interesse sind. Der Schlüssel (aber auch die Schwierigkeit) für die Leistung besteht darin, getrennte Speicherbereiche wieder zu zusammenhängenden Blöcken zusammenzufassen, auf die vor der Räumung in ihrer Gesamtheit (alle Umgebungsdaten sind relevant) zugegriffen wird. Das umfangreiche Typensystem mit besonders benutzerdefinierten Typen in Programmiersprachen kann hier das größte Hindernis sein, aber wir können das Problem jederzeit durch einen benutzerdefinierten Zuweiser und / oder umfangreichere Designs lösen.

Hässlich

"Hässlich" ist schwer zu sagen. Es ist eine subjektive Metrik, und jemand, der in einem sehr leistungskritischen Bereich arbeitet, wird anfangen, seine Vorstellung von "Schönheit" in eine wesentlich datenorientiertere umzuwandeln und sich auf Schnittstellen zu konzentrieren, die Dinge in großen Mengen verarbeiten.

Gefährlich

"Gefährlich" könnte einfacher sein. Im Allgemeinen tendiert die Leistung dazu, auf Code niedrigerer Ebene zuzugreifen. Das Implementieren eines Speicherzuordners zum Beispiel ist unmöglich, ohne unter Datentypen zu greifen und auf der gefährlichen Ebene von Rohbits und Bytes zu arbeiten. Infolgedessen kann es hilfreich sein, das Augenmerk auf sorgfältige Testverfahren in diesen leistungskritischen Subsystemen zu richten und die Gründlichkeit der Tests an die angewandten Optimierungen anzupassen.

Schönheit

All dies würde sich jedoch auf der Ebene der Implementierungsdetails befinden. Sowohl in einer altgedienten, großangelegten als auch in einer leistungskritischen Denkweise tendiert "Schönheit" eher zu Schnittstellendesigns als zu Implementierungsdetails. Es wird zu einer exponentiell höheren Priorität, nach "schönen", verwendbaren, sicheren und effizienten Schnittstellen zu suchen, anstatt nach Implementierungen aufgrund von Kopplungs- und Kaskadenbrüchen, die angesichts einer Änderung des Schnittstellendesigns auftreten können. Implementierungen können jederzeit ausgetauscht werden. Wir iterieren in der Regel nach Bedarf zur Leistung und wie durch Messungen hervorgehoben. Der Schlüssel beim Schnittstellendesign besteht darin, grob genug zu modellieren, um Platz für solche Iterationen zu lassen, ohne das gesamte System zu beschädigen.

Tatsächlich würde ich vorschlagen, dass der Fokus eines Veteranen auf leistungskritische Entwicklung oft den Schwerpunkt auf Sicherheit, Tests und Wartbarkeit legt, genau wie der SE-Jünger im Allgemeinen, da eine umfangreiche Codebasis eine Reihe von Leistungen aufweist -kritische Subsysteme (Partikelsysteme, Bildverarbeitungsalgorithmen, Videoverarbeitung, Audio-Feedback, Raytracer, Mesh-Engines usw.) müssen der Softwareentwicklung besondere Aufmerksamkeit widmen, um ein Ertrinken in einem Wartungs-Albtraum zu vermeiden. Es ist kein Zufall, dass die erstaunlich effizientesten Produkte auch die geringste Anzahl von Fehlern aufweisen können.

TL; DR

Wie auch immer, das ist meine Sichtweise auf das Thema, angefangen von Prioritäten in wirklich leistungskritischen Bereichen, was die Latenz reduzieren und kleine Ineffizienzen akkumulieren kann und was eigentlich "Schönheit" ausmacht (wenn man die Dinge am produktivsten betrachtet).


0

Nicht anders zu sein, aber hier ist was ich tue:

  1. Schreiben Sie es sauber und wartbar.

  2. Führen Sie eine Leistungsdiagnose durch und beheben Sie die Probleme, die Ihnen bekannt sind, und nicht die, die Sie vermuten. Sie unterscheiden sich garantiert von Ihren Erwartungen.

Sie können diese Korrekturen auf eine Weise durchführen, die immer noch klar und wartbar ist. Sie müssen jedoch einen Kommentar hinzufügen, damit Personen, die sich den Code ansehen, wissen, warum Sie dies getan haben. Wenn Sie dies nicht tun, werden sie es rückgängig machen.

Gibt es also einen Kompromiss? Ich denke nicht wirklich.


0

Sie können hässlichen Code schreiben, der sehr schnell ist, und Sie können auch schönen Code schreiben, der so schnell ist wie Ihr hässlicher Code. Der Engpass wird nicht in der Schönheit / Organisation / Struktur Ihres Codes liegen, sondern in den Techniken, die Sie gewählt haben. Verwenden Sie beispielsweise nicht blockierende Steckdosen? Verwenden Sie Single-Threaded-Design? Verwenden Sie eine Warteschlange ohne Sperre für die Kommunikation zwischen Threads? Produzieren Sie Müll für den GC? Führen Sie im kritischen Thread eine blockierende E / A-Operation durch? Wie Sie sehen, hat dies nichts mit Schönheit zu tun.


0

Worauf kommt es dem Endverbraucher an?

  • Performance
  • Merkmale / Funktionalität
  • Design

Fall 1: Optimierter fehlerhafter Code

  • Harte Wartung
  • Schwer lesbar, wenn als Open-Source-Projekt

Fall 2: Nicht optimierter guter Code

  • Einfache Wartung
  • Schlechte Benutzererfahrung

Lösung?

Einfache, leistungskritische Codeteile optimieren

z.B:

Ein Programm, das aus 5 Methoden besteht , von denen 3 für die Datenverwaltung, eine zum Lesen der Festplatte und die andere zum Schreiben der Festplatte bestimmt sind

Diese drei Datenverwaltungsmethoden verwenden die beiden E / A-Methoden und hängen von ihnen ab

Wir würden die I / O-Methoden optimieren.

Grund: Es ist weniger wahrscheinlich, dass E / A-Methoden geändert werden, noch dass sie sich auf das Design der App auswirken. Alles in allem hängt alles in diesem Programm von ihnen ab, und daher scheinen sie leistungskritisch zu sein. Wir würden jeden Code verwenden, um sie zu optimieren .

Dies bedeutet, dass wir durch die Optimierung bestimmter Codeteile einen guten Code und ein übersichtliches Design des Programms erhalten und es gleichzeitig schnell halten

Ich denke..

Ich denke, schlechter Code macht es den Menschen schwer, das Polieren zu optimieren, und kleine Fehler könnten es noch schlimmer machen. Ein guter Code für Anfänger wäre also besser, wenn man diesen hässlichen Code nur gut schreibt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.