gemeinsame vorzeitige Optimierungen und Mikrooptimierungen

7

Ich habe viel relativ einfache numerische Programmierung und eine große Menge an Programmierung im Allgemeinen gemacht. In dieser Zeit habe ich festgestellt, dass viele Menschen dazu neigen, das zu tun, was ich Optimierung durch Verdächtigung oder Optimierung durch Hörensagen nenne. Einige von diesen sind wirklich Optimierungen (und tatsächlich, einige von ihnen machen einen signifikanten Unterschied in der tatsächlichen Geschwindigkeit der Ausführung), und einige nicht. Ich versuche, eine Liste von diesen zu kompilieren, mit dem Endziel, sie zu profilieren, um zu sehen, ob sie wirklich einen Unterschied machen mit modernen Compilern und wie viel von ihnen sie machen.

Einige Beispiele könnten sein:

  • x++ gegen ++x
  • Wenn Sie 1/2*a*b*c/(d*e)*x^2) in einer Schleife berechnen müssen, wobei a, b, c, d, e innerhalb der Schleife nicht variiert, ist die Vorberechnung des Koeffizienten schneller
  • In Fortran werden Arrays so angeordnet, dass der letzte Index am langsamsten variiert. Deshalb verschachteln wir Schleifen in der Reihenfolge k, j, i anstatt i, j, k, um die Cache-Nutzung zu erhöhen
  • Vermeidung von Anweisungen in inneren Schleifen
notJim 01.01.2010, 21:24
quelle

11 Antworten

15

Der schlechte Name für viele vorzeitige Optimierungen kommt davon, für ein anderes Alter zu sein.

Dann:

  • Speicher ist knapp, aber der Zugang ist billig
  • Anweisungen sind teuer
  • Optimierer sind dumm und können leicht manuell übertroffen werden. Ihr Compiler benötigt viele "Hinweise"
  • (zumindest für die x86-Plattform): Multiplikation ist langsam, Division dreifach so, und Fließkomma kann ewig dauern. Source-to-Machine-Code war ziemlich einfach, und die CPUs würden das mehr oder weniger Schritt für Schritt ausführen.

Jetzt:

  • Es gibt mehr als genug Speicher für die meisten Anwendungen, aber der Zugriff ist stark nichtlinear (Hotspot-Zugriff ist immer noch billig)
  • Anweisungen sind sehr billig
  • Optimierer sind schlauer als die meisten von uns. Sie verstehen den C ++ Standard sogar besser als viele von uns
  • (x86) Wir sind von einer CISC-Architektur zu einer RISCified-Architektur übergegangen, die RISCified CISC-Code ausführt. Und nur Agner Fog und Ihr Optimierer wissen, was das eigentlich bedeutet.

Eine Sache, die man beachten sollte, ist, wenn man vom Desktop zum Embedded wechselt, man erhält einen allgemeinen Technologie-Rückfall von zehn Jahren.

Also hier ist eine Liste von Dingen, die ich entweder meinem Compiler überlasse oder nicht mehr mache

  • Multiplikation mit Konstante durch shift-and-add (e.g. x*5 == x+(x<<2)) ersetzen
  • Ersetzen der Ganzzahl durch Konstanten durch Überlauf-Multiplikation
  • Vorberechnung von Konstanten
  • Nachschlagetabellen, z.B. für schnelle aber weniger genaue Sinusberechnung.
  • XOR fpor Integeraustausch
  • hisst einfache konstante Auswertungen aus einer Schleife
  • kurze Schleifen mit konstanter Länge abwickeln

Vorzeitige Optimierungen sind nicht schlecht. Ansonsten müssten wir alle Bubble-Sortierung verwenden, bis bewiesen ist, dass die Sortierung ein Flaschenhals ist :-) Knuth, in seinem ursprünglichen Zitat, kritisierte die Konzentration auf die kleinen Dinge, anstatt sich mit der Leistung auf einer höheren Ebene zu befassen - Design, Datenzugriffsmuster und Algorithmen.

Frühe Optimierung (oder "Optimierung durch Verdächtigung", wie Sie sagen, ich mag diesen Begriff) ist nur dann böse, wenn es sich als nicht effektiv erweist und den Code (wie Lesbarkeit, Einfachheit oder Flexibilität).

Außerdem erhält der Bibliothekscode etwas Spielraum. Sie wissen nicht, wie oft Ihre neuen UnfuddleString-Funktionen schließlich von Endbenutzeraktionen aufgerufen werden.

    
peterchen 19.09.2009, 09:11
quelle
2

Mein Favorit ist es, keine Methodenaufrufe unter den Bedingungen von for-Schleifen zu platzieren.

Beispiel (in einer Sprache mit Schleifen):

%Vor%

Ich erinnere mich an die Lektüre des Underhanded Competition für 2006, bei dem es darum ging, einen perfekten Code zu schreiben, der auf verschiedenen Plattformen mit extrem unterschiedlichen Geschwindigkeiten lief. Der Gewinner hatte im Wesentlichen eine For-Schleife mit einem Strlen-Aufruf in der Bedingung geschrieben, was zu einer 100-fachen Geschwindigkeitsverschlechterung unter Windows mit Cygwins GCC (oder ähnlichem) führte. Grundsätzlich, wenn es jedes Mal ausgewertet wird und der Wert sich nicht ändert ... traue dem Compiler nicht, es herauszuziehen; Verschiebe es selbst.

    
Malaxeur 19.09.2009 01:39
quelle
1

Zwei, die ich gesehen habe:

  • Verwenden Sie den kleinsten möglichen Datentyp. (Float anstelle von Double, Short anstelle von Int.)
  • Verwenden Sie Arrays anstelle von Listen.
Bill the Lizard 19.09.2009 01:33
quelle
1

Inlining Funktionen / Methoden vorzeitig entweder durch einen Compiler-Hinweis oder Kopieren und Einfügen-Code.

    
carl 19.09.2009 01:39
quelle
1

Es ist schwierig, weil einige offensichtliche Optimierungen nicht ohne Programmierintervention gemacht werden, obwohl die Dinge immer besser werden.

x ++ für benutzerdefinierte Klassen in C ++ ist möglicherweise langsamer. Für Primitive, nicht so sehr.

C ++ Compiler schlagen routinemäßig fehl , um je nach Typ mehrere konstante Additionen, Divisionen usw. zu falten. p>

Die Reihenfolge der mehrdimensionalen Array-Scans wird wichtig, wenn das gesamte Array zu groß ist, um in den L1-Cache zu passen.

    
Jonathan Graehl 19.09.2009 01:39
quelle
0
  • Viele Programmierer in meiner Firma werden sehr schnell auf Zeiger-Mathe (C ++) umschalten, anstatt Verweise, Array-Indexoperatoren usw. zu verwenden.
  • Verwendung von const-Zeigern / Referenzen in Funktionsargumenten für kleine Datentypen, bei denen eine Kopie möglicherweise genauso schnell war.
David Rutten 19.09.2009 01:35
quelle
0

Ein sehr häufig:

  • Vermeiden Sie das Erstellen von Funktionen, da Funktionsaufrufe sehr kostspielig sein sollten

Eins, das üblich war (aber hoffentlich nicht mehr, da es viele andere Probleme aufwirft und mit OO-Programmierung sinnlos wurde):

  • verwenden Sie globale Variablen, anstatt konstante Parameter an Funktionen zu übergeben

Zwei, von denen ich vermute, dass sie das genaue Gegenteil des Ziels erreichen (macht die Compiler-Aufgabe schwieriger und erreicht weniger Optimierung):

  • bestehende Variablen wiederverwenden
  • Vermeiden Sie die Erstellung lokaler Variablen in Schleifenblöcken

Einige benutze ich manchmal (wie ich Beweise in der Vergangenheit hatte Compiler nicht so gut mit manchmal große Leistung Vorteile ... ist es immer noch so?)

  • Verwenden Sie static, um den Export lokaler Funktionen außerhalb von Modul
  • zu vermeiden
  • Umgehungslinker durch direkte Aufnahme der Quelldatei (#include "source.c").

Einige alte Klassiker:

  • implizite Vergleiche mit 0 oder NULL ( while (*x++) und so)
kriss 19.09.2009 01:58
quelle
0

x ++ versus ++ x ist kein Optimierungsproblem, sondern ein Problem mit der Klarheit der Absicht. Möchten Sie den alten Wert von x tatsächlich verwenden? (Hinweis: normalerweise nicht.) Wenn Sie das tun, fahren Sie fort und verwenden Sie x ++, aber geben Sie andernfalls dem Wartungsprogrammierer an, der in zwei Jahren darauf achtet, dass dies nur ein Inkrement ist, nicht mehr.

Genau wie Sie etwas ohne const nicht deklarieren würden, wenn es sich nie semantisch ändern sollte, sollten Sie Ihre Pre / Post-Inkremente verwenden, um Ihre Absicht zu zeigen.

    
Bill 19.09.2009 02:03
quelle
0

In for-Schleifen deklarieren keine Variablen.

%Vor%

Ich habe festgestellt, dass dies in einigen Sprachen besser ist, da es einige Probleme zu reduzieren scheint:

%Vor%     
James Black 19.09.2009 02:34
quelle
0

Es gibt eine Tendenz, sich auf kleine Dinge zu konzentrieren und darüber zu urteilen, während das große Bild fehlt. Vieles, wie große Software entworfen wird, hat eine implizite Logik der Performance und ist daher meiner Meinung nach eine vorzeitige Optimierung. Beispiele:

  • Sammlungen mit Hash-Codierung. Hash-Codierung kann O (1) Verhalten (eine Performance-Grundregel) haben, aber für Sammlungen mit vernünftiger Größe kann viel Overkill sein.

  • Benachrichtigungs-Programmierung. Es ist schwierig, Daten vollständig denormalisiert zu halten, daher müssen Programmierer eine Möglichkeit haben, mit der denormalisierten Struktur (die inkonsistente Zustände aufweist) umzugehen. Die Methode, die normalerweise verwendet wird, hat "Eigenschaften" mit Getter und Setter, die dazu ermutigt werden, Änderungen sofort in der gesamten Datenstruktur zu propagieren, um sie konsistent zu halten. Die Begründung ist, dass dies bessere Leistung im Vergleich zu einer Strategie, eine gewisse Menge an Inkonsistenz zu tolerieren und separat damit umzugehen. Wenn die Leistungsoptimierung durchgeführt wird, zeigt sich, dass diese Versuche, die Konsistenz zu erhalten, dazu führen, dass als Reaktion auf kleine Änderungen große zusätzliche Arbeit geleistet wird.

  • Zu viele Klassen und zu viele Datenstrukturen. Oft ist das Grundprinzip für eine Klasse und das Erzeugen von Objekten, dass es ein Konzept im Kopf des Programmierers war, wenn die gleiche Information möglicherweise leicht durch eine Art von O (N) -Sweep aus anderen Daten extrahiert werden konnte. Wenn die Leute O (N) denken, denken sie an eine schlechte Leistung (Schrecken). Daher wird die Leistung zu einer impliziten vorzeitigen Rechtfertigung für die zusätzliche Datenstruktur.

  • Iteratoren. Leute werden ermutigt, Standard-Auflistungsklassen mit Iteratoren zu verwenden, und ihnen wird gesagt, dass der Compiler die Iteratoren optimieren wird (d. H. Sie werden eine gute Leistung haben). Dann, wenn sie in Low-Level-Schleifen verwendet werden, raten Sie was? Durchlaufen Sie den Code auf der Assembly-Ebene und durchlaufen Sie alle Arten von Allgemeinheit, die kein Compiler jemals verstehen könnte. In inneren Schleifen ohne Funktionsaufrufe sind Iteratoren suspekt.

In der Arbeit, die ich im Performance-Tuning gemacht habe, muss ich oft ein Rattennest mit überschüssiger Datenstruktur aus dem Weg räumen, was massive Leistungsprobleme verursacht. Und der häufigste Grund für die überschüssige Datenstruktur ist "Leistung".

Hier ist ein kleines Beispiel.

HINZUGEFÜGT: Es gibt andere Probleme mit falscher Vorhersage, die zu Leistungsproblemen führen können, z. B. übermäßige Globalisierung. Das Extrahieren von Strings aus Ressourcen beim App-Start ist nicht billig.

    
Mike Dunlavey 23.05.2017 12:11
quelle
0

Einige der häufigsten, die ich finde (einige spezifisch in der .NET-Welt):

  1. Verwendung von kleineren Integer-Typen anstelle von Int32 .

  2. ++i verwendet anstelle von i++

  3. Verwenden von StringBuilder class, um eine kleine Anzahl von Zeichenfolgen zu erstellen

  4. for anstelle von foreach

  5. eine allgemeine Abneigung gegen Typ-Validierung und Casting, wie:

    %Vor%
  6. Craze für O (1) Datenstrukturen und nicht wissen, was O (1) ist. Eine HashTable wirkt sich nachteilig aus, wenn die Eingabe klein ist.

  7. Aversion gegen try-catch

  8. macht Methoden und Variablen basierend auf Leistung und nicht auf Absicht statisch.

  9. Vermeiden Sie geschlossene Variablen

    %Vor%
  10. Verwenden von char anstelle von string während der String-Verkettung , wie:

    %Vor%
  11. SELECT COUNT(1) FROM table anstelle von SELECT COUNT(*) FROM table

(Die fettgedruckten sind diejenigen, die "negative" Auswirkungen haben, und kursiv sind das "kann")

Das heißt, Punkt 5 ist etwas, was ich selbst mache (oder nicht, um genau zu sein), ich liebe das as Optimierung für Referenztypen ..:)

    
nawfal 17.04.2013 06:58
quelle