Code-Optimierung: Der optimale Weg zur Optimierung

Veröffentlicht: 2022-03-11

Die Leistungsoptimierung ist eine der größten Bedrohungen für Ihren Code.

Sie denken vielleicht, nicht noch einer dieser Leute . Ich verstehe. Optimierung jeglicher Art sollte eindeutig eine gute Sache sein, nach ihrer Etymologie zu urteilen, also wollen Sie natürlich gut darin sein.

Nicht nur, um sich als besserer Entwickler von der Masse abzuheben. Nicht nur, um nicht „Dan“ bei The Daily WTF zu sein, sondern weil Sie glauben, dass Code-Optimierung das Richtige ist. Sie sind stolz auf Ihre Arbeit.

Computerhardware wird immer schneller und Software einfacher zu erstellen, aber was immer Sie einfach tun wollen, es dauert immer länger als das letzte Mal. Sie schütteln den Kopf über dieses Phänomen (übrigens als Wirthsches Gesetz bekannt) und beschließen, sich diesem Trend zu widersetzen.

Das ist edel von Ihnen, aber hören Sie auf.

Hör einfach auf!

Sie sind in größter Gefahr, Ihre eigenen Ziele zu vereiteln, egal wie erfahren Sie im Programmieren sind.

Wieso das? Lassen Sie uns zurückgehen.

Zunächst einmal, was ist Code-Optimierung?

Wenn wir es definieren, gehen wir oft davon aus, dass wir wollen, dass der Code besser funktioniert . Wir sagen, dass Code-Optimierung das Schreiben oder Umschreiben von Code ist, damit ein Programm den geringstmöglichen Arbeitsspeicher oder Speicherplatz verwendet, seine CPU-Zeit oder Netzwerkbandbreite minimiert oder zusätzliche Kerne optimal nutzt.

In der Praxis verwenden wir manchmal eine andere Definition: Weniger Code schreiben.

Aber der präventiv knallharte Code, den Sie mit diesem Ziel schreiben, ist noch wahrscheinlicher, jemandem ein Dorn im Auge zu werden. Wessen? Die nächste unglückliche Person, die Ihren Code verstehen muss, was vielleicht sogar Sie selbst sind. Und jemand, der klug und fähig ist, wie Sie, kann Selbstsabotage vermeiden: Halten Sie Ihre Ziele edel, aber bewerten Sie Ihre Mittel neu, obwohl sie zweifellos intuitiv zu sein scheinen.

Code Golf: +197 %, Leistung: -398 %, Einfachheit: -9999 %

Code-Optimierung ist also ein etwas schwammiger Begriff. Das ist, bevor wir auch nur einige der anderen Möglichkeiten in Betracht ziehen, wie man Code optimieren kann, was wir weiter unten tun werden.

Beginnen wir damit, auf die Ratschläge der Weisen zu hören, während wir gemeinsam Jacksons berühmte Code-Optimierungsregeln erkunden:

  1. Tu es nicht.
  2. (Nur für Experten!) Tun Sie es noch nicht .

1. Tun Sie es nicht: Perfektionismus kanalisieren

Ich beginne mit einem ziemlich peinlich extremen Beispiel aus einer Zeit, als ich vor langer Zeit gerade meine Füße in der wunderbaren SQL-Welt von „Iss-deinen-Kuchen-und-eat-it-too“ nass wurde. Das Problem war, dass ich dann auf den Kuchen getreten bin und ihn nicht mehr essen wollte, weil er nass war und nach Füßen zu riechen begann.

Ich habe gerade meine Füße in der wunderbaren Welt von SQL zum Aufessen und Aufessen nass gemacht. Das Problem war, dass ich dann auf den Kuchen getreten bin…

Warte ab. Lassen Sie mich aus diesem Autowrack einer Metapher herauskommen, die ich gerade gemacht und erklärt habe.

Ich arbeitete an Forschung und Entwicklung für eine Intranet-App, von der ich hoffte, dass sie eines Tages zu einem vollständig integrierten Managementsystem für das kleine Unternehmen werden würde, in dem ich arbeitete. Es würde alles für sie nachverfolgen, und im Gegensatz zu ihrem damals aktuellen System würden ihre Daten niemals verloren gehen, da es von einem RDBMS unterstützt würde, nicht von dem flockigen, selbst entwickelten Flat-File-Ding, das andere Entwickler verwendet hatten. Ich wollte von Anfang an alles so smart wie möglich gestalten, denn ich hatte ein unbeschriebenes Blatt. Ideen für dieses System explodierten wie ein Feuerwerk in meinem Kopf, und ich begann, Tabellen zu entwerfen – Kontakte und ihre vielen kontextuellen Variationen für ein CRM, Buchhaltungsmodule, Inventar, Einkauf, CMS und Projektmanagement, die ich bald dogfooden würde.

Dass alles zum Erliegen kam, entwicklungs- und leistungsmäßig, wegen … Sie haben es erraten, Optimierung.

Ich sah, dass Objekte (dargestellt als Tabellenzeilen) in der realen Welt viele verschiedene Beziehungen zueinander haben könnten und dass wir davon profitieren könnten, diese Beziehungen zu verfolgen: Wir würden mehr Informationen behalten und könnten schließlich die Geschäftsanalyse überall automatisieren. Da ich dies als technisches Problem betrachtete, tat ich etwas, das wie eine Optimierung der Flexibilität des Systems aussah.

An diesem Punkt ist es wichtig, auf Ihr Gesicht zu achten, da ich nicht verantwortlich gemacht werde, wenn Ihre Handfläche weh tut. Bereit? Ich habe zwei Tabellen erstellt: relationship und eine, auf die es einen Fremdschlüsselverweis hatte, relationship_type . relationship könnte sich auf zwei beliebige Zeilen irgendwo in der gesamten Datenbank beziehen und die Art der Beziehung zwischen ihnen beschreiben.

Datenbanktabellen: Mitarbeiter, Firma, Beziehung, Beziehungstyp

Oh Mann. Ich hatte gerade diese Flexibilität so verdammt optimiert.

Eigentlich zu viel. Jetzt hatte ich ein neues Problem: Ein gegebener relationship_type würde natürlich keinen Sinn zwischen jeder gegebenen Kombination von Zeilen machen. Es mag zwar sinnvoll sein, dass eine person eine employed by zu einem company hat, aber das kann semantisch niemals mit der Beziehung zwischen, sagen wir, zwei document s äquivalent sein.

OK, kein Problem. Wir fügen einfach zwei Spalten zu relationship_type hinzu, die angeben, auf welche Tabellen diese Beziehung angewendet werden könnte. (Bonuspunkte hier, wenn Sie vermuten, dass ich darüber nachgedacht habe, dies zu normalisieren, indem ich diese beiden Spalten in eine neue Tabelle verschoben habe, die auf relationship_type.id verweist, damit Beziehungen, die semantisch auf mehr als ein Tabellenpaar angewendet werden könnten, die Tabellennamen nicht dupliziert hätten. Wenn ich schließlich einen Tabellennamen ändern musste und vergaß, ihn in allen zutreffenden Zeilen zu aktualisieren, könnte dies zu einem Fehler führen!Rückblickend hätten Fehler zumindest Nahrung für die Spinnen geliefert, die meinen Schädel bewohnen.)

Datenbanktabellen: „relationship_type“ und „applyable_to“ und die gefalteten Daten der beiden Spalten „relationship_type“, dargestellt durch Pfeile

Zum Glück wurde ich in einem Hinweisstock-Sturm bewusstlos geschlagen, bevor ich diesen Weg zu weit gegangen bin. Als ich aufwachte, wurde mir klar, dass ich es geschafft hatte, die internen Fremdschlüssel-bezogenen Tabellen des RDBMS mehr oder weniger auf sich selbst neu zu implementieren. Normalerweise genieße ich Momente, die damit enden, dass ich die stolze Aussage „Ich bin so meta“ mache, aber das war leider keiner davon. Vergessen Sie die fehlende Skalierung – die horrende Aufblähung dieses Designs machte das Back-End meiner immer noch einfachen App, deren DB noch kaum mit Testdaten gefüllt war, fast unbrauchbar.

Benutze die Fremdschlüssel, Luke!

Lassen Sie uns für eine Sekunde zurückgehen und einen Blick auf zwei der vielen Metriken werfen, die hier eine Rolle spielen. Eine davon ist Flexibilität, die mein erklärtes Ziel war. In diesem Fall war meine Optimierung, die architektonischer Natur ist, nicht einmal verfrüht:

Code-Optimierungsschritte: Die Architektur ist der erste Teil eines zu optimierenden Programms

(Wir werden in meinem kürzlich veröffentlichten Artikel, Wie man den Fluch der vorzeitigen Optimierung vermeidet, näher darauf eingehen.) Nichtsdestotrotz ist meine Lösung spektakulär gescheitert, weil sie viel zu flexibel war. Die andere Metrik, die Skalierbarkeit, war eine, an die ich noch gar nicht gedacht hatte, die ich aber mindestens genauso spektakulär durch Kollateralschäden zerstören konnte.

Das ist richtig, "Oh."

Doppelte Gesichtspalme, wenn eine Gesichtspalme nicht schneidet

Dies war eine eindrucksvolle Lektion für mich, wie die Optimierung völlig schief gehen kann. Mein Perfektionismus implodierte vollständig: Meine Cleverness hatte mich dazu gebracht, eine der objektiv uncleversten Lösungen hervorzubringen, die ich je gemacht habe.

Optimieren Sie Ihre Gewohnheiten, nicht Ihren Code

Wenn Sie sich dabei ertappen, dass Sie dazu neigen, zu refaktorisieren, bevor Sie überhaupt einen funktionierenden Prototyp und eine Testsuite haben, um ihre Korrektheit zu beweisen, überlegen Sie, wo Sie diesen Impuls sonst kanalisieren können. Sudoku und Mensa sind großartig, aber vielleicht wäre etwas, das Ihrem Projekt direkt zugute kommt, besser:

  1. Sicherheit
  2. Laufzeitstabilität
  3. Klarheit und Stil
  4. Codierungseffizienz
  5. Wirksamkeit testen
  6. Profilierung
  7. Ihr Werkzeugkasten/DE
  8. DRY (Wiederhole dich nicht)

Aber Vorsicht: Die Optimierung eines bestimmten von ihnen geht zu Lasten anderer. Das geht zumindest auf Kosten der Zeit.

Hier ist leicht zu erkennen, wie viel Kunst im Erstellen von Code steckt. Für jeden der oben genannten Punkte kann ich Ihnen Geschichten darüber erzählen, wie zu viel oder zu wenig davon als die falsche Wahl angesehen wurde. Wer hier mitdenkt, ist ebenfalls ein wichtiger Bestandteil des Kontextes.

Zum Beispiel in Bezug auf DRY: Bei einem Job, den ich hatte, habe ich eine Codebasis geerbt, die zu mindestens 80 % aus redundanten Anweisungen bestand, weil ihr Autor anscheinend nicht wusste, wie und wann er eine Funktion schreiben sollte. Die anderen 20 % des Codes waren verwirrend selbstähnlich.

Ich wurde beauftragt, ein paar Funktionen hinzuzufügen. Eine solche Funktion müsste im gesamten zu implementierenden Code wiederholt werden, und jeder zukünftige Code müsste sorgfältig kopiert werden, um die neue Funktion nutzen zu können.

Offensichtlich musste es nur für meine eigene geistige Gesundheit (hoher Wert) und für alle zukünftigen Entwickler umgestaltet werden. Aber da ich neu in der Codebasis war, habe ich zuerst Tests geschrieben, um sicherzustellen, dass mein Refactoring keine Regressionen einführte. Tatsächlich haben sie genau das getan: Ich habe unterwegs zwei Bugs entdeckt, die ich unter all dem Kauderwelsch, das das Skript produziert hat, nicht bemerkt hätte.

Am Ende dachte ich, ich hätte es ziemlich gut gemacht. Nach dem Refactoring beeindruckte ich meinen Chef damit, dass ich das, was als schwierig galt, mit ein paar einfachen Codezeilen implementiert hatte; außerdem war der Code insgesamt um eine Größenordnung performanter. Aber es dauerte nicht lange, bis mir derselbe Chef sagte, ich sei zu langsam gewesen und das Projekt hätte schon fertig sein sollen. Übersetzung: Codierungseffizienz hatte eine höhere Priorität.

Achtung: Die Optimierung eines bestimmten [Aspekts] geht zu Lasten anderer. Das geht zumindest auf Kosten der Zeit.

Ich denke immer noch, dass ich dort den richtigen Kurs eingeschlagen habe, auch wenn die Code-Optimierung damals von meinem Chef nicht direkt geschätzt wurde. Ohne das Refactoring und die Tests hätte es meiner Meinung nach länger gedauert, um tatsächlich richtig zu werden – dh die Konzentration auf die Codierungsgeschwindigkeit hätte es tatsächlich vereitelt. (Hey, das ist unser Thema!)

Vergleichen Sie dies mit etwas Arbeit, die ich an einem kleinen Nebenprojekt von mir gemacht habe. In dem Projekt habe ich eine neue Template-Engine ausprobiert und wollte mir von Anfang an gute Gewohnheiten aneignen, obwohl das Ausprobieren der neuen Template-Engine nicht das Endziel des Projekts war.

Sobald ich bemerkte, dass einige Blöcke, die ich hinzugefügt hatte, einander sehr ähnlich waren und außerdem jeder Block dreimal auf dieselbe Variable verweisen musste, ging die DRY-Glocke in meinem Kopf los und ich machte mich auf die Suche nach dem richtigen Weg, das zu tun, was ich mit dieser Template-Engine versucht habe.

Es stellte sich nach ein paar Stunden vergeblicher Fehlersuche heraus, dass dies mit der Template-Engine derzeit nicht so möglich ist, wie ich es mir vorgestellt habe. Es gab nicht nur keine perfekte DRY-Lösung; es gab überhaupt keine DRY-Lösung!

Beim Versuch, diesen einen Wert von mir zu optimieren, habe ich meine Programmiereffizienz und mein Glück völlig entgleist, weil dieser Umweg mein Projekt den Fortschritt gekostet hat, den ich an diesem Tag hätte haben können.

Selbst dann lag ich völlig falsch? Manchmal lohnt es sich, gerade in einem neuen Tech-Kontext, ein bisschen zu investieren, Best Practices früher statt später kennenzulernen. Weniger Code umzuschreiben und schlechte Angewohnheiten rückgängig zu machen, richtig?

Nein, ich denke, es war unklug, auch nur nach einer Möglichkeit zu suchen, die Wiederholungen in meinem Code zu reduzieren – im krassen Gegensatz zu meiner Haltung in der vorherigen Anekdote. Der Grund dafür ist, dass der Kontext alles ist: Ich habe ein neues Stück Technik in einem kleinen Spielprojekt erkundet und mich nicht auf lange Sicht eingelebt. Ein paar zusätzliche Zeilen und Wiederholungen hätten niemandem geschadet, aber der Fokusverlust hat mir und meinem Projekt geschadet.

Warten Sie, die Suche nach Best Practices kann also eine schlechte Angewohnheit sein? Manchmal. Wenn mein Hauptziel das Erlernen der neuen Engine oder das Lernen im Allgemeinen gewesen wäre, dann wäre das gut investierte Zeit gewesen: Basteln, Grenzen finden, nicht zusammenhängende Funktionen und Fallstricke durch Recherche entdecken. Aber ich hatte vergessen, dass dies nicht mein Hauptziel war, und es kostete mich.

Es ist eine Kunst, wie ich sagte. Und die Entwicklung dieser Kunst profitiert von der Erinnerung: Tu es nicht . Es bringt Sie zumindest dazu, darüber nachzudenken, welche Werte bei Ihrer Arbeit eine Rolle spielen und welche Ihnen in Ihrem Kontext am wichtigsten sind.

Was ist mit dieser zweiten Regel? Wann können wir eigentlich optimieren?

2. Tun Sie es noch nicht : Jemand hat dies bereits getan

OK, ob von Ihnen oder jemand anderem, Sie stellen fest, dass Ihre Architektur bereits festgelegt wurde, die Datenflüsse durchdacht und dokumentiert wurden und es an der Zeit ist, zu programmieren.

Gehen wir noch einen Schritt weiter: Mach es noch nicht .

Das mag nach voreiliger Optimierung riechen, ist aber eine wichtige Ausnahme. Warum? Um das gefürchtete NIHS- oder „Not Invented Here“-Syndrom zu vermeiden – vorausgesetzt, Ihre Prioritäten umfassen die Codeleistung und die Minimierung der Entwicklungszeit. Wenn nicht, wenn Ihre Ziele vollständig lernorientiert sind, können Sie diesen nächsten Abschnitt überspringen.

Es ist zwar möglich, dass Menschen aus purer Hybris das quadratische Rad neu erfinden, aber ich glaube, dass ehrliche, demütige Leute wie Sie und ich diesen Fehler machen können, nur weil sie nicht alle uns zur Verfügung stehenden Optionen kennen. Es ist sicherlich eine Menge Arbeit, jede Option jeder API und jedes Tools in Ihrem Stack zu kennen und den Überblick zu behalten, während sie wachsen und sich weiterentwickeln.

Aber diese Zeit zu investieren, macht Sie zu einem Experten und verhindert, dass Sie die zillionste Person auf CodeSOD sind, die für die Spur der Verwüstung verflucht und verspottet wird, die ihre faszinierende Interpretation von Datums-Zeit-Rechnern oder String-Manipulatoren hinterlassen hat.

(Ein guter Kontrapunkt zu diesem allgemeinen Muster ist die alte Java- Calendar -API, die jedoch inzwischen behoben wurde.)

Überprüfen Sie Ihre Standardbibliothek, überprüfen Sie das Ökosystem Ihres Frameworks, suchen Sie nach FOSS, das Ihr Problem bereits löst

Wahrscheinlich haben die Konzepte, mit denen Sie es zu tun haben, ziemlich standardmäßige und bekannte Namen, sodass Ihnen eine schnelle Internetsuche eine Menge Zeit sparen wird.

Als Beispiel bereitete ich kürzlich eine Analyse von KI-Strategien für ein Brettspiel vor. Eines Morgens wachte ich auf und erkannte, dass die Analyse, die ich plante, um Größenordnungen effizienter durchgeführt werden könnte, wenn ich einfach ein bestimmtes Kombinatorik-Konzept verwenden würde, an das ich mich erinnerte. Da ich zu diesem Zeitpunkt nicht daran interessiert war, den Algorithmus für dieses Konzept selbst herauszufinden, war ich bereits voraus, indem ich den richtigen Namen für die Suche kannte. Ich stellte jedoch fest, dass ich es nach etwa 50 Minuten Recherche und Ausprobieren von vorläufigem Code nicht geschafft hatte, den halbfertigen Pseudocode, den ich gefunden hatte, in eine korrekte Implementierung umzuwandeln. (Können Sie glauben, dass es da draußen einen Blogbeitrag gibt, in dem der Autor eine falsche Algorithmusausgabe annimmt, den Algorithmus falsch implementiert, um den Annahmen zu entsprechen, Kommentatoren darauf hinweisen, und dann Jahre später immer noch nicht behoben ist?) An diesem Punkt mein Morgentee eingetreten, und ich suchte nach [name of concept] [my programming language] . 30 Sekunden später hatte ich nachweislich korrekten Code von GitHub und ging zu dem über, was ich eigentlich tun wollte. Konkret zu werden und die Sprache einzubeziehen, anstatt anzunehmen, dass ich sie selbst umsetzen müsste, bedeutete alles.

Zeit, Ihre Datenstruktur zu entwerfen und Ihren Algorithmus zu implementieren

… noch einmal: Spielen Sie kein Code-Golf. Priorisieren Sie Korrektheit und Klarheit in realen Projekten.

Zeitaufwand: 10 Stunden, Ausführungszeit: +25 %, Speicherverbrauch: +3 %, Verwirrung: 100 %

OK, Sie haben also nachgesehen, und es gibt nichts, was Ihr Problem bereits in Ihre Toolchain integriert oder im Internet frei lizenziert hat. Sie rollen Ihre eigenen aus.

Kein Problem. Der Rat ist einfach, in dieser Reihenfolge:

  1. Gestalten Sie es so, dass es einem unerfahrenen Programmierer einfach zu erklären ist.
  2. Schreiben Sie einen Test, der den Erwartungen entspricht, die durch dieses Design erzeugt werden.
  3. Schreiben Sie Ihren Code so, dass ein unerfahrener Programmierer das Design leicht daraus entnehmen kann.

Einfach, aber vielleicht schwer zu befolgen. Hier kommen Programmiergewohnheiten und Codegerüche, Kunst, Handwerk und Eleganz ins Spiel. Es gibt offensichtlich einen technischen Aspekt bei dem, was Sie an diesem Punkt tun, aber spielen Sie noch einmal kein Code-Golf. Priorisieren Sie Korrektheit und Klarheit in realen Projekten.

Wenn Sie Videos mögen, hier ist eines von jemandem, der die obigen Schritte mehr oder weniger befolgt. Für die Video-Aversen fasse ich zusammen: Es ist ein Algorithmus-Codierungstest bei einem Google-Vorstellungsgespräch. Der Befragte gestaltet den Algorithmus zunächst so, dass er leicht zu kommunizieren ist. Bevor Sie Code schreiben, gibt es Beispiele für die Ausgabe, die von einem funktionierenden Design erwartet wird. Dann folgt natürlich der Code.

Was die Tests selbst betrifft, weiß ich, dass die testgetriebene Entwicklung in manchen Kreisen umstritten sein kann. Ich denke, ein Grund dafür ist, dass es übertrieben werden kann, religiös verfolgt bis zu dem Punkt, an dem Entwicklungszeit geopfert wird. (Wieder schießen wir uns selbst in den Fuß, indem wir von Anfang an versuchen, auch nur eine Variable zu sehr zu optimieren.) Sogar Kent Beck treibt TDD nicht so extrem, und er erfand extreme Programmierung und schrieb das Buch über TDD. Beginnen Sie also mit etwas Einfachem, um sicherzustellen, dass Ihre Ausgabe korrekt ist. Schließlich würden Sie das nach dem Codieren sowieso manuell tun, oder? (Ich entschuldige mich, wenn Sie so ein Rockstar-Programmierer sind, dass Sie Ihren Code nicht einmal ausführen, nachdem Sie ihn zum ersten Mal geschrieben haben. In diesem Fall würden Sie vielleicht erwägen, den zukünftigen Betreuern Ihres Codes einen Test zu überlassen, nur damit Sie wissen, dass sie es nicht tun werden Brechen Sie Ihre großartige Implementierung ab.) Anstatt also einen manuellen, visuellen Unterschied zu machen, lassen Sie mit einem Test bereits den Computer diese Arbeit für Sie erledigen.

Vermeiden Sie während des eher mechanischen Prozesses der Implementierung Ihrer Algorithmen und Datenstrukturen zeilenweise Optimierungen und denken Sie nicht einmal daran , eine benutzerdefinierte externe Sprache auf niedrigerer Ebene zu verwenden (Assembly, wenn Sie in C codieren, C, wenn Sie codieren in Perl usw.) an dieser Stelle. Der Grund ist einfach: Wenn Ihr Algorithmus vollständig ersetzt wird – und Sie erfahren erst später im Prozess, ob dies erforderlich ist – dann werden Ihre Low-Level-Optimierungsbemühungen am Ende keine Wirkung haben.

Ein ECMAScript-Beispiel

Auf der ausgezeichneten Community-Code-Review-Site exercism.io habe ich kürzlich eine Übung gefunden, die explizit vorschlug, entweder eine Optimierung für Deduplizierung oder für Klarheit zu versuchen. Ich habe die Deduplizierung optimiert, nur um zu zeigen, wie lächerlich Dinge werden können, wenn Sie DRY – eine ansonsten nützliche Codierungs-Denkweise, wie ich oben erwähnt habe – zu weit treiben. So sah mein Code aus:

 const zeroPhrase = "No more"; const wallPhrase = " on the wall"; const standardizeNumber = number => { if (number === 0) { return zeroPhrase; } return '' + number; } const bottlePhrase = number => { const possibleS = (number === 1) ? '' : 's'; return standardizeNumber(number) + " bottle" + possibleS + " of beer"; } export default class Beer { static verse(number) { const nextNumber = (number === 0) ? 99 : (number - 1); const thisBottlePhrase = bottlePhrase(number); const nextBottlePhrase = bottlePhrase(nextNumber); let phrase = thisBottlePhrase + wallPhrase + ", " + thisBottlePhrase.toLowerCase() + ".\n"; if (number === 0) { phrase += "Go to the store and buy some more"; } else { const bottleReference = (number === 1) ? "it" : "one"; phrase += "Take " + bottleReference + " down and pass it around"; } return phrase + ", " + nextBottlePhrase.toLowerCase() + wallPhrase + ".\n"; } static sing(start = 99, end = 0) { return Array.from(Array(start - end + 1).keys()).map(offset => { return this.verse(start - offset); }).join('\n'); } }

Da gibt es kaum eine Verdoppelung der Saiten! Indem ich es so schreibe, habe ich manuell eine Form der Textkomprimierung für das Bierlied (aber nur für das Bierlied) implementiert. Was war der Nutzen genau? Nun, sagen wir mal, du willst darüber singen, Bier aus Dosen statt aus Flaschen zu trinken. Ich könnte dies erreichen, indem ich eine einzelne Instanz von bottle in can umändere.

Hübsch!

…rechts?

Nö, denn dann brechen alle Tests ab. OK, das ist einfach zu beheben: Wir suchen einfach nach bottle in der Unit-Test-Spezifikation und ersetzen sie. Und das ist genau so einfach, wie das mit dem Code selbst zu tun, und birgt die gleichen Risiken, Dinge unbeabsichtigt zu beschädigen.

In der Zwischenzeit werden meine Variablen später seltsam benannt, wobei Dinge wie bottlePhrase überhaupt nichts mit Flaschen zu tun haben. Die einzige Möglichkeit, dies zu vermeiden, besteht darin, genau vorhergesehen zu haben, welche Art von Änderung vorgenommen werden würde, und in meinen Variablennamen einen allgemeineren Begriff wie vessel oder container anstelle von bottle zu verwenden.

Die Klugheit, auf diese Weise zukunftssicher zu sein, ist ziemlich fraglich. Wie stehen die Chancen, dass Sie überhaupt etwas ändern wollen? Und wenn ja, wird das, was Sie ändern, so bequem funktionieren? Was ist im Beispiel von bottlePhrase , wenn Sie in eine Sprache lokalisieren möchten, die mehr als zwei Pluralformen hat? Das ist richtig, Refactoring-Zeit, und der Code sieht danach möglicherweise noch schlechter aus.

Aber wenn sich Ihre Anforderungen ändern und Sie nicht nur versuchen, sie vorherzusehen, dann ist es vielleicht an der Zeit, umzugestalten. Oder vielleicht können Sie es noch verschieben: Wie viele Schiffstypen oder Lokalisierungen werden Sie realistischerweise hinzufügen? Wie auch immer, wenn Sie Ihre Deduplizierung mit Klarheit ausbalancieren müssen, lohnt es sich auf jeden Fall, sich diese Demonstration von Katrina Owen anzusehen.

Zurück zu meinem eigenen hässlichen Beispiel: Unnötig zu sagen, dass die Vorteile der Deduplizierung hier nicht einmal so sehr realisiert werden. Was hat es in der Zwischenzeit gekostet?

Abgesehen davon, dass das Schreiben überhaupt länger dauert, ist es jetzt ein bisschen weniger trivial zu lesen, zu debuggen und zu warten. Stellen Sie sich das Lesbarkeitsniveau mit einer moderaten Menge an erlaubter Duplizierung vor. Zum Beispiel, jede der vier Versvariationen buchstabieren zu lassen.

Aber wir haben immer noch nicht optimiert!

Jetzt, wo Ihr Algorithmus implementiert ist und Sie bewiesen haben, dass seine Ausgabe korrekt ist, herzlichen Glückwunsch! Sie haben eine Basis!

Endlich ist es an der Zeit … zu optimieren, richtig? Nö, noch Mach es noch nicht . Es ist an der Zeit, Ihre Basislinie zu nehmen und einen schönen Benchmark zu erstellen. Legen Sie diesbezüglich einen Schwellenwert für Ihre Erwartungen fest und fügen Sie ihn in Ihre Testsuite ein. Wenn dann etwas diesen Code plötzlich langsamer macht – selbst wenn er noch funktioniert –, werden Sie es wissen, bevor er die Tür verlässt.

Warten Sie immer noch mit der Optimierung, bis Sie ein ganzes Stück der relevanten Benutzererfahrung implementiert haben. Bis zu diesem Punkt zielen Sie möglicherweise auf einen völlig anderen Teil des Codes ab, als Sie benötigen.

Stellen Sie Ihre App (oder Komponente) fertig, falls Sie dies noch nicht getan haben, und legen Sie dabei alle Ihre algorithmischen Benchmark-Baselines fest.

Sobald dies erledigt ist, ist dies ein guter Zeitpunkt, um End-to-End-Tests zu erstellen und zu bewerten, die die häufigsten realen Nutzungsszenarien Ihres Systems abdecken.

Vielleicht findest du heraus, dass alles in Ordnung ist.

Oder vielleicht haben Sie festgestellt, dass etwas in seinem realen Kontext zu langsam ist oder zu viel Speicher beansprucht.

OK, jetzt können Sie optimieren

Es gibt nur einen Weg, objektiv zu sein. Es ist an der Zeit, Flame-Diagramme und andere Profiling-Tools herauszubringen. Erfahrene Ingenieure können öfter besser raten als Anfänger, aber darum geht es nicht: Der einzige Weg, es sicher zu wissen, ist ein Profil. Dies ist immer das erste, was Sie tun müssen, wenn Sie den Code auf Leistung optimieren.

Sie können während eines bestimmten End-to-End-Tests ein Profil erstellen, um herauszufinden, was wirklich die größte Wirkung erzielen wird. (Und später, nach der Bereitstellung, ist die Überwachung von Nutzungsmustern eine großartige Möglichkeit, um auf dem Laufenden zu bleiben, welche Aspekte Ihres Systems in Zukunft am relevantesten zu messen sind.)

Beachten Sie, dass Sie nicht versuchen, den Profiler in seiner vollen Tiefe zu nutzen – Sie suchen im Allgemeinen mehr nach Profilerstellung auf Funktionsebene als nach Profilerstellung auf Anweisungsebene, da Ihr Ziel an dieser Stelle nur darin besteht, herauszufinden, welcher Algorithmus der Engpass ist .

Nachdem Sie nun die Profilerstellung verwendet haben, um den Engpass Ihres Systems zu identifizieren, können Sie jetzt tatsächlich versuchen, eine Optimierung vorzunehmen, in der Gewissheit, dass sich Ihre Optimierung lohnt. Sie können auch beweisen, wie effektiv (oder ineffektiv) Ihr Versuch war, dank dieser Basis-Benchmarks, die Sie unterwegs durchgeführt haben.

Allgemeine Techniken

Denken Sie zunächst daran, so lange wie möglich auf hohem Niveau zu bleiben:

Auf der Ebene des Gesamtalgorithmus ist eine Technik die Stärkereduktion. Wenn Sie Schleifen auf Formeln reduzieren, achten Sie jedoch darauf, Kommentare zu hinterlassen. Nicht jeder kennt oder erinnert sich an jede Kombinationsformel. Seien Sie auch vorsichtig bei der Verwendung von Mathematik: Manchmal ist das, was Sie für eine Reduzierung der Kraft halten, am Ende nicht so. Nehmen wir zum Beispiel an, dass x * (y + z) eine klare algorithmische Bedeutung hat. Wenn Ihr Gehirn irgendwann aus irgendeinem Grund darauf trainiert wurde, ähnliche Begriffe automatisch zu entgruppieren, könnten Sie versucht sein, dies als x * y + x * z umzuschreiben. Zum einen stellt dies eine Barriere zwischen den Leser und die klare algorithmische Bedeutung, die dort gewesen war. (Schlimmer noch, es ist jetzt aufgrund der erforderlichen zusätzlichen Multiplikationsoperation weniger effizient. Es ist, als hätte sich das Ausrollen von Schleifen gerade die Hose verkrustet.) In jedem Fall würde eine kurze Notiz über Ihre Absichten viel bewirken und Ihnen vielleicht sogar helfen, Ihre zu erkennen eigenen Fehler, bevor Sie ihn begehen.

Egal, ob Sie eine Formel verwenden oder einfach nur einen schleifenbasierten Algorithmus durch einen anderen schleifenbasierten Algorithmus ersetzen, Sie sind bereit, den Unterschied zu messen.

Aber vielleicht können Sie eine bessere Leistung erzielen, indem Sie einfach Ihre Datenstruktur ändern. Informieren Sie sich über die Leistungsunterschiede zwischen den verschiedenen Operationen, die Sie für die von Ihnen verwendete Struktur ausführen müssen, und über alle Alternativen. Vielleicht sieht ein Hash etwas chaotischer aus, um in Ihrem Kontext zu funktionieren, aber lohnt sich die überlegene Suchzeit gegenüber einem Array? Dies sind die Arten von Kompromissen, über die Sie entscheiden müssen.

Sie werden vielleicht feststellen, dass dies darauf hinausläuft, zu wissen, welche Algorithmen in Ihrem Namen ausgeführt werden, wenn Sie eine Komfortfunktion aufrufen. Also ist es am Ende wirklich dasselbe wie Kraftabbau. Und zu wissen, was die Bibliotheken Ihres Anbieters hinter den Kulissen tun, ist nicht nur für die Leistung von entscheidender Bedeutung, sondern auch, um unbeabsichtigte Fehler zu vermeiden.

Mikrooptimierungen

OK, die Funktionalität Ihres Systems ist fertig, aber aus UX-Sicht könnte die Leistung noch etwas weiter verfeinert werden. Angenommen, Sie haben oben alles getan, was Sie können, ist es an der Zeit, die Optimierungen in Betracht zu ziehen, die wir bisher die ganze Zeit vermieden haben. Bedenken Sie, dass dieses Optimierungsniveau immer noch ein Kompromiss gegen Klarheit und Wartbarkeit ist. Aber Sie haben entschieden, dass es an der Zeit ist, also fahren Sie mit der Profilerstellung auf Statement-Ebene fort, jetzt, wo Sie sich im Kontext des gesamten Systems befinden, wo es wirklich darauf ankommt.

Genau wie bei den von Ihnen verwendeten Bibliotheken wurden auf der Ebene Ihres Compilers oder Interpreters unzählige Engineering-Stunden zu Ihrem Vorteil investiert. (Schließlich sind Compiler-Optimierung und Code-Generierung große Themen für sich). Dies gilt sogar auf Prozessorebene. Der Versuch, Code zu optimieren, ohne zu wissen, was auf den untersten Ebenen passiert, ist wie der Gedanke, dass ein Allradantrieb bedeutet, dass Ihr Fahrzeug auch leichter anhalten kann.

Es ist schwierig, darüber hinaus gute allgemeine Ratschläge zu geben, da dies wirklich von Ihrem Tech-Stack abhängt und worauf Ihr Profiler zeigt. Aber weil Sie messen, sind Sie bereits in einer hervorragenden Position, um Hilfe zu bitten, wenn sich Lösungen nicht organisch und intuitiv aus dem Problemkontext ergeben. (Schlaf und Zeit, die Sie damit verbringen, über etwas anderes nachzudenken, können ebenfalls helfen.)

An diesem Punkt würde Jeff Atwood je nach Kontext und Skalierungsanforderungen wahrscheinlich vorschlagen, einfach Hardware hinzuzufügen, was billiger sein kann als Entwicklerzeit.

Vielleicht gehst du diesen Weg nicht. In diesem Fall kann es hilfreich sein, verschiedene Kategorien von Codeoptimierungstechniken zu untersuchen:

  • Caching
  • Bit-Hacks und solche, die für 64-Bit-Umgebungen spezifisch sind
  • Loop-Optimierung
  • Optimierung der Speicherhierarchie

Genauer:

  • Tipps zur Codeoptimierung in C und C++
  • Tipps zur Codeoptimierung in Java
  • Optimierung der CPU-Auslastung in .NET
  • ASP.NET-Webfarm-Caching
  • Tuning von SQL-Datenbanken oder Tuning von Microsoft SQL Server im Besonderen
  • Skalierung von Scala's Play! Rahmen
  • Erweiterte WordPress-Leistungsoptimierung
  • Code-Optimierung mit JavaScript-Prototypen und Scope-Ketten
  • Optimierung der React-Leistung
  • Effizienz der iOS-Animation
  • Tipps zur Android-Leistung

Auf jeden Fall habe ich noch ein paar Don'ts für dich:

Verwenden Sie eine Variable nicht für mehrere unterschiedliche Zwecke. In Bezug auf die Wartungsfreundlichkeit ist dies wie ein Auto ohne Öl zu fahren. Nur in den extremsten eingebetteten Situationen hat dies jemals Sinn gemacht, und selbst in diesen Fällen würde ich argumentieren, dass dies nicht mehr der Fall ist. Dies zu organisieren ist Aufgabe des Compilers. Machen Sie es selbst, verschieben Sie dann eine Codezeile, und Sie haben einen Fehler eingeführt. Ist Ihnen die Illusion, Erinnerungen zu retten, das wert?

Verwenden Sie keine Makros und Inline-Funktionen, ohne zu wissen warum. Ja, Funktionsaufruf-Overhead ist ein Kostenfaktor. Wenn Sie dies jedoch vermeiden, wird Ihr Code oft schwieriger zu debuggen und manchmal sogar langsamer. Diese Technik überall anzuwenden, nur weil es hin und wieder eine gute Idee ist, ist ein Beispiel für einen goldenen Hammer.

Schlaufen nicht von Hand aufrollen. Auch diese Form der Schleifenoptimierung ist fast immer besser durch einen automatisierten Prozess wie die Kompilierung optimiert, nicht durch Einbußen bei der Lesbarkeit Ihres Codes.

Die Ironie in den letzten beiden Codeoptimierungsbeispielen besteht darin, dass sie tatsächlich leistungsmindernd sein können. Da Sie Benchmarks durchführen, können Sie dies natürlich für Ihren speziellen Code beweisen oder widerlegen. Aber selbst wenn Sie eine Leistungsverbesserung sehen, kehren Sie zur Kunstseite zurück und prüfen Sie, ob der Gewinn den Verlust an Lesbarkeit und Wartbarkeit wert ist.

Es gehört Ihnen: Optimal optimierte Optimierung

Der Versuch einer Leistungsoptimierung kann von Vorteil sein. Meistens wird es jedoch sehr verfrüht durchgeführt, bringt eine Litanei schlimmer Nebenwirkungen mit sich und führt ironischerweise zu einer schlechteren Leistung. Ich hoffe, Sie haben ein erweitertes Verständnis für die Kunst und Wissenschaft der Optimierung und vor allem für ihren richtigen Kontext gewonnen.

Ich freue mich, wenn uns das dabei hilft, die Vorstellung vom Schreiben von perfektem Code von vornherein abzulegen und stattdessen korrekten Code zu schreiben. Wir müssen daran denken, von oben nach unten zu optimieren, nachzuweisen, wo die Engpässe liegen, und vor und nach der Behebung zu messen. Das ist die optimale, optimale Strategie zur Optimierung der Optimierung. Viel Glück.