Was ist Ihre Weise, ein Gleichgewicht zwischen Optimierung und dem Erhalten von Sachen zu halten?
Nehmen Sie sich die Implementierungsdetails leicht, aber entwerfen Sie Designs mit viel Raum zum Optimieren. Nun, das ist der schwierige Teil, aber es ist nicht so schwer wie es klingt, sobald Sie sich daran gewöhnen. Der allgemeine Grund, warum Leute sich in Flaschenhals-Designs gefangen finden, ist normalerweise, weil die Entwürfe zu granular sind.
Als ein extremes Beispiel nehmen Sie eine Video-Processing-Anwendung, deren Design um eine abstrakte IPixel
dreht. Die Abstraktion ermöglicht es der Software, Videos mit unterschiedlichen Pixelformaten problemlos zu verarbeiten und trotzdem einheitlichen Code zu schreiben, der mit allen Pixelformaten funktioniert.
Eine solche Anwendung ist in Bezug auf die Leistung auf der zentralen Design-Ebene geschraubt, wird wahrscheinlich keine wettbewerbsfähige Leistung für die Bearbeitung, Codierung, Decodierung und Wiedergabe ohne eine epische architektonische Überarbeitung bieten. Und das ist, weil es sich dazu entschlossen hat, zu grob auf einer Ebene zu abstrahieren.
Durch die Wahl, auf der Ebene eines Pixels zu abstrahieren, entsteht ein dynamischer Versandaufwand pro Pixel. Der analoge virtuelle Zeiger (oder was auch immer die Sprache verwendet), um Merkmale wie virtuellen Versand, Laufzeittyp-Information (Reflektion, etc.) usw. zuzulassen, ist wahrscheinlich oft größer als das gesamte Pixel selbst, wobei seine Speicherbenutzung und Cache-Fehler sequentiell verdoppelt oder verdreifacht werden verarbeiten. Außerdem, wenn Sie im Nachhinein Multithread-Bildverarbeitung in vielen Bereichen möchten, müssten Sie jeden einzelnen Platz, der mit einem IPixel
gleichzeitig arbeitet, neu schreiben.
In der Zwischenzeit hätte dies alles vermieden werden können, wenn die Software einfach ihre Abstraktionen auf einer gröberen Ebene wie IImage
entworfen hätte und es vermieden hätte, einzelne Pixelobjekte dem Rest des Systems auszusetzen. Ein Bild ist effektiv eine Sammlung von Pixel (oft Millionen von Pixeln) und es kann Operationen, die viele Pixel auf einmal verarbeiten. Jetzt wird der Verarbeitungs- und Speicher-Overhead, der mit der Verarbeitung von Pixeln verbunden ist, für ein Bild mit einer Million Pixel auf 1/1.000.000 reduziert, zu welchem Zeitpunkt es trivialisiert wird. Das lässt auch den Bildoperationen viel Spielraum, um zum Beispiel Pixel parallel zu verarbeiten und jetzt auf einer zentralen Ebene zu vektorisieren, ohne epische Mengen an Code neu zu schreiben, da der Client-Code nicht einzeln ein Pixel auf einmal bearbeitet, sondern stattdessen ganze Pixel anfordert Bildoperationen, die ausgeführt werden sollen.
Während dies wie ein Kinderspiel mit Bildverarbeitung scheint, die inhärent ein sehr performance-kritischen Bereich ist, gibt es viel Raum, dies in anderen Domänen zu tun. Bei Ihrem klassischen Vererbungsbeispiel müssen Sie Dog
nicht erben Mammal
. Sie können Dogs
erben Mammals
.
Zurück zu den Dingen, beginne ich mit einer datenorientierten Denkweise, nicht um die effizientesten cachefreundlichen, peinlich parallelen, thread-sicheren, SIMD-freundlichen Datendarstellungen und hochmodernen Datenstrukturen und Algorithmen zu erhalten beim ersten Versuch. Sonst könnte ich eine ganze Woche damit verbringen, Dinge mit VTune in der Hand abzustimmen, während Benchmarks immer schneller laufen (ich liebe das, aber es ist definitiv nicht produktiv, überall und im Voraus zu arbeiten). Ich habe nur genug darüber nachgedacht, um die geeignete Granularität zu bestimmen, die ich verwenden sollte, um Dinge zu entwerfen: "sollte ich das System von Dog
oder abhängig machen?", so etwas. Und es erfordert nicht einmal so viel Nachdenken. Für OOP ist es so, "verarbeitet das System hunderttausend Hunde in jedem einzelnen Frame? Ja/nein?" Wenn "Ja", sollten Sie keine zentrale Schnittstelle Dog
entwerfen und keine zentrale Schnittstelle IMammal
entwerfen. Design Dogs
erben IMammals
, so wie wir die IPixel
Schnittstelle in der analogen Bildverarbeitung Szenario oben vermeiden, wenn wir Millionen von Pixeln gleichzeitig verarbeiten werden.
Die Größe der Daten sollte Ihnen auch einen Treffer geben. Wenn die Daten kleiner als 64 Byte oder weniger sind, besteht die Möglichkeit, dass sie keine Schnittstelle offenlegen, die Abhängigkeiten akkumuliert, es sei denn, sie ist definitiv nicht leistungskritisch. Stattdessen sollte es eine Sammlungsschnittstelle verfügbar machen, um mit vielen dieser Dinge gleichzeitig fertig zu werden. Unterdessen, wenn die Daten riesig sind, sagen wir 4 Kilobyte, dann ist es wahrscheinlich, dass es kaum helfen würde, eine Sammlungsschnittstelle zu enthüllen, und Sie könnten einfach eine skalare Schnittstelle entwerfen, die sich mit einem dieser Dinge gleichzeitig beschäftigt.
Multithreading ist die gleiche Art von Sache. Sie möchten beispielsweise nicht zu detailliert auf eine Ebene zugreifen und möchten nicht, dass Ihre Zugriffsmuster weiterhin gemeinsame Ressourcen treffen. Für Thread-Sicherheit möchten Sie auch in der Lage sein, einen Codeabschnitt zu nehmen und leicht zu verstehen, auf welchen Status von welchem Thread zugegriffen wird.Um dies zu erreichen, benötigen Sie ein gröberes Design, das eine homogenere Verarbeitung aufweist, so dass Sie die Speicherzugriffsmuster innerhalb der Implementierung des Designs selbst steuern und minimieren können, den Zugriff auf gemeinsam genutzte Ressourcen minimieren, Sperren auf zu granularer Ebene vermeiden oder Vermeiden Sie möglicherweise sogar eine völlige Sperrung. Solange Ihre Designs genug Raum zum Atmen lassen, können Sie im Nachhinein viel erreichen, aber der Schlüssel ist, dass Sie sich Platz zum Atmen lassen.
Ein Teeny-Ding, auf das eine Schiffsladung verschiedener Dinge im gesamten System angewiesen ist, die nicht-homogene Verarbeitung machen, hinterlässt keinen solchen Raum. Dort könnte man das Szenario des analogen Rennwagens mit nur 10 Metern Straße nutzen. Eine heftige Sache, die eine Bootsladung von Teeny-Sachen verarbeitet, die sie homogen speichert, lässt endlosen Raum übrig, um später zu optimieren.
Diese ziemlich subjektiv, empfehlen CW. –
Können Sie konkretere Beispiele machen? Die "allgemeine Aussage" ist meiner Meinung nach falsch. Ich meine natürlich, dass eine Serverkomponente, die gleichzeitig Clientanforderungen bedient, eine Art von "Multithread-Design" benötigt. – Alex
@Alex: Wenn du Webserver meinst, dann muss ich sagen, dass ich Server-Komponenten fast nie eines guten Multithread-Designs benötige, weil die Datenbank normalerweise der einzige Punkt der Synchronisation ist und ich nie eine Web-Server-Komponente gesehen habe, die algorithmisch war komplex zum Beispiel im Vergleich zu einer Datenbank oder einem Compiler. – Lothar