Beeinflusst die Objektorientierung wirklich die Leistung des Algorithmus?

14

Die Objektorientierung hat mir bei der Implementierung vieler Algorithmen sehr geholfen. Objektorientierte Sprachen führen Sie jedoch manchmal zu einem "einfachen" Ansatz, und ich bezweifle, dass dieser Ansatz immer eine gute Sache ist.

OO ist sehr hilfreich bei der schnellen und einfachen Codierung von Algorithmen. Aber könnte diese OOP ein Nachteil für leistungsbasierte Software sein, dh wie schnell wird das Programm ausgeführt?

Das Speichern von Diagrammknoten in einer Datenstruktur scheint zum Beispiel in erster Linie "unkompliziert" zu sein. Wenn Knotenobjekte jedoch viele Attribute und Methoden enthalten, kann dies zu einem langsamen Algorithmus führen?

Mit anderen Worten, könnten viele Referenzen zwischen vielen verschiedenen Objekten oder die Verwendung vieler Methoden aus vielen Klassen zu einer "schweren" Implementierung führen?

Florents Tselai
quelle
1
Eine merkwürdige Frage. Ich kann verstehen, wie OOP auf der Ebene der Architektur hilft. Eine Ebene der Algorithmenimplementierung basiert jedoch normalerweise auf Abstraktionen, die für alles, wofür OOP steht, sehr fremd sind. Wahrscheinlich ist die Leistung nicht das größte Problem für Ihre OOP-Algorithmenimplementierungen. Was die Leistung angeht, so hängt der größte Engpass bei OOP normalerweise mit virtuellen Anrufen zusammen.
SK-logic
@ SK-logic> Objektorientierung tendiert dazu, alles per Zeiger zu manipulieren, was eine wichtigere Arbeitsbelastung auf der Seite der Speicherzuweisung mit sich bringt, und nicht lokalisierte Daten tendieren dazu, sich nicht im CPU-Cache zu befinden und nicht zuletzt eine Menge indirekter Daten zu implizieren Verzweigung (virtuelle Funktionen), die für die CPU-Pipeline tödlich ist. OO ist eine gute Sache, kann aber in einigen Fällen durchaus Performance-Kosten verursachen.
Deadalnix
Wenn die Knoten in Ihrem Diagramm hundert Attribute haben, müssen Sie sie platzieren, unabhängig von dem für die tatsächliche Implementierung verwendeten Paradigma, und ich verstehe nicht, wie ein einzelnes Paradigma diesbezüglich im Allgemeinen einen Vorteil hat. @deadalnix: Vielleicht können die konstanten Faktoren schlechter sein, da bestimmte Optimierungen schwieriger sind. Beachten Sie aber, dass ich härter sage , nicht unmöglich - zum Beispiel kann PyPy Objekte in engen Schleifen entpacken und JVMs haben seit Ewigkeiten virtuelle Funktionsaufrufe inliniert.
Python eignet sich gut für Prototyping-Algorithmen, und dennoch benötigen Sie häufig keine Klasse, wenn Sie einen typischen Algorithmus darin implementieren.
Job
1
+1 Um die Objektorientierung mit Algorithmen
umlcat

Antworten:

16

Die Objektorientierung kann aufgrund der Kapselung bestimmte algorithmische Optimierungen verhindern. Zwei Algorithmen mögen besonders gut zusammenarbeiten, aber wenn sie sich hinter OO-Schnittstellen verstecken, geht die Möglichkeit verloren, ihre Synergien zu nutzen.

Schauen Sie sich numerische Bibliotheken an. Viele von ihnen (nicht nur die, die in den 60ern oder 70ern geschrieben wurden) sind keine OOP. Es gibt einen Grund dafür: Numerische Algorithmen funktionieren besser als entkoppelte Gruppe modulesals als OO-Hierarchien mit Schnittstellen und Kapselung.

quant_dev
quelle
2
Der Hauptgrund dafür ist, dass nur in C ++ die Verwendung von Ausdrucksvorlagen gefunden wurde, um die OO-Version so effizient wie möglich zu gestalten.
DeadMG
4
Schauen Sie sich die modernen C ++ - Bibliotheken an (STL, Boost) - sie sind auch überhaupt nicht OOP. Und das nicht nur wegen der Leistung. Algorithmen können normalerweise in einem OOP-Stil nicht gut dargestellt werden. Dinge wie die generische Programmierung sind für Low-Level-Algorithmen viel besser geeignet.
SK-logic
3
Was-was-was? Ich glaube, ich komme von einem anderen Planeten als quant_dev und SK-logic. Nein, ein anderes Universum. Mit verschiedenen Gesetzen der Physik und allem.
Mike Nakis
5
@MikeNakis: Der Unterschied in der Sichtweise liegt in (1), ob ein bestimmtes Stück Computercode überhaupt von OOP in Bezug auf die Lesbarkeit profitieren kann (was bei numerischen Rezepten nicht der Fall ist); (2) ob die OOP Klasse Design ausrichtet mit der optimalen Datenstruktur und Algorithmus (meine Antwort sehen); und (3) ob jede Indirektionsebene einen ausreichenden "Wert" liefert (in Bezug auf die pro Funktionsaufruf geleistete Arbeit oder die konzeptionelle Klarheit pro Ebene), rechtfertigt den Overhead (aufgrund der Indirektion, des Funktionsaufrufs, der Ebenen oder des Kopierens von Daten). (4) Schließlich ist die Komplexität des Compilers / JIT / Optimierers ein begrenzender Faktor.
rwong
2
@ MikeNakis, was meinst du? Denken Sie, STL ist eine OOP-Bibliothek? Generisches Programmieren passt sowieso nicht zu OOP. Und es muss nicht erwähnt werden, dass OOP ein zu enger Rahmen ist, der nur für sehr wenige praktische Aufgaben geeignet ist und für alles andere fremd ist.
SK-logic
9

Was bestimmt die Leistung?

Die Grundlagen: Datenstrukturen, Algorithmen, Computerarchitektur, Hardware. Plus Aufwand.

Ein OOP-Programm kann dazu entworfen werden genau mit der Auswahl der Datenstrukturen und Algorithmen übereinstimmt, die nach der CS-Theorie als optimal erachtet werden. Es hat die gleichen Leistungsmerkmale wie das optimale Programm, zuzüglich eines gewissen Overheads. Der Overhead kann in der Regel minimiert werden.

Allerdings ist das ein Programm anfänglich nur mit OOP-Bedenken entworfen wurde, ohne die Grundlagen zu berücksichtigen, kann jedoch anfänglich suboptimal sein. Die Suboptimalität kann manchmal durch Umgestaltung beseitigt werden. Manchmal ist dies nicht der Fall - eine vollständige Umschreibung ist erforderlich.

Vorsichtsmaßnahme: Ist die Leistung von Unternehmenssoftware von Bedeutung?

Ja, aber Time-to-Market (TTM) ist um Größenordnungen wichtiger. Unternehmenssoftware legt den Schwerpunkt auf die Anpassbarkeit des Codes an komplexe Geschäftsregeln. Leistungsmessungen sollten während des gesamten Entwicklungslebenszyklus durchgeführt werden. (Siehe Abschnitt: Was bedeutet optimale Leistung? ) Es sollten nur marktfähige Verbesserungen vorgenommen und in späteren Versionen schrittweise eingeführt werden.

Was bedeutet optimale Leistung?

Im Allgemeinen besteht das Problem mit der Software-Leistung darin, dass, um zu beweisen, dass "eine schnellere Version existiert", diese schnellere Version zuerst existieren muss (dh kein anderer Beweis als er selbst).

Manchmal wird diese schnellere Version zuerst in einer anderen Sprache oder einem anderen Paradigma gesehen. Dies sollte als Hinweis auf eine Verbesserung verstanden werden, nicht als Beurteilung der Minderwertigkeit einiger anderer Sprachen oder Paradigmen.

Warum machen wir OOP, wenn dies unsere Suche nach optimaler Leistung behindert?

OOP führt Overhead (in Bezug auf Speicherplatz und Ausführung) ein, um die "Bearbeitbarkeit" und damit den Geschäftswert des Codes zu verbessern. Dies reduziert die Kosten für Weiterentwicklung und Optimierung. Siehe @ MikeNakis .

Welche Teile von OOP können ein anfänglich suboptimales Design fördern?

Die Teile von OOP, die (i) Einfachheit / Intuitivität fördern, (ii) Umgang mit umgangssprachlichen Entwurfsmethoden anstelle von Grundlagen, (iii) mehrere maßgeschneiderte Implementierungen desselben Zwecks verhindern.

  • KUSS
  • YAGNI
  • TROCKEN
  • Objektdesign (z. B. mit CRC-Karten) ohne Berücksichtigung der Grundlagen

Die strikte Anwendung einiger OOP-Richtlinien (Kapselung, Nachrichtenübergabe, eine Sache gut machen) führt in der Tat zunächst zu langsamerem Code. Leistungsmessungen helfen bei der Diagnose dieser Probleme. Solange die Datenstruktur und der Algorithmus mit dem theoretisch vorhergesagten optimalen Design übereinstimmen, kann der Overhead normalerweise minimiert werden.

Was sind die allgemeinen Abhilfemaßnahmen für OOP-Gemeinkosten?

Wie bereits erwähnt, werden Datenstrukturen verwendet, die für das Design optimal sind.

Einige Sprachen unterstützen Code-Inlining, wodurch eine gewisse Laufzeitleistung wiederhergestellt werden kann.

Wie könnten wir OOP einführen, ohne Leistung zu opfern?

Lernen und wenden Sie sowohl das OOP als auch die Grundlagen an.

Es ist wahr, dass die strikte Einhaltung von OOP Sie möglicherweise daran hindert, eine schnellere Version zu schreiben. Manchmal kann eine schnellere Version nur von Grund auf neu geschrieben werden. Aus diesem Grund ist es hilfreich, mehrere Codeversionen unter Verwendung verschiedener Algorithmen und Paradigmen (OOP, generisch, funktional, mathematisch, Spaghetti) zu schreiben und anschließend mithilfe von Optimierungstools jede Version an die beobachtete maximale Leistung heranzuführen.

Gibt es Codetypen, die von OOP nicht profitieren?

(Erweitert aus der Diskussion zwischen [@quant_dev], [@ SK-logic] und [@MikeNakis])

  1. Numerische Rezepte, die aus der Mathematik stammen.
    • Die mathematischen Gleichungen und Transformationen selbst können als Objekte verstanden werden.
    • Es sind sehr ausgefeilte Codetransformationstechniken erforderlich, um effizienten ausführbaren Code zu generieren. Die naive ("White-Board") Implementierung wird eine miserable Leistung haben.
    • Die heutigen Mainstream-Compiler sind jedoch nicht in der Lage, dies zu tun.
    • Spezielle Software (MATLAB und Mathematica usw.) verfügt sowohl über JIT- als auch über symbolische Löser, die in der Lage sind, für einige Unterprobleme effizienten Code zu generieren. Diese spezialisierten Löser können als Spezialkompilierer (Vermittler zwischen von Menschen lesbarem Code und maschinenausführbarem Code) angesehen werden, die selbst von einem OOP-Design profitieren.
    • Für jedes Unterproblem sind eigene "Compiler" - und "Code-Transformationen" erforderlich. Daher ist dies ein sehr aktiver offener Forschungsbereich, in dem jedes Jahr neue Ergebnisse erscheinen.
    • Da die Recherche lange dauert, müssen Softwareentwickler die Optimierung auf Papier durchführen und den optimierten Code in Software umschreiben. Der transkribierte Code könnte tatsächlich unverständlich sein.
  2. Sehr niedriger Code.
      *
rwong
quelle
8

Dabei geht es nicht wirklich um Objektorientierung, sondern um Container. Wenn Sie eine doppelt verknüpfte Liste zum Speichern von Pixeln in Ihrem Videoplayer verwendet haben, leidet diese.

Wenn Sie jedoch den richtigen Container verwenden, gibt es keinen Grund, warum ein std :: vector langsamer als ein Array ist, und da Sie alle gängigen Algorithmen bereits dafür geschrieben haben - von Experten - ist er wahrscheinlich schneller als Ihr selbst geschriebener Array-Code.

Martin Beckett
quelle
1
Da Compiler nicht optimal sind (oder die Regeln der Programmiersprache das Ausnutzen bestimmter Annahmen oder Optimierungen verbieten), gibt es tatsächlich einen Aufwand, der nicht beseitigt werden kann. Bestimmte Optimierungen, z. B. die Vektorisierung, stellen Anforderungen an die Datenorganisation (z. B. Struktur von Arrays anstelle von Array von Strukturen), die OOP entweder verbessern oder behindern kann. (Ich habe kürzlich gerade an einer std :: vector-Optimierungsaufgabe gearbeitet.)
rwong
5

OOP ist offensichtlich eine gute Idee und kann wie jede gute Idee überstrapaziert werden. Nach meiner Erfahrung ist es viel zu viel genutzt. Schlechte Leistung und schlechte Wartbarkeit sind die Folge.

Es hat nichts mit dem Aufwand zu tun, virtuelle Funktionen aufzurufen, und nicht viel mit dem, was der Optimierer / Jitter tut.

Es hat alles mit Datenstrukturen zu tun, die zwar die beste Big-O-Leistung aufweisen, jedoch sehr schlechte konstante Faktoren aufweisen. Dies erfolgt unter der Annahme, dass ein Leistungsproblem in der App an einer anderen Stelle auftritt.

Dies äußert sich zum einen darin, wie oft pro Sekunde eine neue Operation ausgeführt wird, für die eine O (1) -Leistung angenommen wird, die jedoch Hunderte bis Tausende von Anweisungen ausführen kann (einschließlich des entsprechenden Löschvorgangs) oder GC-Zeit). Dies kann durch Speichern der verwendeten Objekte verringert werden, macht den Code jedoch weniger "sauber".

Eine andere Art, wie sich dies manifestiert, ist die Art und Weise, wie Benutzer dazu ermutigt werden, Eigenschaftsfunktionen, Benachrichtigungshandler, Aufrufe von Basisklassenfunktionen und alle Arten von unterirdischen Funktionsaufrufen zu schreiben, um die Konsistenz aufrechtzuerhalten. Für die Aufrechterhaltung der Konsistenz sind sie von begrenztem Erfolg, aber sie sind äußerst erfolgreich darin, Zyklen zu verschwenden. Programmierer verstehen das Konzept normalisierter Daten , wenden diese jedoch in der Regel nur auf das Datenbankdesign an. Sie wenden es nicht auf das Datenstrukturdesign an, zumindest nicht, weil OOP ihnen sagt, dass sie es nicht müssen. So einfach wie das Setzen eines Modified-Bits in einem Objekt kann es zu einem Tsunami von Aktualisierungen in der Datenstruktur kommen, da keine Klasse, die ihren Code wert ist, einen Modified-Aufruf annimmt und ihn nur speichert .

Vielleicht ist die Leistung einer bestimmten App genau so, wie sie geschrieben wurde.

Auf der anderen Seite, wenn es ein Leistungsproblem gibt, ist hier ein Beispiel, wie ich es tune. Es ist ein mehrstufiger Prozess. In jeder Phase entfällt ein großer Teil der Zeit auf bestimmte Aktivitäten, die durch etwas Schnelleres ersetzt werden könnten. (Ich habe nicht "Engpass" gesagt. Dies sind nicht die Dinge, die Profiler gut finden können.) Dieser Prozess erfordert häufig, um die Beschleunigung zu erreichen, einen umfassenden Austausch der Datenstruktur. Häufig ist diese Datenstruktur nur vorhanden, weil die OOP-Praxis empfohlen wird.

Mike Dunlavey
quelle
3

Theoretisch könnte dies zu Langsamkeit führen, aber selbst dann wäre es kein langsamer Algorithmus, sondern eine langsame Implementierung. In der Praxis können Sie anhand der Objektorientierung verschiedene Was-wäre-wenn-Szenarien ausprobieren (oder den Algorithmus in Zukunft überarbeiten) und so algorithmische Verbesserungen vornehmen, die Sie nicht erhoffen könnten, wenn Sie ihn in der ersten Phase auf Spaghetti-Weise geschrieben hätten Ort, denn die Aufgabe wäre entmutigend. (Sie müssten im Wesentlichen die ganze Sache umschreiben.)

Wenn Sie beispielsweise die verschiedenen Aufgaben und Entitäten in bereinigte Objekte unterteilt haben, können Sie möglicherweise später problemlos eine Caching-Funktion zwischen einige (für sie transparente) Objekte einbetten, die zu Tausenden von Ergebnissen führen kann. Falte Verbesserung.

Im Allgemeinen führen die Arten von Verbesserungen, die Sie mit einer einfachen Sprache erreichen können (oder clevere Tricks mit einer höheren Sprache), zu konstanten (linearen) Zeitverbesserungen, die in der Big-Oh-Notation nicht berücksichtigt werden. Mit algorithmischen Verbesserungen können Sie möglicherweise nichtlineare Verbesserungen erzielen. Das ist unbezahlbar.

Mike Nakis
quelle
1
+1: Der Unterschied zwischen Spaghetti und objektorientiertem Code (oder Code, der in einem gut definierten Paradigma geschrieben ist) ist: Jede neu geschriebene Version guten Codes bringt neues Verständnis in das Problem. Jede neu geschriebene Version von Spaghetti bringt keine Einsicht.
rwong
@rwong könnte nicht besser erklärt werden ;-)
Umlcat
3

Aber könnte diese OOP ein Nachteil für leistungsbasierte Software sein, dh wie schnell wird das Programm ausgeführt?

Oft ja !!! ABER...

Mit anderen Worten, könnten viele Referenzen zwischen vielen verschiedenen Objekten oder die Verwendung vieler Methoden aus vielen Klassen zu einer "schweren" Implementierung führen?

Nicht unbedingt. Dies hängt von der Sprache / dem Compiler ab. Zum Beispiel wird ein optimierender C ++ - Compiler, vorausgesetzt, Sie verwenden keine virtuellen Funktionen, Ihren Objekt-Overhead häufig auf Null reduzieren. Sie können beispielsweise einen Wrapper über inteinen solchen schreiben oder einen intelligenten Zeiger mit Gültigkeitsbereich über einen einfachen alten Zeiger, der genauso schnell arbeitet wie die direkte Verwendung dieser einfachen alten Datentypen.

In anderen Sprachen wie Java ist der Aufwand für ein Objekt geringfügig (in vielen Fällen recht gering, in einigen seltenen Fällen jedoch astronomisch, wenn es sich um sehr kleine Objekte handelt). Beispielsweise ist Integeres wesentlich weniger effizient als int(benötigt 16 Bytes im Gegensatz zu 4 bei 64-Bit). Dabei handelt es sich nicht nur um krassen Müll oder ähnliches. Im Gegenzug bietet Java die Möglichkeit, jeden benutzerdefinierten Typ einheitlich zu reflektieren und alle nicht als gekennzeichneten Funktionen außer Kraft zu setzen final.

Nehmen wir jedoch das beste Szenario: den optimierenden C ++ - Compiler, mit dem Objektschnittstellen bis auf Null optimiert werden können . Selbst dann verschlechtert OOP häufig die Leistung und verhindert, dass sie den Höchstwert erreicht. Das mag sich wie ein komplettes Paradox anhören: Wie könnte es sein? Das Problem liegt in:

Interface Design und Encapsulation

Das Problem ist, dass selbst wenn ein Compiler die Struktur eines Objekts auf Null reduzieren kann (was zumindest sehr oft für die Optimierung von C ++ - Compilern zutrifft), die Kapselung und das Schnittstellendesign (und die angesammelten Abhängigkeiten) von feinkörnigen Objekten häufig das verhindern Optimalste Datendarstellung für Objekte, die von der Masse aggregiert werden sollen (was bei leistungskritischer Software häufig der Fall ist).

Nehmen Sie dieses Beispiel:

class Particle
{
public:
    ...

private:
    double birth;                // 8 bytes
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
    /*padding*/                  // 4 bytes of padding
};
Particle particles[1000000];     // 1mil particles (~24 megs)

Angenommen, unser Speicherzugriffsmuster besteht darin, diese Partikel einfach nacheinander zu durchlaufen und sie wiederholt um jedes Bild zu bewegen, sie von den Ecken des Bildschirms abzuprallen und dann das Ergebnis zu rendern.

Es ist bereits ein eklatanter 4-Byte-Padding-Overhead zu sehen, der erforderlich ist, um das birthElement ordnungsgemäß auszurichten , wenn Partikel zusammenhängend aggregiert werden. Bereits ~ 16,7% des Speichers werden mit dem für die Ausrichtung verwendeten Totraum verschwendet.

Dies könnte problematisch erscheinen, da wir heutzutage Gigabyte DRAM haben. Doch selbst die scheußlichsten Maschinen, die wir heute haben, haben oft nur 8 Megabyte, wenn es um die langsamste und größte Region des CPU-Caches (L3) geht. Je weniger wir hineinpassen können, desto mehr zahlen wir für den wiederholten DRAM-Zugriff und desto langsamer wird es. Plötzlich scheint es nicht mehr trivial zu sein, 16,7% des Arbeitsspeichers zu verschwenden.

Diesen Overhead können wir leicht beseitigen, ohne die Ausrichtung des Feldes zu beeinträchtigen:

class Particle
{
public:
    ...

private:
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
};
Particle particles[1000000];     // 1mil particles (~12 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Jetzt haben wir den Speicher von 24 MB auf 20 MB reduziert. Mit einem sequentiellen Zugriffsmuster verbraucht das Gerät diese Daten jetzt viel schneller.

Aber schauen wir uns dieses birthFeld etwas genauer an. Nehmen wir an, es zeichnet die Startzeit auf, zu der ein Partikel geboren (erzeugt) wird. Stellen Sie sich vor, auf das Feld wird nur zugegriffen, wenn ein Partikel zum ersten Mal erstellt wird, und alle 10 Sekunden, um zu prüfen, ob ein Partikel stirbt und an einer zufälligen Stelle auf dem Bildschirm wiedergeboren wird. In diesem Fall birthist ein kaltes Feld. In unseren leistungskritischen Schleifen wird nicht darauf zugegriffen.

Infolgedessen sind die tatsächlichen leistungskritischen Daten nicht 20 Megabyte, sondern ein zusammenhängender Block von 12 Megabyte. Der aktuelle Arbeitsspeicher, auf den wir häufig zugreifen, ist auf die Hälfte seiner Größe geschrumpft ! Erwarten Sie erhebliche Geschwindigkeitssteigerungen gegenüber unserer ursprünglichen 24-Megabyte-Lösung (muss nicht gemessen werden - wurde bereits tausend Mal durchgeführt, ist aber im Zweifelsfall unverbindlich).

Beachten Sie jedoch, was wir hier gemacht haben. Wir haben die Einkapselung dieses Partikelobjekts vollständig aufgehoben. Sein Status ist jetzt zwischen Particleden privaten Feldern eines Typs und einem separaten, parallelen Array aufgeteilt. Und hier steht granulares objektorientiertes Design im Weg.

Wir können die optimale Datendarstellung nicht ausdrücken, wenn wir uns auf das Interface-Design eines einzelnen, sehr granularen Objekts wie eines einzelnen Partikels, eines einzelnen Pixels, sogar eines einzelnen 4-Komponenten-Vektors, möglicherweise sogar eines einzelnen "Kreaturen" -Objekts in einem Spiel beschränken usw. Die Geschwindigkeit eines Geparden wird verschwendet, wenn er auf einer 2 Quadratmeter großen kleinen Insel steht, und genau das leistet ein sehr granulares objektorientiertes Design häufig in Bezug auf die Leistung. Es beschränkt die Datendarstellung auf einen suboptimalen Charakter.

Nehmen wir an, wir bewegen nur Partikel, und greifen in drei separaten Schleifen auf deren x / y / z-Felder zu. In diesem Fall können wir von der SoA-ähnlichen SIMD-Struktur mit AVX-Registern profitieren, die 8 SPFP-Operationen parallel vektorisieren können. Aber um dies zu tun, müssen wir jetzt diese Darstellung verwenden:

float particle_x[1000000];       // 1mil particle X positions (~4 megs)
float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Jetzt fliegen wir mit der Partikelsimulation, aber schauen Sie, was mit unserem Partikeldesign passiert ist. Es wurde komplett abgerissen und wir betrachten nun 4 parallele Arrays und haben kein Objekt, sie zu aggregieren. Unser objektorientiertes ParticleDesign ist sayonara gegangen.

Das ist mir schon oft passiert, als ich in leistungskritischen Bereichen gearbeitet habe, in denen Benutzer Geschwindigkeit fordern und nur die Korrektheit das ist, was sie mehr fordern. Diese kleinen, winzigen objektorientierten Entwürfe mussten abgerissen werden, und die Kaskadenbrüche erforderten oft eine langsame Abschreibungsstrategie für den schnelleren Entwurf.

Lösung

Das obige Szenario stellt nur ein Problem bei granularen objektorientierten Designs dar. In diesen Fällen müssen wir häufig die Struktur abreißen, um effizientere Darstellungen als Ergebnis von SoA-Wiederholungen, Aufteilen von Heiß- / Kaltfeldern und Reduzierung des Paddings für sequentielle Zugriffsmuster zu erzielen (Padding ist manchmal hilfreich für die Leistung bei wahlfreiem Zugriff) Muster in AoS-Fällen, aber fast immer ein Hindernis für sequentielle Zugriffsmuster usw.

Wir können jedoch diese endgültige Darstellung übernehmen und dennoch eine objektorientierte Schnittstelle modellieren:

// Represents a collection of particles.
class ParticleSystem
{
public:
    ...

private:
    double particle_birth[1000000];  // 1mil particle births (~8 bytes)
    float particle_x[1000000];       // 1mil particle X positions (~4 megs)
    float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
    float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
};

Jetzt geht es uns gut. Wir können alle objektorientierten Goodies bekommen, die wir mögen. Der Gepard hat ein ganzes Land, durch das er so schnell wie möglich rennen kann. Unsere Schnittstellendesigns führen uns nicht länger in eine Engpass-Ecke.

ParticleSystemkann möglicherweise sogar abstrakt sein und virtuelle Funktionen verwenden. Es ist strittig, wir zahlen für den Overhead auf Partikelebene anstatt auf Partikelebene . Der Overhead beträgt 1 / 1.000.000stel dessen, was sonst wäre, wenn wir Objekte auf der Ebene der einzelnen Partikel modellieren würden.

Das ist also die Lösung in wirklich leistungskritischen Bereichen, die eine hohe Last bewältigen, und für alle Arten von Programmiersprachen (von dieser Technik profitieren C, C ++, Python, Java, JavaScript, Lua, Swift usw.). Und es kann nicht einfach als "vorzeitige Optimierung" bezeichnet werden, da es sich um das Interface-Design und die Architektur der . Wir können keine Codebasis schreiben, die ein einzelnes Partikel als Objekt mit einer Schiffsladung von Client-Abhängigkeiten zu a modelliertParticle'söffentliche Schnittstelle und dann später unsere Meinung ändern. Das habe ich oft getan, als ich aufgerufen wurde, um ältere Codebasen zu optimieren, und es kann Monate dauern, Zehntausende von Codezeilen sorgfältig umzuschreiben, um das umfangreichere Design zu verwenden. Dies wirkt sich idealerweise darauf aus, wie wir die Dinge im Voraus gestalten, vorausgesetzt, wir können mit einer hohen Last rechnen.

Ich halte diese Antwort in der einen oder anderen Form in vielen Performance-Fragen, insbesondere im Zusammenhang mit objektorientiertem Design, aufrecht. Objektorientiertes Design kann immer noch mit den höchsten Leistungsanforderungen kompatibel sein, aber wir müssen die Art und Weise, wie wir darüber nachdenken, ein wenig ändern. Wir müssen dem Geparden etwas Raum geben, damit er so schnell wie möglich rennt, und das ist oft unmöglich, wenn wir kleine Objekte entwerfen, die kaum irgendeinen Zustand speichern.


quelle
Fantastisch. Dies ist, wonach ich eigentlich gesucht habe, um OOP mit einem hohen Leistungsbedarf zu kombinieren. Ich kann wirklich nicht verstehen, warum es nicht mehr upvoted ist.
Pbx
2

Ja, die objektorientierte Denkweise kann auf jeden Fall neutral oder negativ sein, wenn es um Hochleistungsprogrammierung geht, sowohl auf algorithmischer als auch auf Implementierungsebene. Wenn OOP die algorithmische Analyse ersetzt, kann dies zu einer vorzeitigen Implementierung führen, und auf der untersten Ebene müssen die OOP-Abstraktionen beiseite gelegt werden.

Das Problem ergibt sich aus der Betonung von OOP auf das Nachdenken über einzelne Instanzen. Ich denke, es ist fair zu sagen, dass die OOP-Denkweise über einen Algorithmus darin besteht, über einen bestimmten Satz von Werten nachzudenken und ihn auf diese Weise zu implementieren. Wenn dies Ihr Weg auf höchster Ebene ist, ist es unwahrscheinlich, dass Sie eine Transformation oder Restrukturierung realisieren, die zu Big O-Gewinnen führen würde.

Auf der algorithmischen Ebene wird oft an das Gesamtbild und die Einschränkungen oder Beziehungen zwischen Werten gedacht, die zu Big-O-Gewinnen führen. Ein Beispiel könnte sein, dass es in der OOP-Denkweise nichts gibt, was Sie dazu bringen würde, "einen kontinuierlichen Bereich von Ganzzahlen zu summieren" von einer Schleife zu transformieren(max + min) * n/2

Auf der Implementierungsebene sind Computer zwar für die meisten Algorithmen auf Anwendungsebene "schnell genug", bei leistungskritischem Code auf niedriger Ebene ist jedoch die Lokalität von großer Bedeutung. Die OOP-Betonung auf das Nachdenken über eine einzelne Instanz und die Werte eines Durchgangs durch die Schleife kann wiederum negativ sein. In leistungsfähigem Code möchten Sie die Schleife möglicherweise nicht einfach schreiben, sondern nur teilweise abrollen, mehrere Ladeanweisungen oben gruppieren, sie dann in eine Gruppe umwandeln und dann in eine Gruppe schreiben. Währenddessen würden Sie auf Zwischenberechnungen und vor allem auf den Cache- und Speicherzugriff achten. Probleme, bei denen die OOP-Abstraktionen nicht mehr gültig sind. Und wenn man es befolgt, kann es irreführend sein: Auf dieser Ebene muss man die Repräsentationen auf Maschinenebene kennen und darüber nachdenken.

Wenn Sie sich etwas wie die Performance Primitives von Intel ansehen, haben Sie buchstäblich Tausende von Implementierungen der Fast Fourier Transform, von denen jede optimiert wurde, um für eine bestimmte Datengröße und Maschinenarchitektur besser zu funktionieren. (Faszinierenderweise stellt sich heraus, dass der Großteil dieser Implementierungen maschinengeneriert ist: Markus Püschel Automatic Performance Programming )

Wie in den meisten Antworten bereits erwähnt, ist OOP für die meiste Entwicklung und für die meisten Algorithmen für die Leistung irrelevant. Solange Sie nicht "vorzeitig pessimisieren" und viele nicht-lokale Anrufe hinzufügen, ist der thisZeiger weder hier noch da.

Larry OBrien
quelle
0

Es ist verwandt und oft übersehen.

Es ist keine einfache Antwort, es hängt davon ab, was Sie tun möchten.

Einige Algorithmen weisen eine bessere Leistung bei Verwendung einer einfach strukturierten Programmierung auf, während andere eine bessere Objektorientierung verwenden.

Vor der Objektorientierung unterrichten viele Schulen das Design von Algorithmen mit strukturierter Programmierung. Heutzutage unterrichten viele Schulen objektorientiertes Programmieren und ignorieren dabei Algorithmus-Design und -Leistung.

Natürlich gab es dort Schulen, die strukturiertes Programmieren lehrten, die sich überhaupt nicht für Algorithmen interessierten.

umlcat
quelle
0

Die Leistung hängt letztendlich von den CPU- und Speicherzyklen ab. Der prozentuale Unterschied zwischen dem Overhead von OOP-Messaging und -Kapselung und einer offeneren Programmiersemantik kann jedoch signifikant genug sein oder auch nicht, um einen spürbaren Unterschied in der Leistung Ihrer Anwendung zu bewirken. Wenn eine App an eine Festplatte oder einen Daten-Cache gebunden ist, kann jeglicher OOP-Overhead durch das Rauschen vollständig verloren gehen.

In den inneren Schleifen der Echtzeit-Signal- und Bildverarbeitung und in anderen numerisch rechengebundenen Anwendungen kann der Unterschied jedoch ein erheblicher Prozentsatz der CPU- und Speicherzyklen sein, was die Ausführung von OOP-Overhead erheblich verteuern kann.

Die Semantik einer bestimmten OOP-Sprache kann genügend Möglichkeiten für den Compiler bieten oder nicht, um diese Zyklen zu optimieren, oder damit die Verzweigungsvorhersageschaltungen der CPU immer richtig raten und diese Zyklen mit Pre-Fetch und Pipelining abdecken.

hotpaw2
quelle
0

Ein gutes objektorientiertes Design hat mir geholfen, eine Anwendung erheblich zu beschleunigen. A musste auf algorithmische Weise komplexe Grafiken erzeugen. Ich habe es durch Microsoft Visio-Automatisierung gemacht. Ich habe gearbeitet, war aber unglaublich langsam. Glücklicherweise hatte ich eine zusätzliche Abstraktionsebene zwischen der Logik (dem Algorithmus) und dem Visio-Material eingefügt. Meine Visio-Komponente hat ihre Funktionalität über eine Schnittstelle verfügbar gemacht. Dadurch konnte ich die langsame Komponente leicht durch eine andere SVG-Datei ersetzen, die mindestens 50-mal schneller war! Ohne eine saubere objektorientierte Herangehensweise wären die Codes für den Algorithmus und die Vision-Steuerung auf eine Weise verwickelt gewesen, die die Änderung in einen Albtraum verwandelt hätte.

Olivier Jacot-Descombes
quelle
Meinten Sie OO Design angewendet mit einer prozeduralen Sprache oder OO Design & OO Programmiersprache?
umlcat
Ich spreche von einer C # -Anwendung. Sowohl das Design als auch die Sprache sind OO. Da die OO-Intensität der Sprache einige kleine Leistungseinbußen mit sich bringt (virtuelle Methodenaufrufe, Objekterstellung, Mitgliederzugriff über die Schnittstelle), hat mir das OO-Design geholfen, eine viel schnellere Anwendung zu erstellen. Was ich sagen möchte ist: Vergessen Sie Leistungseinbußen durch OO (Sprache und Design). OO wird Ihnen keinen Schaden zufügen, es sei denn, Sie führen umfangreiche Berechnungen mit Millionen von Iterationen durch. Wo Sie normalerweise viel Zeit verlieren, ist I / O.
Olivier Jacot-Descombes