Kapitel 1. Software-Effizienz ist wichtig
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Die Hauptaufgabe von Softwareentwicklern ist die kostengünstige Entwicklung von wartbarer und nützlicher Software.
Jon Louis Bentley, Writing Efficient Programs (Prentice Hall, 1982)
Auch nach 40 Jahren ist Jons Definition von Entwicklung ziemlich genau. Das ultimative Ziel eines jeden Ingenieurs ist es, ein nützliches Produkt zu entwickeln, das die Bedürfnisse der Nutzer über die gesamte Lebensdauer erfüllt. Leider ist sich heutzutage nicht jeder Entwickler über die Bedeutung der Softwarekosten im Klaren. Die Wahrheit kann brutal sein; zu sagen, dass der Entwicklungsprozess teuer sein kann, ist vielleicht eine Unterschätzung. Rockstar hat zum Beispiel 5 Jahre und 250 Ingenieure gebraucht, um das beliebte Videospiel Grand Theft Auto 5 zu entwickeln, das schätzungsweise 137,5 Millionen Dollar gekostet hat. Auf der anderen Seite musste Apple bis zur ersten Veröffentlichung von macOS im Jahr 2001 weit über 500 Millionen Dollar ausgeben, um ein brauchbares, kommerzielles Betriebssystem zu entwickeln.
Angesichts der hohen Kosten für die Softwareproduktion ist es wichtig, dass wir unsere Anstrengungen auf die Dinge konzentrieren, die am wichtigsten sind. Im Idealfall wollen wir keine Zeit und Energie für unnötige Aktionen verschwenden, z. B. wochenlanges Refactoring, das die Komplexität des Codes nicht objektiv reduziert, oder tiefgreifende Mikrooptimierungen einer Funktion, die nur selten ausgeführt wird. Deshalb erfindet die Branche ständig neue Muster, um einen effizienten Entwicklungsprozess zu verfolgen. Agile Kanban-Methoden, die es uns ermöglichen, uns an ständig wechselnde Anforderungen anzupassen, spezielle Programmiersprachen für mobile Plattformen wie Kotlin oder Frameworks für die Erstellung von Websites wie React sind nur einige Beispiele. Ingenieure und Ingenieurinnen innovieren in diesen Bereichen, denn jede Ineffizienz erhöht die Kosten.
Was die Sache noch schwieriger macht, ist, dass wir uns bei der Entwicklung von Software auch der zukünftigen Kosten bewusst sein sollten. Einige Quellen schätzen sogar, dass die Betriebs- und Wartungskosten höher sein können als die ursprünglichen Entwicklungskosten. Code-Änderungen, um wettbewerbsfähig zu bleiben, Fehlerbehebungen, Zwischenfälle, Installationen und schließlich die Rechenkosten (einschließlich des Stromverbrauchs) sind nur einige Beispiele für die Gesamtbetriebskosten von Software, die wir berücksichtigen müssen. Agile Methoden helfen dabei, diese Kosten frühzeitig zu erkennen, indem sie die Software häufig freigeben und früher Feedback einholen.
Aber sind die Gesamtbetriebskosten höher, wenn wir Effizienz- und Geschwindigkeitsoptimierungen in unserem Softwareentwicklungsprozess außer Acht lassen? In vielen Fällen sollte es kein Problem sein, ein paar Sekunden länger auf die Ausführung unserer Anwendung zu warten. Hinzu kommt, dass die Hardware jeden Monat billiger und schneller wird. Im Jahr 2022 war es nicht schwer, ein Smartphone mit einem Dutzend GByte Arbeitsspeicher zu kaufen. Fingergroße 2-TB-SSD-Festplatten mit einem Lese- und Schreibdurchsatz von 7 GB/s sind erhältlich. Selbst PC-Workstations für den Heimgebrauch erreichen nie dagewesene Leistungswerte. Mit 8 oder mehr CPUs, die jeweils Milliarden von Zyklen pro Sekunde ausführen können, und mit 2 TB Arbeitsspeicher können wir Dinge schnell berechnen. Außerdem können wir später immer noch Optimierungen hinzufügen, oder?
Maschinen sind im Vergleich zu Menschen immer billiger geworden; jede Diskussion über die Effizienz von Computern, die das nicht berücksichtigt, ist kurzsichtig. "Effizienz bedeutet, die Gesamtkosten zu senken - nicht nur die Maschinenzeit während der Programmlaufzeit, sondern auch die Zeit, die der Programmierer und die Benutzer des Programms aufwenden.
Brian W. Kernighan und P. J. Plauger, The Elements of Programming Style (McGraw-Hill, 1978)
Schließlich ist es ein kompliziertes Thema, die Laufzeit oder den Platzbedarf der Software zu verbessern. Vor allem, wenn du neu bist, verlierst du oft Zeit mit der Optimierung, ohne dass dein Programm deutlich schneller wird. Und selbst wenn wir anfangen, uns um die Latenz zu kümmern, die durch unseren Code verursacht wird, werden Dinge wie die Java Virtual Machine oder der Go-Compiler ihre Optimierungen trotzdem anwenden. Es mag sich wie eine schlechte Idee anhören, mehr Zeit auf etwas Kniffliges zu verwenden, wie die Effizienz auf moderner Hardware, die auch die Zuverlässigkeit und Wartbarkeit unseres Codes beeinträchtigen kann. Dies sind nur einige Gründe, warum Ingenieure Leistungsoptimierungen in der Regel an die unterste Stelle der Prioritätenliste für die Entwicklung setzen.
Wie bei jeder extremen Vereinfachung besteht leider auch hier ein gewisses Risiko, wenn man die Prioritäten bei der Leistung herunterschraubt. Mach dir aber keine Sorgen! In diesem Buch werde ich nicht versuchen, dich davon zu überzeugen, dass du jetzt die Anzahl der Nanosekunden messen solltest, die jede Codezeile einbringt oder jedes Bit, das sie im Speicher belegt, bevor du sie in deine Software einfügst. Das solltest du nicht. Es liegt mir fern, dich zu motivieren, die Leistung ganz oben auf deine Entwicklungsprioritätenliste zu setzen.
Es gibt jedoch einen Unterschied zwischen dem bewussten Aufschieben von Optimierungen und dummen Fehlern, die Ineffizienzen und Verlangsamungen verursachen. Ein bekanntes Sprichwort sagt : "Das Perfekte ist der Feind des Guten", aber wir müssen zuerst das ausgewogene Gute finden. Deshalb möchte ich eine subtile, aber wesentliche Änderung der Art und Weise vorschlagen, wie wir als Softwareentwickler/innen über die Anwendungsleistung nachdenken sollten. Damit kannst du kleine, aber effektive Gewohnheiten in deinen Programmier- und Entwicklungsmanagementzyklus einbringen. Auf der Grundlage von Daten und so früh wie möglich im Entwicklungszyklus wirst du lernen, wie du erkennen kannst, wann du Ineffizienzen im Programm getrost ignorieren oder verschieben kannst. Und schließlich, wann du es dir nicht leisten kannst, Leistungsoptimierungen auszulassen, wo und wie du sie effektiv anwendest und wann du aufhören solltest.
In "Hinter der Leistung" werden wir das Wort Leistung auspacken und lernen, wie es mit der Effizienz im Titel dieses Buches zusammenhängt. Dann werden wir in "Common Efficiency Misconceptions" fünf schwerwiegende Missverständnisse rund um Effizienz und Leistung hinterfragen, die Entwickler/innen oft unterschlagen. Du wirst lernen, dass das Nachdenken über Effizienz nicht nur für "High-Performance"-Software reserviert ist. Software.
Hinweis
Einige der Kapitel, wie dieses, Kapitel 3 und Teile anderer Kapitel, sind völlig sprachunabhängig, so dass sie auch für Nicht-Go-Entwickler praktisch sein sollten!
In "Der Schlüssel zur pragmatischen Code-Performance" werde ich dir schließlich erklären, warum die Konzentration auf die Effizienz es uns ermöglicht, effektiv über Leistungsoptimierungen nachzudenken, ohne dabei Zeit und andere Softwarequalitäten zu opfern. Dieses Kapitel mag sich theoretisch anfühlen, aber glaub mir, die Erkenntnisse werden dein grundlegendes Urteilsvermögen darüber schulen, wie und ob du bestimmte Effizienzoptimierungen, Algorithmen und Codeverbesserungen, die in anderen Teilen dieses Buches vorgestellt werden, übernehmen solltest. Vielleicht hilft es dir auch, deinen Produktmanager oder Stakeholder zu motivieren, zu erkennen, dass ein effizienteres Bewusstsein für dein Projekt von Vorteil sein kann.
Beginnen wir damit, die Definition von Effizienz auszupacken.
Hinter der Leistung
Bevor wir erörtern, warum Software-Effizienz oder -Optimierungen wichtig sind, müssen wir zunächst das überstrapazierte Wort Leistung entmystifizieren. In der Technik wird dieses Wort in vielen Kontexten verwendet und kann verschiedene Dinge bedeuten, also packen wir es aus, um Verwirrung zu vermeiden.
Wenn jemand sagt: "Diese Anwendung läuft schlecht", meint er in der Regel, dass dieses Programm langsam läuft.1 Wenn dieselben Leute aber sagen: "Bartek arbeitet nicht gut", meinen sie damit wahrscheinlich nicht, dass Bartek zu langsam vom Computer zum Besprechungsraum läuft. Meiner Erfahrung nach betrachten viele Menschen in der Softwareentwicklung das Wort Leistung als Synonym für Geschwindigkeit. Für andere bedeutet es die Gesamtqualität der Ausführung, was die ursprünglicheDefinition dieses Wortes ist.2 Dieses Phänomen wird manchmal als "semantische Diffusion" bezeichnet. Es tritt auf, wenn ein Wort von größeren Gruppen mit einer anderen Bedeutung verwendet wird, als es ursprünglich hatte.
Das Wort "Leistung" in "Computerleistung" bedeutet dasselbe wie "Leistung" in anderen Zusammenhängen, nämlich: "Wie gut erledigt der Computer die Arbeit, die er erledigen soll?"
Arnold O. Allen, Introduction to Computer Performance Analysis with Mathematica (Morgan Kaufmann, 1994)
Ich denke, Arnolds Definition beschreibt das Wort " Leistung" so genau wie möglich, daher ist das vielleicht der erste Punkt, den du aus diesem Buch mitnehmen kannst. Sei konkret.
Kläre, wenn jemand das Wort "Leistung" verwendet
Sei beim Lesen der Dokumentation, des Codes, der Bug-Tracker oder bei Vorträgen auf Konferenzen vorsichtig, wenn du das Wort " Leistung" hörst. Stelle Nachfragen und vergewissere dich, was der Autor meint.
In der Praxis kann die Leistung, also die Qualität der Gesamtausführung, viel mehr beinhalten, als wir normalerweise denken. Es mag sich pingelig anfühlen, aber wenn wir die Kosteneffizienz der Softwareentwicklung verbessern wollen, müssen wir klar, effizient undeffektiv kommunizieren!
Ich schlage vor, das Wort " Leistung" zu vermeiden, es sei denn, wir können seine Bedeutung präzisieren. Stell dir vor, du meldest einen Fehler in einem Bugtracker wie GitHub Issues. Vor allem dort solltest du nicht einfach nur "schlechte Leistung" erwähnen, sondern das unerwartete Verhalten der Anwendung, das du beschrieben hast, genau beschreiben. Ähnlich verhält es sich, wenn du im Changelog Verbesserungen für eine Softwareversion beschreibst,3 erwähne nicht nur, dass eine Änderung "die Leistung verbessert" hat. Beschreibe, was genau verbessert wurde. Vielleicht ist ein Teil des Systems jetzt weniger anfällig für Fehler bei der Benutzereingabe, verbraucht weniger Arbeitsspeicher (wenn ja, wie viel weniger und unter welchen Umständen?) oder führt etwas schneller aus (um wie viele Sekunden schneller, bei welcher Art von Arbeitslast?). Wenn du dich klar ausdrückst, sparst du Zeit für dich und deine Nutzer.
Ich werde dieses Wort in meinem Buch ausdrücklich erwähnen. Wann immer du also das Wort " Leistung" bei der Beschreibung der Software siehst, erinnere dich an diese Visualisierung in Abbildung 1-1.
Im Prinzip bedeutet Software-Performance "wie gut Software läuft" und besteht aus drei Kernelementen der Ausführung, die du verbessern (oder opfern) kannst:
- Genauigkeit
-
Die Anzahl der Fehler, die du bei der Arbeit machst, um die Aufgabe zu erfüllen. Bei Software kann dies anhand der Anzahl der falschen Ergebnisse gemessen werden, die deine Anwendung produziert. Zum Beispiel, wie viele Anfragen in einem Websystem mit einem HTTP-Statuscode ungleich 200 abgeschlossen wurden.
- Geschwindigkeit
-
Wie schnell du die Arbeit erledigst, die zur Bewältigung der Aufgabe nötig ist - die Rechtzeitigkeit der Ausführung. Dies lässt sich an der Latenzzeit oder am Durchsatz ablesen. Wir können zum Beispiel schätzen, dass eine typische Komprimierung von 1 GB Daten im Speicher etwa 10 s (Latenzzeit) dauert und einen Durchsatz von etwa 100 MBps ermöglicht.
- Effizienz
-
Das Verhältnis zwischen der von einem dynamischen System gelieferten Nutzenergie und der ihm zugeführten Energie. Einfacher ausgedrückt, ist dies ein Indikator dafür, wie viele zusätzliche Ressourcen, Energie oder Arbeit zur Bewältigung der Aufgabe eingesetzt wurden. Mit anderen Worten: wie viel Mühe wir verschwendet haben. Wenn wir z. B. 64 Byte wertvoller Daten von der Festplatte holen und dafür 420 Byte im Arbeitsspeicher benötigen, beträgt die Speichereffizienz 15,23 %.
Das bedeutet nicht, dass unser Betrieb in absoluten Zahlen 15,23 % effizient ist. Wir haben keine Berechnungen zu Energie, CPU-Zeit, Wärme und anderen Effizienzen angestellt. In der Praxis geben wir meist an, welche Effizienz wir im Sinn haben. In unserem Beispiel war das der Speicherplatz.
Zusammenfassend lässt sich sagen, dass Leistung eine Kombination aus mindestens diesen drei Elementen ist:
Durch die Verbesserung einer dieser Faktoren wird die Leistung der laufenden Anwendung oder des Systems gesteigert. Sie kann die Zuverlässigkeit, Verfügbarkeit, Ausfallsicherheit, die Gesamtlatenz und vieles mehr verbessern. Ebenso kann das Ignorieren einer dieser Punkte unsere Software weniger nützlich machen.4 Die Frage ist, an welchem Punkt wir "aufhören" und sagen, dass es gut genug ist. Diese drei Elemente mögen sich unzusammenhängend anfühlen, aber in Wirklichkeit sind sie miteinander verbunden. So können wir zum Beispiel eine bessere Zuverlässigkeit und Verfügbarkeit erreichen, ohne die Genauigkeit zu verändern (nicht die Anzahl der Fehler zu reduzieren). Bei der Effizienz zum Beispiel verringert die Reduzierung des Speicherverbrauchs die Wahrscheinlichkeit, dass der Speicher knapp wird und die Anwendung oder das Host-Betriebssystem abstürzt. Dieses Buch konzentriert sich auf Wissen, Techniken und Methoden, mit denen du die Effizienz und Geschwindigkeit deines laufenden Codes erhöhen kannst, ohne die Genauigkeit zu beeinträchtigen.
Es ist kein Fehler, dass der Titel meines Buches "Efficient Go" lautet.
Mein Ziel ist es, dir pragmatische Fähigkeiten zu vermitteln, die es dir ermöglichen, mit minimalem Aufwand hochwertigen, genauen, effizienten und schnellen Code zu produzieren. Wenn ich zu diesem Zweck von der Gesamteffizienz des Codes spreche (ohne eine bestimmte Ressource zu nennen), meine ich sowohl Geschwindigkeit als auch Effizienz, wie in Abbildung 1-1 dargestellt. Vertrau mir, das wird uns helfen, effektiv durch das Thema zu kommen. Warum das so ist, erfährst du in "Der Schlüssel zur pragmatischen Codeleistung".
Die irreführende Verwendung des Begriffs " Leistung" ist vielleicht nur die Spitze des Eisbergs der Missverständnisse beim Thema Effizienz. Wir werden jetzt viele weitere schwerwiegende Klischees und Tendenzen durchgehen, die dazu führen, dass sich die Entwicklung unserer Software verschlechtert. Im besten Fall führt dies zu teureren oder weniger wertvollen Programmen. Im schlimmsten Fall führt es zu schwerwiegenden sozialen und finanziellen Organisationsproblemen.
Häufige Missverständnisse über die Effizienz
Die Anzahl der Male, in denen ich bei Codeüberprüfungen oder Sprintplanungen gebeten wurde, die Effizienz der Software "vorerst" zu ignorieren, ist schwindelerregend. Und du hast das wahrscheinlich auch schon gehört! Auch die Änderungsvorschläge anderer habe ich aus denselben Gründen schon oft abgelehnt. Vielleicht wurden unsere Änderungen damals aus guten Gründen abgelehnt, vor allem wenn es sich um Mikro-Optimierungen handelte, die die Software unnötig kompliziert machten.
Andererseits gab es auch Fälle, in denen die Gründe für die Ablehnung auf allgemeinen, sachlichen Missverständnissen beruhten. Versuchen wir, einige der schädlichsten Missverständnisse auszuräumen. Sei vorsichtig, wenn du einige dieser verallgemeinerten Aussagen hörst. Wenn du sie entmystifizierst, kannst du langfristig enorme Entwicklungskosten sparen.
Optimierter Code ist nicht lesbar
Eine der wichtigsten Eigenschaften von Softwarecode ist zweifelsohne seine Lesbarkeit.
Es ist wichtiger, den Zweck des Codes unverwechselbar zu machen, als die Virtuosität anzuzeigen.... Das Problem mit undurchsichtigem Code ist, dass das Debuggen und Ändern viel schwieriger wird, und das sind ohnehin schon die schwierigsten Aspekte der Computerprogrammierung. Außerdem besteht die Gefahr, dass ein zu schlaues Programm nicht das sagt, was du dachtest, dass es sagt.
Brian W. Kernighan und P. J. Plauger, The Elements of Programming Style (McGraw-Hill, 1978)
Wenn wir an ultraschnellen Code denken, kommen uns manchmal als Erstes diese cleveren Low-Level-Implementierungen mit einem Haufen Byte-Shifts, magischen Byte-Paddings und ausgerollten Schleifen in den Sinn. Oder noch schlimmer: reiner Assembler-Code, der mit deiner Anwendung verknüpft ist.
Ja, solche Low-Level-Optimierungen können unseren Code deutlich weniger lesbar machen, aber wie du in diesem Buch lernen wirst, sind solche extremen Änderungen in der Praxis selten. Code-Optimierungen können zu zusätzlicher Komplexität führen, die kognitive Belastung erhöhen und unseren Code schwieriger zu warten machen. Das Problem ist, dass Ingenieure dazu neigen, Optimierung bis zum Äußersten mit Komplexität zu assoziieren und Effizienzoptimierung wie Feuer zu vermeiden. In ihren Augen hat das sofort negative Auswirkungen auf die Lesbarkeit. Dieser Abschnitt soll dir zeigen, dass es Wege gibt, effizienzoptimierten Code verständlich zu machen. Effizienz und Lesbarkeit können nebeneinander bestehen.
Das gleiche Risiko besteht auch, wenn wir andere Funktionen hinzufügen oder den Codeaus anderen Gründen ändern. Wenn wir uns zum Beispiel weigern, einen effizienteren Code zu schreiben, weil wir befürchten, dass die Lesbarkeit abnimmt, ist das so, als würden wir uns weigern, wichtige Funktionen hinzuzufügen, um Komplexität zu vermeiden. Auch das ist eine berechtigte Frage, und wir können in Erwägung ziehen, die Funktion zu streichen, aber wir sollten zuerst die Konsequenzen abwägen. Das Gleiche gilt für Änderungen an der Effizienz.
Wenn du zum Beispiel eine zusätzliche Validierung der Eingabe vornehmen willst, kannst du naiv einen komplexen 50-zeiligen Code-Wasserfall aus if
Anweisungen direkt in die Bearbeitungsfunktion einfügen. Das könnte den nächsten Leser deines Codes zum Weinen bringen (oder dich selbst, wenn du dir diesen Code Monate später wieder ansiehst). Alternativ kannst du auch alles in einereinzigen func validate(input string) error
Funktion kapseln, was die Komplexität nur geringfügig erhöht. Außerdem kannst du den Code so gestalten, dass er auf der Seite des Aufrufers oder in der Middleware validiert wird, damit du den Bearbeitungsblock nicht ändern musst. Wir können auch unser Systemdesign überdenken und die Komplexität der Validierung in ein anderes System oder eine andere Komponente verlagern, sodass diese Funktion nicht implementiert wird. Es gibt viele Möglichkeiten, ein bestimmtes Feature zusammenzustellen, ohne unsere Ziele zu vernachlässigen.
Wie unterscheiden sich Leistungsverbesserungen in unserem Code von zusätzlichen Funktionen? Ich würde sagen, sie sind es nicht. Du kannst Leistungsoptimierungen mit Blick auf die Lesbarkeit entwerfen, genauso wie du es mit Funktionen tust. Beides kann für die Leser völlig transparent sein, wenn es unter Abstraktionen versteckt wird.5
Dennoch neigen wir dazu, Optimierungen als Hauptursache für Lesbarkeitsprobleme zu sehen. Die schädlichste Folge dieses und anderer Missverständnisse in diesem Kapitel ist, dass sie oft als Ausrede benutzt werden, um Leistungsverbesserungen komplett zu ignorieren. Das führt oft zu einer so genannten vorzeitigen Pessimierung, also dazu, dass das Programm weniger effizient wird - das Gegenteil von Optimierung.
Sei sanft zu dir selbst, sei sanft zu deinem Code: Wenn alle anderen Dinge gleich sind, insbesondere die Komplexität und Lesbarkeit des Codes, sollten bestimmte effiziente Entwurfsmuster und Programmiersprachen ganz natürlich aus deinen Fingerspitzen fließen und nicht schwieriger zu schreiben sein als die pessimistischen Alternativen. Das ist keine verfrühte Optimierung, sondern die Vermeidung von unnötiger Pessimierung.
H. Sutter und A. Alexandrescu, C++ Coding Standards: 101 Rules, Guidelines, and Best Practices (Addison-Wesley, 2004)
Lesbarkeit ist wichtig. Ich würde sogar behaupten, dass unlesbarer Code auf lange Sicht selten effizient ist. Wenn sich Software weiterentwickelt, kann es leicht passieren, dass wir eine vorher gemachte, zu clevere Optimierung kaputt machen, weil wir sie falsch interpretieren oder missverstehen. Ähnlich wie bei Bugs und Fehlern ist es einfacher, in kniffligem Code Leistungsprobleme zu verursachen. In Kapitel 10 findest du Beispiele für bewusste Änderungen an der Leistung, wobei der Schwerpunkt auf der Wartbarkeit und Lesbarkeit liegt.
Lesbarkeit ist wichtig!
Es ist einfacher, lesbaren Code zu optimieren, als stark optimierten Code lesbar zu machen. Das gilt sowohl für Menschen als auch für Compiler, die versuchen könnten, deinen Code zu optimieren!
Optimierung führt oft zu weniger lesbarem Code, weil wir nicht von Anfang an eine gute Effizienz in unsere Software einplanen. Wenn du dich weigerst, jetzt über Effizienz nachzudenken, kann es zu spät sein, den Code später zu optimieren, ohne die Lesbarkeit zu beeinträchtigen. Es ist viel einfacher, einen Weg zu finden, eine einfachere und effizientere Arbeitsweise in den neuen Modulen einzuführen, in denen wir gerade erst mit der Entwicklung von APIs und Abstraktionen begonnen haben. Wie du in Kapitel 3 lernen wirst, können wir die Leistung auf vielen verschiedenen Ebenen optimieren, nicht nur durch Erbsenzählerei und Code-Tuning. Vielleicht können wir einen effizienteren Algorithmus, eine schnellere Datenstruktur oder einen anderen Systemkompromiss wählen. Diese Maßnahmen werden wahrscheinlich zu einem saubereren, wartbaren Code und einer besseren Leistung führen, als wenn wir die Effizienz erst nach der Veröffentlichung der Software verbessern. Bei vielen Einschränkungen wie Abwärtskompatibilität, Integrationen oder strengen Schnittstellen besteht unsere einzige Möglichkeit, die Leistung zu verbessern, darin, den Code oder das System mit zusätzlicher, oft erheblicher Komplexität zu versehen.
Code nach der Optimierung kann besser lesbar sein
Überraschenderweise kann Code nach der Optimierung besser lesbar sein! Schauen wir uns ein paar Go-Code-Beispiele an. Beispiel 1-1 ist eine naive Anwendung eines Getter-Musters, das ich persönlich schon hunderte Male gesehen habe, wenn ich Go-Code von Schülern oder jungen Entwicklern geprüft habe.
Beispiel 1-1. Einfache Berechnung für das Verhältnis der gemeldeten Fehler
type
ReportGetter
interface
{
Get
(
)
[
]
Report
}
func
FailureRatio
(
reports
ReportGetter
)
float64
{
if
len
(
reports
.
Get
(
)
)
==
0
{
return
0
}
var
sum
float64
for
_
,
report
:=
range
reports
.
Get
(
)
{
if
report
.
Error
(
)
!=
nil
{
sum
++
}
}
return
sum
/
float64
(
len
(
reports
.
Get
(
)
)
)
}
Dies ist ein vereinfachtes Beispiel, aber es gibt ein weit verbreitetes Muster, bei dem eine Funktion oder Schnittstelle übergeben wird, um die für den Betrieb benötigten Elemente zu erhalten, anstatt sie direkt zu übergeben. Das ist nützlich, wenn Elemente dynamisch hinzugefügt, zwischengespeichert oder aus entfernten Datenbanken abgerufen werden.
Beachte, dass wir
Get
dreimal ausführen, um Berichte abzurufen.
Ich denke, du wirst zustimmen, dass der Code aus Beispiel 1-1 in den meisten Fällen funktioniert. Er ist einfach und gut lesbar. Dennoch würde ich einen solchen Code wegen möglicher Effizienz- und Genauigkeitsprobleme wahrscheinlich nicht akzeptieren. Ich würde stattdessen eine einfache Änderung wie in Beispiel 1-2 vorschlagen.
Beispiel 1-2. Einfache, effizientere Berechnung für das Verhältnis der gemeldeten Fehler
func
FailureRatio
(
reports
ReportGetter
)
float64
{
got
:=
reports
.
Get
(
)
if
len
(
got
)
==
0
{
return
0
}
var
sum
float64
for
_
,
report
:=
range
got
{
if
report
.
Error
(
)
!=
nil
{
sum
++
}
}
return
sum
/
float64
(
len
(
got
)
)
}
Im Vergleich zu Beispiel 1-1 rufe ich
Get
nicht an drei Stellen auf, sondern nur einmal und verwende das Ergebnis über die Variablegot
wieder.
Einige Entwickler könnten argumentieren, dass die Funktion FailureRatio
potenziell nur sehr selten genutzt wird; sie liegt nicht auf einem kritischen Pfad, und die aktuelle Implementierung von ReportGetter
ist sehr billig und schnell. Sie könnten argumentieren, dass wir ohne Messung oder Benchmarking nicht entscheiden können, was effizienter ist (was meistens stimmt!). Sie könnten meinen Vorschlag eine "verfrühte Optimierung" nennen.
Ich halte das jedoch für einen sehr beliebten Fall von voreiliger Pessimisierung. Es ist ein alberner Fall von Ablehnung von effizienterem Code, der die Dinge im Moment nicht sehr beschleunigt, aber auch nicht schadet. Im Gegenteil: Ich würde behaupten, dass Beispiel 1-2 in vielerlei Hinsicht überlegen ist:
- Ohne Messungen ist der Code aus Beispiel 1-2 effizienter.
-
Schnittstellen ermöglichen es uns, die Implementierung zu ersetzen. Sie stellen einen bestimmten Vertrag zwischen Nutzern und Implementierungen dar. Aus dem Blickwinkel der
FailureRatio
Funktion können wir nichts annehmen, was über diesen Vertrag hinausgeht. Wahrscheinlich können wir nicht davon ausgehen, dass der Code vonReportGetter.Get
immer schnell und billig sein wird.6 Morgen könnte jemand denGet
Code mit der teuren E/A-Operation gegen ein Dateisystem, der Implementierung mit Mutexen oder dem Aufruf der entfernten Datenbank austauschen.7Natürlich können wir sie später mit einem geeigneten Effizienzablauf, den wir in "Effizienzbewusster Entwicklungsablauf" besprechen werden , iterieren und optimieren , aber wenn es eine sinnvolle Änderung ist, die auch andere Dinge verbessert, kann es nicht schaden, sie jetzt vorzunehmen.
- Der Codevon Beispiel 1-2 ist sicherer.
-
Es ist möglicherweise nicht auf den ersten Blick sichtbar, aber der Code aus Beispiel 1-1 birgt ein erhebliches Risiko, Race Conditions einzuführen. Wir könnten auf ein Problem stoßen, wenn die Implementierung von
ReportGetter
mit anderen Threads synchronisiert wird, die das Ergebnis vonGet()
im Laufe der Zeit dynamisch verändern. Es ist besser, Race Conditions zu vermeiden und die Konsistenz innerhalb eines Funktionskörpers sicherzustellen. Race-Fehler sind am schwierigsten zu debuggen und zu entdecken, daher ist es besser, auf Nummer sicher zu gehen. - Der Codevon Beispiel 1-2 ist besser lesbar.
-
Wir fügen zwar eine weitere Zeile und eine zusätzliche Variable hinzu, aber am Ende sagt uns der Code in Beispiel 1-2 ausdrücklich, dass wir das gleiche Ergebnis für drei Verwendungen verwenden wollen. Indem wir drei Instanzen des
Get()
-Aufrufs durch eine einfache Variable ersetzen, minimieren wir auch die potenziellen Nebeneffekte und machen unsereFailureRatio
rein funktional (mit Ausnahme der ersten Zeile). Beispiel 1-2 ist also auf jeden Fall lesbarer als Beispiel 1-1.
Warnung
Eine solche Aussage mag richtig sein, aber das Böse liegt im Teil "verfrüht". Nicht jede Leistungsoptimierung ist verfrüht. Außerdem ist eine solche Regel kein Freibrief dafür, effizientere Lösungen mit vergleichbarer Komplexität abzulehnen oder zu vergessen.
Ein weiteres Beispiel für optimierten Code, der Klarheit schafft, ist der Code in den Beispielen 1-3 und 1-4.
Beispiel 1-3. Einfache Schleife ohne Optimierung
func
createSlice
(
n
int
)
(
slice
[
]
string
)
{
for
i
:=
0
;
i
<
n
;
i
++
{
slice
=
append
(
slice
,
"I"
,
"am"
,
"going"
,
"to"
,
"take"
,
"some"
,
"space"
)
}
return
slice
}
Beispiel 1-3 zeigt, wie wir normalerweise Slices in Go füllen, und du könntest sagen, dass hier nichts falsch ist. Es funktioniert einfach. Ich würde jedoch argumentieren, dass wir in der Schleife nicht so anhängen sollten, wenn wir schon vorher genau wissen, wie viele Elemente wir an die Slice anhängen werden. Stattdessen sollten wir es meiner Meinung nach immer so schreiben wie in Beispiel 1-4.
Beispiel 1-4. Einfache Schleife mit Vorab-Zuweisungsoptimierung. Ist das weniger lesbar?
func
createSlice
(
n
int
)
[
]
string
{
slice
:=
make
(
[
]
string
,
0
,
n
*
7
)
for
i
:=
0
;
i
<
n
;
i
++
{
slice
=
append
(
slice
,
"I"
,
"am"
,
"going"
,
"to"
,
"take"
,
"some"
,
"space"
)
}
return
slice
}
Wir werden über Effizienzoptimierungen wie die in den Beispielen 1-2 und 1-4 in "Pre-Allocate If You Can" sprechen , mit dem tieferenGo-Laufzeitwissen aus Kapitel 4. Im Prinzip sorgen beide dafür, dass unser Programm weniger Arbeit macht. In Beispiel 1-4 muss die interne Implementierung von append
dank der anfänglichen Vorabzuweisung die Slicegröße im Speicher nicht schrittweise erweitern. Wir machen das einmal zu Beginn. Jetzt möchte ich, dass du dich auf die folgende Frage konzentrierst: Ist dieser Code mehr oder weniger lesbar?
Lesbarkeit kann oft subjektiv sein, aber ich würde sagen, dass der effizientere Code aus Beispiel 1-4 verständlicher ist. Der Code ist zwar eine Zeile länger und damit etwas komplexer, aber gleichzeitig ist er eindeutig und klar in seiner Aussage. Er hilft nicht nur der Go-Laufzeit, weniger Arbeit zu verrichten, sondern gibt dem Leser auch einen Hinweis auf den Zweck dieser Schleife und wie viele Iterationen wir genau erwarten.
Wenn du noch nie die rohe Verwendung der eingebauten Funktion make
in Go gesehen hast, würdest du wahrscheinlich sagen, dass dieser Code weniger lesbar ist. Das ist fair. Sobald du jedoch den Vorteil erkennst und anfängst, dieses Muster konsequent im Code zu verwenden, wird es zu einer guten Gewohnheit. Außerdem verrät dir jede Slice-Erstellung ohne diese Vorabzuweisung etwas. Zum Beispiel, dass die Anzahl der Iterationen unvorhersehbar ist und du deshalb vorsichtiger sein solltest. Du weißt also etwas, bevor du dir den Inhalt der Schleife überhaupt angesehen hast! Um eine solche Angewohnheit in der gesamten Prometheus- und Thanos-Codebasis durchgängig zu machen, haben wir sogar einen entsprechenden Eintrag in den Thanos Go Coding Style Guide aufgenommen.
Lesbarkeit ist nicht in Stein gemeißelt; sie ist dynamisch
Die Fähigkeit, bestimmten Softwarecode zu verstehen, kann sich im Laufe der Zeit ändern, selbst wenn sich der Code nie ändert. Konventionen kommen und gehen, wenn die Sprachgemeinschaft neue Dinge ausprobiert. Mit strikter Konsistenz kannst du dem Leser helfen, auch komplexere Teile deines Programms zu verstehen, indem du eine neue, klare Konvention einführst.
Lesbarkeit heute versus früher
Im Allgemeinen wenden Entwickler oft Knuths Zitat "Verfrühte Optimierung ist die Wurzel allen Übels" an8 um Probleme mit der Lesbarkeit durch Optimierungen zu verringern. Dieses Zitat stammt jedoch aus einer längst vergangenen Zeit. Obwohl wir aus der Vergangenheit viel über die allgemeine Programmierung lernen können, gibt es viele Dinge, die wir seit 1974 enorm verbessert haben. Damals war es zum Beispiel üblich, dem Namen einer Variablen Informationen über ihren Typ hinzuzufügen, wie in Beispiel 1-5 zu sehen ist.9
Beispiel 1-5. Beispiel für die Anwendung der ungarischen Notation von Systems auf Go-Code
type
structSystem
struct
{
sliceU32Numbers
[]
uint32
bCharacter
byte
f64Ratio
float64
}
Die ungarische Notation war nützlich, weil Compiler und integrierte Entwicklungsumgebungen (IDEs) zu diesem Zeitpunkt noch nicht sehr ausgereift waren. Aber heutzutage können wir in unseren IDEs oder sogar auf Repository-Websites wie GitHub mit dem Mauszeiger über die Variable fahren, um sofort ihren Typ zu erfahren. Wir können innerhalb von Millisekunden zur Variablendefinition gehen, den Kommentar lesen und alle Aufrufe und Mutationen finden. Mit intelligenten Codevorschlägen, fortschrittlichen Hervorhebungen und der Dominanz der objektorientierten Programmierung, die Mitte der 1990er Jahre entwickelt wurde, haben wir Werkzeuge in der Hand, mit denen wir Funktionen und Effizienzoptimierungen (Komplexität) hinzufügen können, ohne die praktische Lesbarkeit wesentlich zu beeinträchtigen.10 Außerdem sind die Zugänglichkeit und die Fähigkeiten der Beobachtungs- und Debugging-Werkzeuge enorm gewachsen, was wir in Kapitel 6 untersuchen werden. Das erlaubt zwar immer noch keinen cleveren Code, aber wir können größereCodebasen schneller verstehen.
Zusammenfassend lässt sich sagen, dass die Leistungsoptimierung wie eine weitere Funktion in unserer Software ist, und wir sollten sie entsprechend behandeln. Sie kann die Komplexität erhöhen, aber es gibt Möglichkeiten, den kognitiven Aufwand zu minimieren, der nötig ist, um unseren Code zu verstehen.11
Wie man effizienten Code besser lesbar macht
-
Entferne oder vermeide unnötige Optimierungen.
-
Verkapsle komplexen Code hinter einer klaren Abstraktion (z.B. einerSchnittstelle).
-
Halte den "heißen" Code (den kritischen Teil, der eine bessere Effizienz erfordert) vom "kalten" Code (der selten ausgeführt wird) getrennt.
Wie wir in diesem Kapitel gelernt haben, gibt es sogar Fälle, in denen ein effizienteres Programm oft ein Nebeneffekt des einfachen, expliziten und verständlichen Codes ist.
Du wirst es nicht brauchen
You Aren't Going to Need It (YAGNI) ist eine mächtige und beliebte Regel, die ich oft verwende, wenn ich eine Software schreibe oder überprüfe.
Eines der bekanntesten Prinzipien von XP [Extreme Programming] ist das You Aren't Going to Need It (YAGNI)-Prinzip. Das YAGNI-Prinzip unterstreicht den Wert des Aufschubs einer Investitionsentscheidung angesichts der Unsicherheit über den Ertrag der Investition. Im Kontext von XP bedeutet das, dass die Implementierung von unscharfen Funktionen so lange aufgeschoben wird, bis die Unsicherheit über ihren Wert beseitigt ist.
Hakan Erdogmu und John Favaro, "Keep Your Options Open: Extreme Programming und die Ökonomie der Flexibilität"
Im Prinzip bedeutet das, dass wir zusätzliche Arbeit vermeiden, die für die aktuellen Anforderungen nicht unbedingt notwendig ist. Es beruht auf der Tatsache, dass sich die Anforderungen ständig ändern und wir unsere Software schnell weiterentwickeln müssen.
Stellen wir uns eine mögliche Situation vor, in der Katie, eine leitende Softwareentwicklung, die Aufgabe erhält, einen einfachen Webserver zu erstellen. Nichts Ausgefallenes, nur ein HTTP-Server, der einen REST-Endpunkt bereitstellt. Katie ist eine erfahrene Entwicklerin, die in der Vergangenheit wahrscheinlich schon hundert ähnliche Endpunkte erstellt hat. Sie fängt an, programmiert die Funktionen und testet den Server in kürzester Zeit. Als sie noch etwas Zeit übrig hat, beschließt sie, eine zusätzliche Funktion hinzuzufügen: eine einfache Autorisierungsschicht mit Überbringertoken. Katie weiß, dass eine solche Änderung nicht den aktuellen Anforderungen entspricht, aber sie hat schon Hunderte von REST-Endpunkten geschrieben, und alle hatten eine ähnliche Autorisierung. Die Erfahrung zeigt ihr, dass solche Anforderungen höchstwahrscheinlich auch bald kommen werden, also wird sie vorbereitet sein. Denkst du, dass eine solche Änderung sinnvoll ist und akzeptiert werden sollte?
Obwohl Katie gute Absichten und solide Erfahrungen gezeigt hat, sollten wir davon absehen, solche Änderungen zusammenzuführen, um die Qualität des Webservercodes und die Kosteneffizienz der Entwicklung insgesamt zu erhalten. Mit anderen Worten: Wir sollten die YAGNI-Regel anwenden. Warum? In den meisten Fällen können wir ein Feature nicht vorhersagen. Wenn wir uns an die Anforderungen halten, können wir Zeit und Komplexität sparen. Es besteht das Risiko, dass das Projekt nie eine Autorisierungsschicht benötigt, z. B. wenn der Server hinter einem speziellen Autorisierungs-Proxy läuft. In einem solchen Fall kann der zusätzliche Code, den Katie geschrieben hat, hohe Kosten verursachen, selbst wenn er nicht gebraucht wird. Es handelt sich um zusätzlichen Code, der gelesen werden muss, was die kognitive Belastung erhöht. Außerdem ist es schwieriger, diesen Code bei Bedarf zu ändern oder zu überarbeiten.
Betreten wir nun einen graueren Bereich. Wir haben Katie erklärt, warum wir den Autorisierungscode ablehnen müssen. Sie stimmte zu und beschloss stattdessen, den Server zu überwachen, indem sie ihn mit ein paar wichtigen Metriken ausstattete. Verstößt diese Änderung auch gegen die YAGNI-Regel?
Wenn die Überwachung Teil der Anforderungen ist, verstößt sie nicht gegen die YAGNI-Regel und sollte akzeptiert werden. Wenn nicht, ist es schwer zu sagen, ohne den vollständigen Kontext zu kennen. Kritische Überwachung sollte ausdrücklich in den Anforderungen erwähnt werden. Doch selbst wenn das nicht der Fall ist, ist die Beobachtbarkeit des Webservers das Erste, was wir brauchen, wenn wir einen solchen Code irgendwo ausführen. Woher sollen wir sonst wissen, dass er überhaupt läuft? In diesem Fall macht Katie technisch etwas Wichtiges, das sofort nützlich ist. Letztendlich sollten wir unseren gesunden Menschenverstand und unser Urteilsvermögen walten lassen und die Überwachung zu den Softwareanforderungen hinzufügen oder explizit entfernen, bevor wir diese Änderung zusammenführen.
Später, in ihrer Freizeit, beschloss Katie, einen einfachen Cache zu den notwendigen Berechnungen hinzuzufügen, der die Leistung der separaten Endpunkt-Lesevorgänge steigert. Sie hat sogar einen kurzen Benchmark geschrieben und durchgeführt, um die Verbesserung der Latenzzeit und des Ressourcenverbrauchs des Endpunkts zu überprüfen. Ist das ein Verstoß gegen die YAGNI-Regel?
Die traurige Wahrheit bei der Softwareentwicklung ist, dass Leistungseffizienz und Reaktionszeit oft nicht in den Anforderungen der Interessengruppen enthalten sind. Das Ziel für die Leistung einer Anwendung ist es, "einfach nur zu funktionieren" und "schnell genug" zu sein, ohne genau zu wissen, was das bedeutet. Wie man praktische Anforderungen an die Software-Effizienz definiert, besprechen wir in "Ressourcenbewusste Effizienzanforderungen". Für dieses Beispiel nehmen wir das Schlimmste an. In der Anforderungsliste stand nichts über die Leistung. Sollten wir dann die YAGNI-Regel anwenden und Katies Änderung ablehnen?
Auch hier ist es schwer zu sagen, ohne den vollständigen Kontext zu kennen. Die Implementierung eines robusten und brauchbaren Caches ist nicht trivial, also wie komplex ist der neue Code? Sind die Daten, an denen wir arbeiten, leicht "cachbar"?12 Wissen wir, wie oft ein solcher Endpunkt genutzt werden wird (ist er ein kritischer Pfad)? Wie weit soll er skalieren? Andererseits ist die Berechnung desselben Ergebnisses für einen stark genutzten Endpunkt sehr ineffizient, daher ist Cache ein gutes Muster.
Ich würde Katie vorschlagen, ähnlich vorzugehen wie bei der Änderung der Überwachung: Sie sollte mit dem Team diskutieren, um die Leistungsgarantien zu klären, die der Webservice bieten sollte. So können wir feststellen, ob der Cache jetzt erforderlich ist oder gegen die YAGNI-Regel verstößt.
Als letzte Änderung hat Katie eine vernünftige Effizienzoptimierung vorgenommen, wie die Verbesserung der Slice-Vorabzuweisung, die du in Beispiel 1-4 gelernt hast. Sollten wir eine solche Änderung akzeptieren?
Ich würde hier streng sein und ja sagen. Ich schlage vor, immer eine Vorabzuweisung vorzunehmen, wie in Beispiel 1-4, wenn du die Anzahl der Elemente im Voraus kennst. Verstößt das nicht gegen die Kernaussage der YAGNI-Regel? Auch wenn etwas allgemein anwendbar ist, solltest du es nicht tun, bevor du sicher bist , dass du es brauchst?
Ich würde argumentieren, dass kleine Effizienzgewohnheiten, die die Lesbarkeit des Codes nicht beeinträchtigen (manche verbessern sie sogar), generell ein wesentlicher Teil der Arbeit eines Entwicklers sein sollten, auch wenn sie nicht ausdrücklich in den Anforderungen erwähnt werden. Wir werden sie als "Vernünftige Optimierungen" behandeln . Auch grundlegende bewährte Methoden wie die Versionierung von Code, kleine Schnittstellen oder die Vermeidung großer Abhängigkeiten werden in den Projektanforderungen nicht genannt.
Die wichtigste Erkenntnis ist, dass die Anwendung der YAGNI-Regel hilft, aber es ist nicht erlaubt, dass Entwickler die Leistungseffizienz komplett ignorieren. In der Regel sind es Tausende von kleinen Dingen, die für die übermäßige Ressourcennutzung und die Latenz einer Anwendung verantwortlich sind, und nicht nur ein einziger Punkt, den wir später beheben können. Idealerweise helfen gut definierte Anforderungen dabei, die Anforderungen an die Effizienz deiner Software zu klären, aber sie werden niemals alle Details und bewährten Methoden abdecken, die wir versuchen sollten anzuwenden.
Die Hardware wird schneller und billiger
Als ich mit dem Programmieren anfing, hatten wir nicht nur langsame Prozessoren, sondern auch sehr begrenzten Speicher - manchmal in Kilobyte gemessen. Wir mussten also an den Speicher denken und den Speicherverbrauch klug optimieren.
Valentin Simonov, "Optimiere zuerst die Lesbarkeit"
Zweifelsohne ist die Hardware von leistungsfähiger und günstiger als je zuvor. Wir erleben jedes Jahr oder jeden Monat technologische Fortschritte an fast jeder Front. Von Single-Core-Pentium-CPUs mit einer Taktrate von 200 MHz im Jahr 1995 bis hin zu kleineren, energieeffizienten CPUs mit einer Geschwindigkeit von 3 bis 4 GHz. Die Größe des Arbeitsspeichers stieg von einigen Dutzend MB im Jahr 2000 auf 64 GB in Personalcomputern 20 Jahre später, mit schnelleren Zugriffsmustern. In der Vergangenheit wurden Festplatten mit geringer Kapazität durch SSD ersetzt, dann durch 7 GBps schnelle NVME SSD Festplatten mit einigen TB Speicherplatz. Netzwerkschnittstellen haben einen Durchsatz von 100 Gigabit erreicht. Was die dezentrale Speicherung angeht, so erinnere ich mich an Disketten mit 1,44 MB Speicherplatz, dann an schreibgeschützte CD-ROMs mit einer Kapazität von bis zu 553 MB; als Nächstes gab es Blu-Ray, DVDs mit Lese- und Schreibfunktion, und jetzt sind SD-Karten mit einer Größe von einem TB leicht zu bekommen.
Fügen wir nun zu den oben genannten Fakten die weit verbreitete Meinung hinzu, dass der amortisierte Stundenwert typischer Hardware billiger ist als die Entwicklerstunde. Mit all dem würde man sagen, dass es keine Rolle spielt, ob eine einzelne Funktion im Code 1 MB mehr benötigt oder übermäßig viele Festplattenlesungen durchführt. Warum sollten wir Funktionen verzögern und leistungsbewusste Ingenieure ausbilden oder in sie investieren, wenn wir größere Server kaufen und insgesamt weniger bezahlen können?
Wie du dir wahrscheinlich vorstellen kannst, ist das nicht so einfach. Lass uns dieses ziemlich schädliche Argument auspacken und die Effizienz von der To-Do-Liste der Softwareentwicklung streichen.
Zunächst einmal ist die Behauptung, dass es billiger ist, mehr Geld für Hardware auszugeben, als teure Entwicklerzeit in Effizienzthemen zu investieren, sehr kurzsichtig. Das ist so, als ob wir jedes Mal, wenn etwas kaputt geht, ein neues Auto kaufen und ein altes verkaufen sollten, weil die Reparatur nicht trivial ist und viel kostet. Manchmal mag das funktionieren, aber in den meisten Fällen ist es nicht sehr effizient oder nachhaltig.
Nehmen wir an, das Jahresgehalt eines Softwareentwicklers liegt bei 100.000 US-Dollar. Zusammen mit den anderen Personalkosten muss das Unternehmen jährlich 120.000 USD zahlen, also 10.000 USD monatlich. Für 10.000 $ im Jahr 2021 könntest du einen Server mit 1 TB DDR4-Speicher, zwei High-End-CPUs, einer 1-Gigabit-Netzwerkkarte und 10 TB Festplattenspeicher kaufen. Lassen wir die Kosten für den Energieverbrauch erst einmal außer Acht. Ein solcher Deal bedeutet, dass unsere Software jeden Monat Terabytes an Speicherplatz überplanen kann, und wir wären immer noch besser dran, als einen Ingenieur einzustellen, der das optimiert, richtig? Leider funktioniert das so nicht.
Es stellt sich heraus, dass die Zuweisung von Terabytes häufiger vorkommt, als du denkst, und du musst nicht einen ganzen Monat warten! Abbildung 1-2 zeigt einen Screenshot des Heap-Speicherprofils eines einzelnen Replikats (von insgesamt sechs) eines einzelnen Thanos-Dienstes (von Dutzenden), der fünf Tage lang in einem einzigen Cluster lief. Wir werden in Kapitel 9 besprechen, wie man Profile liest und verwendet, aber Abbildung 1-2 zeigt den gesamten Speicher, der von einer Series
Funktion seit dem letzten Neustart des Prozesses vor fünf Tagen zugewiesen wurde.
Der größte Teil dieses Speichers wurde bereits freigegeben, aber diese Software aus dem Thanos-Projekt hat in nur fünf Tagen insgesamt 17,61 TB verbraucht.13 Wenn du stattdessen Desktop-Anwendungen oder Tools schreibst, wirst du früher oder später auf ein ähnliches Größenproblem stoßen. Wenn zum Beispiel eine Funktion 1 MB verbraucht und mehrmals für eine häufig genutzte Funktion in unserer Anwendung verwendet wird, kann das zu Gigabytes oder Terabytes an verschwendetem Speicher führen. Und zwar nicht in einem Monat, sondern an einem einzigen Tag durch einen einzigen Desktop-Nutzer. Daher kann eine geringe Ineffizienz schnell zu übermäßigen Hardwareressourcen führen.
Es gibt noch mehr. Um sich eine Gesamtzuweisung von 10 TB leisten zu können, reicht es nicht aus, einen Server mit so viel Speicher zu kaufen und für den Energieverbrauch zu bezahlen. Zu den amortisierten Kosten gehören auch das Schreiben, der Kauf oder zumindest die Wartung von Firmware, Treibern, Betriebssystemen und Software zur Überwachung, Aktualisierung und zum Betrieb des Servers. Da wir für zusätzliche Hardware auch zusätzliche Software benötigen, müssen wir per Definition Geld für Ingenieure ausgeben. Wenn wir uns nicht auf Leistungsoptimierungen konzentrieren würden, könnten wir Entwicklungskosten sparen. Im Gegenzug würden wir mehr Geld für andere Ingenieure ausgeben, die für die Wartung der überbeanspruchten Ressourcen benötigt werden, oder einen Cloud-Provider bezahlen, der diese zusätzlichen Kosten plus Gewinn bereits in die Rechnung für die Cloud-Nutzung eingerechnet hat.
Andererseits kosten 10 TB Speicher heute viel, aber morgen könnten die Kosten aufgrund des technologischen Fortschritts nur noch marginal sein. Was wäre, wenn wir Leistungsprobleme ignorieren und warten, bis die Serverkosten sinken oder mehr Nutzer/innen ihre Laptops oder Telefone durch schnellere Geräte ersetzen? Abwarten ist einfacher, als knifflige Leistungsprobleme zu beheben!
Leider können wir nicht auf die Effizienz der Softwareentwicklung verzichten und erwarten, dass die Fortschritte bei der Hardware die Bedürfnisse und Leistungsfehler abmildern. Die Hardware wird immer schneller und leistungsfähiger, ja. Aber leider nicht schnell genug. Schauen wir uns die drei Hauptgründe für diesen nicht intuitiven Effekt an.
Die Software erweitert sich, um den verfügbaren Speicher zu füllen
Dieser Effekt ist als Parkinsons Gesetz bekannt.14 Es besagt, dass unabhängig davon, wie viele Ressourcen wir haben, die Nachfrage tendenziell dem Angebot entspricht. Das Parkinsonsche Gesetz ist zum Beispiel an den Universitäten deutlich sichtbar. Egal, wie viel Zeit der Professor für Aufgaben oder Prüfungsvorbereitungen zur Verfügung stellt, die Studierenden werden sie immer voll ausschöpfen und das meiste davon wahrscheinlich in letzter Minute erledigen.15 Ein ähnliches Verhalten können wir auch in der Softwareentwicklung beobachten.
Software wird schneller langsamer als Hardware schneller wird
Niklaus Wirth erwähnt einen Begriff für "fette Software", der erklärt, warum es immer mehr Nachfrage nach mehr Hardware geben wird.
Mehr Hardware-Leistung war zweifellos der Hauptanreiz für die Anbieter, komplexere Probleme anzugehen.... Aber es ist nicht die inhärente Komplexität, die uns Sorgen machen sollte; es ist die selbstverschuldete Komplexität. Es gibt viele Probleme, die schon vor langer Zeit gelöst wurden, aber für dieselben Probleme werden uns jetzt Lösungen angeboten, die in viel umfangreichere Software verpackt sind.
Niklaus Wirth, "Ein Plädoyer für schlanke Software"
Die Software wird schneller langsamer als die Hardware leistungsfähiger, weil die Produkte in ein besseres Benutzererlebnis investieren müssen, um rentabel zu sein. Dazu gehören hübschere Betriebssysteme, leuchtende Icons, komplexe Animationen, hochauflösende Videos auf Websites oder ausgefallene Emojis, die dank Gesichtserkennungstechniken deinen Gesichtsausdruck imitieren. Es ist ein ständiger Kampf um die Kunden, der zu mehr Komplexität und damit zu höheren Anforderungen an die Rechenleistung führt.
Hinzu kommt die rasante Demokratisierung von Software dank des besseren Zugangs zu Computern, Servern, Mobiltelefonen, IoT-Geräten und jeder anderen Art von Elektronik. Wie Marc Andreessen sagte: "Software frisst die Welt". Die COVID-19-Pandemie, die Ende 2019 begann, beschleunigte die Digitalisierung noch mehr, da internetbasierte Dienste zum entscheidenden Rückgrat der modernen Gesellschaft wurden. Wir haben zwar jeden Tag mehr Rechenleistung zur Verfügung, aber immer mehr Funktionen und Benutzerinteraktionen verbrauchen sie und verlangen nach noch mehr davon. Letztendlich würde ich behaupten, dass unser überstrapaziertes 1 MB in der oben erwähnten Einzelfunktion ziemlich schnell zu einem kritischen Engpass in diesem Ausmaß werden könnte.
Wenn dir das immer noch sehr hypothetisch vorkommt, schau dir einfach die Software um dich herum an. Wir nutzen soziale Medien, wo allein Facebook 4 PB16 an Daten pro Tag. Wir suchen online und Google verarbeitet dabei 20 PB Daten pro Tag. Das sind allerdings seltene, planetare Systeme mit Milliarden von Nutzern. Typische Entwickler haben solche Probleme nicht, richtig? Wenn ich mir die meisten der von mir mitentwickelten oder genutzten Programme anschaue, stoßen sie früher oder später auf Leistungsprobleme, die mit der hohen Datennutzung zusammenhängen. ZumBeispiel:
-
Eine in React geschriebene Prometheus-UI-Seite führte eine Suche nach Millionen von Metriknamen durch oder versuchte, Hunderte von Megabyte an komprimierten Samples abzurufen, was zu Latenzen im Browser und einer explosionsartigen Speichernutzung führte.
-
Bei geringer Nutzung erzeugte ein einzelner Kubernetes-Cluster in unserer Infrastruktur täglich 0,5 TB an Logs (die meisten davon wurden nie genutzt).
-
Das hervorragende Grammatikprüfungsprogramm, das ich beim Schreiben dieses Buches verwendet habe, machte zu viele Netzwerkanrufe, wenn der Text mehr als 20.000 Wörter hatte, was meinen Browser erheblich verlangsamte.
-
Unser einfaches Skript zur Formatierung unserer Dokumentation in Markdown und zur Überprüfung von Links brauchte Minuten, um alle Elemente zu verarbeiten.
-
Unser Go-Job zur statischen Analyse und das Linting überstiegen 4 GB Speicher und brachten unsere CI-Aufträge zum Absturz.
-
Meine IDE brauchte 20 Minuten, um den gesamten Code unseres Mono-Repos zu indizieren, obwohl ich das auf einem erstklassigen Laptop gemacht habe.
-
Ich habe meine 4K-Ultrawide-Videos von GoPro immer noch nicht bearbeitet, weil die Software zu verzögert ist.
Ich könnte ewig mit Beispielen weitermachen, aber der Punkt ist, dass wir in einer wirklich "großen Datenwelt" leben. Deshalb müssen wir den Speicher und andere Ressourcen sinnvoll nutzen.
In Zukunft wird es noch viel schlimmer werden. Unsere Soft- und Hardware muss das extreme Datenwachstum bewältigen, schneller als jede Hardwareentwicklung. Wir stehen kurz vor der Einführung von 5G-Netzen, die bis zu 20 Gigabit pro Sekunde übertragen können. Wir führen Mini-Computer in fast jedem Gegenstand ein, den wir kaufen, z. B. in Fernsehern, Fahrrädern, Waschmaschinen, Gefrierschränken, Schreibtischlampen oder sogar Deodorants! Wir nennen diese Bewegung das "Internet der Dinge" (IoT). Es wird geschätzt, dass die Daten dieser Geräte von 18,3 ZB im Jahr 2019 auf 73,1 ZB im Jahr 2025 ansteigen werden.17 Die Industrie kann 8K-Fernseher herstellen, die Auflösungen von 7.680 × 4.320, also etwa 33 Millionen Pixel, wiedergeben. Wenn du schon einmal Computerspiele geschrieben hast, kennst du dieses Problem wahrscheinlich gut - es ist sehr aufwändig, so viele Pixel in hochrealistischen Spielen mit immersiven, stark zerstörerischen Umgebungen bei 60+ Bildern pro Sekunde zu rendern. Moderne Kryptowährungen und Blockchain-Algorithmen stellen auch eine Herausforderung für die Energieeffizienz von Berechnungen dar. So verbrauchte der Bitcoin während des Wertmaximums etwa 130 Terawattstunden Energie (0,6 % des weltweiten Stromverbrauchs).
Technologische Grenzen
Der letzte, aber nicht unwichtigste Grund für die nicht schnell genug voranschreitende Hardwareentwicklung ist, dass die Hardwareentwicklung an einigen Fronten wie der CPU-Geschwindigkeit (Taktrate) oder der Speicherzugriffsgeschwindigkeit ins Stocken geraten ist. Wir werden uns in Kapitel 4 mit den Herausforderungen dieser Situation befassen, aber ich glaube, jeder Entwickler sollte sich der grundlegenden technologischen Grenzen bewusst sein, an die wir gerade stoßen.
Es wäre merkwürdig, ein modernes Buch über Effizienz zu lesen, in dem das Mooresche Gesetz nicht erwähnt wird, oder? Du hast wahrscheinlich schon einmal davon gehört. Es wurde erstmals 1965 vom ehemaligen CEO und Mitbegründer von Intel, Gordon Moore, formuliert.
Die Komplexität für minimale Bauteilkosten [die Anzahl der Transistoren, mit minimalen Herstellungskosten pro Chip] ist etwa um den Faktor zwei pro Jahr gestiegen. ... Längerfristig ist die Steigerungsrate etwas unsicherer, obwohl es keinen Grund gibt, anzunehmen, dass sie nicht mindestens 10 Jahre lang nahezu konstant bleiben wird. Das bedeutet, dass im Jahr 1975 die Anzahl der Bauteile pro integriertem Schaltkreis bei minimalen Kosten 65.000 betragen wird.
Gordon E. Moore, "Cramming More Components onto Integrated Circuits", Electronics 38 (1965)
Moores Beobachtung hatte einen großen Einfluss auf die Halbleiterindustrie. Aber die Verkleinerung der Transistoren wäre nicht so vorteilhaft gewesen, wenn Robert H. Dennard und sein Team nicht gewesen wären. Im Jahr 1974 fanden sie heraus, dass der Stromverbrauch proportional zur Größe des Transistors ist (konstante Leistungsdichte).18 Das bedeutet, dass kleinere Transistoren stromsparender sind. Letztlich versprachen beide Gesetze ein exponentielles Wachstum der Leistung pro Watt bei Transistoren. Das motivierte die Investoren, ständig nach Möglichkeiten zu forschen und zu entwickeln, um die Größe der MOSFET19 Transistoren zu verringern. Außerdem können wir mehr von ihnen auf noch kleineren, dichteren Mikrochips unterbringen, was die Herstellungskosten senkt. Die Industrie verringerte kontinuierlich den Platzbedarf für die gleiche Menge an Rechenleistung und verbesserte jeden Chip, von der CPU über RAM und Flash-Speicher bis hin zu GPS-Empfängern und hochauflösenden Kamerasensoren.
In der Praxis hat Moores Vorhersage nicht 10 Jahre gehalten, wie er dachte, sondernbisher fast 60, und sie gilt immer noch. Wir erfinden immer winzigere, mikroskopisch kleine Transistoren, die sich derzeit um ~70 nm bewegen. Wahrscheinlich können wir sie sogar noch kleiner machen. Leider haben wir, wie in Abbildung 1-3 zu sehen ist, die physikalische Grenze der Dennard'schen Skalierung um 2006 erreicht.20
Technisch gesehen bleibt der Stromverbrauch durch die höhere Dichte der winzigen Transistoren zwar konstant, aber solche dichten Chips erhitzen sich schnell. Bei einer Taktfrequenz von mehr als 3-4 GHz werden deutlich mehr Strom und andere Kosten für die Kühlung der Transistoren benötigt, um sie am Laufen zu halten. Wenn du also nicht vorhast, Software auf dem Grund des Ozeans laufen zu lassen,21 wirst du in absehbarer Zeit keine CPUs mit schnellerer Befehlsausführung bekommen. Wir können nur mehr Kerne haben.
Schnellere Ausführung ist energieeffizienter
Was haben wir also bisher gelernt? Die Geschwindigkeit der Hardware ist begrenzt, die Software wird immer umfangreicher und wir müssen mit dem ständigen Wachstum von Daten und Nutzern umgehen. Leider ist das noch nicht das Ende. Es gibt eine lebenswichtige Ressource, die wir bei der Entwicklung der Software oft vergessen: Strom. Jede Berechnung unseres Prozesses benötigt Strom, der auf vielen Plattformen wie Handys, Smartwatches, IoT-Geräten oder Laptops stark begrenzt ist. Es ist nicht intuitiv, dass es eine starke Korrelation zwischen Energieeffizienz und der Geschwindigkeit und Effizienz von Software gibt. Ich liebe die Präsentation von Chandler Carruth, die diesen überraschenden Zusammenhang gut erklärt:
Wenn du jemals etwas über "stromsparende Anweisungen" oder "Optimierung des Stromverbrauchs" gelesen hast, solltest du sehr misstrauisch werden. ... Das ist meist völliger wissenschaftlicher Schrott. Hier ist die führende Theorie darüber, wie man Akkulaufzeit sparen kann: Beende die Ausführung des Programms. Im Ernst: Rase in den Schlaf. Je schneller deine Software läuft, desto weniger Strom verbraucht sie. ... Jeder Mikroprozessor, den du heute kaufen kannst, spart Strom, indem er sich selbst ausschaltet. Und zwar so schnell und so häufig wie möglich.
Chandler Carruth, "Efficiency with Algorithms, Performance with Data Structures", CppCon 2014
Zusammenfassend lässt sich sagen: Vermeide die häufige Falle, Hardware als eine immer schnellere und billigere Ressource zu betrachten, die uns von der Optimierung unseres Codes befreit. Das ist eine Falle. Eine solche kaputte Schleife führt dazu, dass Ingenieure ihre Ansprüche an die Leistung ihres Codes immer weiter senken und immer mehr und schnellere Hardware verlangen. Billigere und leichter zugängliche Hardware schafft dann noch mehr geistigen Spielraum, um die Effizienz zu überspringen und so weiter. Es gibt erstaunliche Innovationen wie die M1-Silikone von Apple,22 RISC-V Standard,23 und praktischere Quantum-Computing-Geräte, die viel versprechen. Leider wächst die Hardware ab 2022 langsamer, als die Software Effizienz benötigt.
Effizienz verbessert die Zugänglichkeit und Inklusion
Softwareentwickler/innen sind in Bezug auf die Maschinen, die wir benutzen, oft "verwöhnt" und von der typischen menschlichen Realität abgekoppelt. Oft wird Software auf hochwertigen Laptops oder mobilen Geräten entwickelt und getestet. Wir müssen uns bewusst machen, dass viele Menschen und Organisationen ältere Hardware oder schlechtere Internetverbindungen nutzen.24 Es kann sein, dass die Menschen deine Anwendungen auf langsameren Computern ausführen müssen. Es könnte sich lohnen, die Effizienz in unserem Entwicklungsprozess zu berücksichtigen, um die Zugänglichkeit der Software insgesamt zu verbessern unddieeinzubeziehen.
Wir können stattdessen horizontal skalieren
Wie wir in den vorherigen Abschnitten gelernt haben, erwarten wir, dass unsere Software früher oder später mehr Daten verarbeiten wird. Aber es ist unwahrscheinlich, dass dein Projekt vom ersten Tag an Milliarden von Nutzern haben wird. Wir können enorme Softwarekomplexität und Entwicklungskosten vermeiden, indem wir zu Beginn unseres Entwicklungszyklus pragmatisch eine viel geringere Anzahl von Nutzern, Vorgängen oder Datengrößen anstreben. Zum Beispiel vereinfachen wir den anfänglichen Programmierzyklus, indem wir von einer geringen Anzahl von Notizen in der mobilen Notiz-App, von weniger Anfragen pro Sekunde in dem zu entwickelnden Proxy oder von kleineren Dateien in dem Datenkonvertierungstool ausgehen, an dem das Team gerade arbeitet. Es ist in Ordnung, die Dinge zu vereinfachen. Es ist auch wichtig, die Leistungsanforderungen in der frühen Entwurfsphase grob vorherzusagen.
Ebenso wichtig ist es, die erwartete Auslastung und Nutzung der Software mittel- bis langfristig zu ermitteln. Das Softwaredesign, das auch bei erhöhter Auslastung ein ähnliches Leistungsniveau garantiert, ist skalierbar. Im Allgemeinen ist Skalierbarkeit in der Praxis sehr schwer und teuer zu erreichen.
Auch wenn ein System heute zuverlässig funktioniert, heißt das nicht, dass es auch in Zukunft zuverlässig funktionieren wird. Ein häufiger Grund für eine Verschlechterung ist die gestiegene Belastung: Vielleicht ist das System von 10.000 gleichzeitigen Nutzern auf 100.000 gleichzeitige Nutzer angewachsen, oder von 1 Million auf 10 Millionen. Vielleicht verarbeitet es auch viel größere Datenmengen als zuvor. Skalierbarkeit ist der Begriff, den wir verwenden, um die Fähigkeit eines Systems zu beschreiben, mit erhöhter Last umzugehen.
Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017)
Wenn wir über Effizienz sprechen, kommen wir in diesem Buch zwangsläufig auch auf das Thema Skalierbarkeit zu sprechen. Für die Zwecke dieses Kapitels können wir jedoch die Skalierbarkeit unserer Software in zwei Typen unterscheiden, die in Abbildung 1-4 dargestellt sind.
- Vertikale Skalierbarkeit
-
Die erste und manchmal einfachste Möglichkeit, unsere Anwendung zu skalieren, besteht darin, die Software auf Hardware mit mehr Ressourcen laufen zu lassen - "vertikale" Skalierbarkeit. Wir könnten zum Beispiel Parallelität für die Software einführen, um nicht nur einen, sondern drei CPU-Kerne zu nutzen. Wenn die Last steigt, stellen wir mehr CPU-Kerne zur Verfügung. Wenn unser Prozess speicherintensiv ist, können wir die Anforderungen an den Arbeitsspeicher erhöhen und mehr Speicherplatz verlangen. Dasselbe gilt für jede andere Ressource wie Festplatte, Netzwerk oder Strom. Das bleibt natürlich nicht ohne Folgen. Im besten Fall hast du diesen Platz auf dem Zielrechner. Möglicherweise kannst du diesen Platz schaffen, indem du andere Prozesse auf andere Rechner verlegst (z. B. wenn du in der Cloud arbeitest) oder sie vorübergehend schließt (nützlich, wenn du auf einem Laptop oder Smartphone arbeitest). Im schlimmsten Fall musst du einen größeren Computer oder ein leistungsfähigeres Smartphone oder Laptop kaufen. Letzteres ist in der Regel nur sehr eingeschränkt möglich, vor allem, wenn du Software für Kunden anbietest, die nicht in der Cloud laufen. Schließlich ist die Benutzerfreundlichkeit von ressourcenhungrigen Anwendungen oder Websites, die nur vertikal skalieren, viel geringer.
Die Situation ist etwas besser, wenn du oder deine Kunden deine Software in der Cloud betreiben. Du kannst "einfach" einen größeren Server kaufen. Ab 2022 kannst du deine Software auf der AWS-Plattform auf 128 CPU-Kerne, fast 4 TB Arbeitsspeicher und 14 GBit/s Bandbreite aufstocken.25 Im Extremfall kannst du auch einen IBM-Großrechner mit 190 Kernen und 40 TB Arbeitsspeicher kaufen, der andere Programmierparadigmen erfordert.
Leider hat die vertikale Skalierbarkeit in vielerlei Hinsicht ihre Grenzen. Selbst in der Cloud oder in Rechenzentren können wir die Hardware nicht unendlich hoch skalieren. Erstens sind riesige Maschinen selten und teuer. Zweitens, wie wir in Kapitel 4 lernen werden, stoßen größere Maschinen auf komplexe Probleme, die durch viele versteckte einzelne Fehlerpunkte verursacht werden. Teile wie der Speicherbus, Netzwerkschnittstellen, NUMA-Knoten und das Betriebssystem selbst können überlastet und zu langsam sein.26
- Horizontale Skalierbarkeit
-
Anstelle einer größeren Maschine könnten wir versuchen, die Berechnungen auf mehrere kleinere, weniger komplexe und viel billigere Geräte zu verteilen. Zum Beispiel:
-
Um in einer mobilen Messaging-App nach Nachrichten mit dem Wort "Zuhause" zu suchen, könnten wir Millionen vergangener Nachrichten abrufen (oder sie überhaupt erst lokal speichern) und einen Regex-Abgleich für jede einzelne durchführen. Stattdessen können wir eine API entwickeln und per Fernzugriff ein Backend-System aufrufen, das die Suche in 100 Aufträge aufteilt, die 1/100 des Datensatzes entsprechen.
-
Anstatt "monolithische" Software zu bauen, können wir verschiedene Funktionen auf separate Komponenten verteilen und zu einem "Microservice"-Design übergehen.
-
Anstatt ein Spiel, das teure CPUs und GPUs benötigt, auf einem PC oder einer Spielkonsole laufen zu lassen, könnten wir es in einer Cloud laufen lassen und die Ein- und Ausgabe in hoher Auflösung streamen.
-
Die horizontale Skalierbarkeit ist einfacher zu nutzen, da sie weniger Einschränkungen hat und in der Regel eine große Dynamik zulässt. Wenn die Software z. B. nur in einem bestimmten Unternehmen eingesetzt wird, kann es sein, dass du nachts fast keine Nutzer/innen hast, aber tagsüber ein hohes Aufkommen. Mit horizontaler Skalierbarkeit ist es einfach, eine automatische Skalierung zu implementieren, die je nach Bedarf in Sekundenschnelle aus- und wieder eingeht.
Andererseits ist horizontale Skalierbarkeit auf der Softwareseite viel schwieriger zu realisieren. Verteilte Systeme, Netzwerkauswirkungen und schwierige Probleme, die nicht aufgeteilt werden können, sind einige der vielen Komplikationen bei der Entwicklung solcher Systeme. Deshalb ist es in manchen Fällen besser, sich an die vertikale Skalierbarkeit zu halten.
Mit Blick auf die horizontale und vertikale Skalierbarkeit wollen wir uns ein bestimmtes Szenario aus der Vergangenheit ansehen. Viele moderne Datenbanken nutzen die Verdichtung, um Daten effizient zu speichern und nachzuschlagen. Dabei können wir viele Indizes wiederverwenden, dieselben Daten deduplizieren und fragmentierte Teile in den sequenziellen Datenstrom aufnehmen, um sie schneller zu lesen. Zu Beginn des Thanos-Projekts beschlossen wir, der Einfachheit halber einen sehr naiven Verdichtungsalgorithmus zu verwenden. Wir haben errechnet, dass es theoretisch nicht nötig ist, den Verdichtungsprozess innerhalb eines einzelnen Datenblocks zu parallelisieren. Bei einem stetigen Strom von 100 GB (oder mehr) verdichteter Daten aus einer einzigen Quelle könnten wir mit einer einzigen CPU, einer minimalen Menge an Speicher und etwas Festplattenplatz auskommen. Die Implementierung war anfangs sehr naiv und unoptimiert. Wir folgten der YAGNI-Regel und vermieden vorschnelle Optimierungsmuster. Wir wollten die Komplexität und den Aufwand vermeiden, der mit der Optimierung der Zuverlässigkeits- und Funktionsmerkmale des Projekts verbunden ist. Infolgedessen stießen Nutzer, die unser Projekt einsetzten, schnell auf Verdichtungsprobleme: Sie waren zu langsam, um die eingehenden Daten zu bewältigen, oder verbrauchten Hunderte von GB Speicher pro Vorgang. Die Kosten waren das erste Problem, aber nicht das dringendste. Das größere Problem war, dass viele Thanos-Nutzer keine größeren Maschinen in ihren Rechenzentren hatten, um den Speicher vertikal zu skalieren.
Auf den ersten Blick sah das Verdichtungsproblem wie ein Skalierbarkeitsproblem aus. Der Verdichtungsprozess war auf Ressourcen angewiesen, die wir nicht einfach unendlich aufstocken konnten. Da die Nutzerinnen und Nutzer schnell eine Lösung wollten, begannen wir gemeinsam mit der Community ein Brainstorming über mögliche horizontale Skalierungstechniken. Wir sprachen über die Einführung eines Zeitplannungsprogramms, das die Verdichtungsaufträge verschiedenen Maschinen zuweist, oder über intelligente Peer-Netzwerke, die ein Gossip-Protokoll verwenden. Ohne ins Detail zu gehen, würden beideLösungen zu einer enormen Komplexität führen, die denAufwand für die Entwicklung und den Betrieb des gesamten Systems wahrscheinlich verdoppeln oder verdreifachen würde. Zum Glück brauchten mutige und erfahrene Entwickler ein paar Tage, um den Code so umzugestalten, dass er effizienter und leistungsfähiger wurde. Dadurch konnte die neuere Version von Thanos doppelt so schnell verdichten und die Daten direkt von der Festplatte streamen, was einen minimalen Spitzenspeicherverbrauch ermöglichte. Einige Jahre später verfügt das Thanos-Projekt immer noch nicht über eine komplexe horizontale Skalierbarkeit für die Verdichtung, abgesehen von einfachem Sharding, selbst wenn Tausende von erfolgreichen Nutzern es mit Milliarden von Metriken betreiben.
Es mag sich jetzt komisch anfühlen, aber in gewisser Weise ist diese Geschichte ziemlich beängstigend. Wir waren so kurz davor, eine enorme, verteilte Komplexität auf Systemebene einzuführen, die auf sozialem Druck und dem Druck der Kunden beruhte. Es würde Spaß machen, sie zu entwickeln, aber es könnte auch die Akzeptanz des Projekts zum Einsturz bringen. Vielleicht fügen wir es eines Tages hinzu, aber zuerst werden wir sicherstellen, dass es keine andere Effizienzoptimierung zur Verdichtung gibt. Eine ähnliche Situation hat sich in meiner Karriere sowohl in offenen als auch in geschlossenen Quellen für kleinere und größere Projekte wiederholt.
Verfrühte Skalierbarkeit ist schlimmer als verfrühte Effizienzoptimierungen!
Achte darauf, dass du die Effizienz auf der Algorithmus- und Codeebene verbesserst, bevor du komplexe skalierbare Muster einführst.
Wenn wir uns nicht auf die Effizienz unserer Software konzentrieren, können wir schnell gezwungen sein, eine verfrühte horizontale Skalierbarkeit einzuführen, wie das Beispiel der "glücklichen" Thanos-Verdichtung zeigt. Das ist eine gewaltige Falle, denn mit etwas Optimierungsaufwand könnten wir den Sprung in die Komplikationen der Skalierbarkeitsmethode komplett vermeiden. Mit anderen Worten: Die Vermeidung von Komplexität kann zu noch größerer Komplexität führen. Das scheint mir ein unbemerktes, aber kritisches Problem in der Branche zu sein. Es ist auch einer der Hauptgründe, warum ich dieses Buch geschrieben habe.
Die Komplikationen ergeben sich aus der Tatsache, dass die Komplexität irgendwo untergebracht werden muss. Wir wollen den Code nicht verkomplizieren, also müssen wir das System verkomplizieren, was, wenn es aus ineffizienten Komponenten aufgebaut ist, Ressourcen und eine enorme Menge an Entwickler- oder Bedienerzeit verschwendet. Die horizontale Skalierbarkeit ist besonders komplex. Sie beinhaltet von vornherein Netzwerkoperationen. Wie wir vielleicht aus dem CAP-Theorem wissen,27 stoßen wir unweigerlich auf Verfügbarkeits- oder Konsistenzprobleme, sobald wir anfangen, unsere Prozesse zu verteilen. Glaub mir, diese elementaren Einschränkungen zu mildern, mit Race Conditions umzugehen und die Welt der Netzwerklatenzen und Unvorhersehbarkeiten zu verstehen, ist hundertmal schwieriger, als eine kleine Effizienzoptimierung hinzuzufügen, die sich z. B. hinter der Schnittstelle io.Reader
versteckt.
Es mag dir so vorkommen, als würde sich dieser Abschnitt nur mit Infrastruktursystemen befassen. Das ist nicht wahr. Er gilt für jede Software. Wenn du zum Beispiel eine Frontend-Software oder eine dynamische Website schreibst, könntest du versucht sein, kleine Client-Berechnungen in das Backend zu verlagern. Das sollte man aber nur dann tun, wenn die Berechnung von der Last abhängt und die Hardwarekapazitäten des Userspaces übersteigt. Eine verfrühte Verlagerung auf den Server könnte dich die Komplexität kosten, die durch zusätzliche Netzwerkaufrufe, mehr zu bearbeitende Fehlerfälle und eine Überlastung des Servers mit Denial of Service (DoS) verursacht wird.28
Ein weiteres Beispiel stammt aus meiner Erfahrung. In meiner Masterarbeit ging es um eine "Particle Engine Using Computing Cluster". Im Prinzip ging es darum, einem 3D-Spiel in der Unity-Engine eine Partikel-Engine hinzuzufügen. Der Trick war, dass die Partikel-Engine nicht auf den Client-Rechnern laufen sollte, sondern die "teuren" Berechnungen auf einen nahe gelegenen Supercomputer meiner Universität namens "Tryton" auslagern sollte.29 Weißt du was? Trotz des ultraschnellen InfiniBand-Netzwerks,30 waren alle Partikel, die ich zu simulieren versuchte (realistischer Regen und Menschenmengen), viel langsamer und weniger zuverlässig, wenn sie auf unseren Supercomputer ausgelagert wurden. Es war nicht nur weniger komplex, sondern auch viel schneller, alles auf Client-Rechnern zu berechnen.
Zusammenfassend lässt sich sagen: Wenn jemand sagt: "Wir brauchen nicht zu optimieren, wir können einfach horizontal skalieren", sei sehr misstrauisch. In der Regel ist es einfacher und billiger, mit Effizienzverbesserungen zu beginnen, bevor wir die Skalierbarkeit anstreben. Andererseits sollte ein Urteil dir sagen, wann Optimierungen zu komplex werden und Skalierbarkeit eine bessere Option sein könnte. Mehr dazu erfährst du in Kapitel 3.
Die Zeit bis zur Markteinführung ist wichtiger
Zeit ist teuer. Ein Aspekt davon ist, dass die Zeit und das Fachwissen von Softwareentwicklern viel kosten. Je mehr Funktionen deine Anwendung oder dein System haben soll, desto mehr Zeit wird benötigt, um die Lösung zu entwerfen, zu implementieren, zu testen, zu sichern und ihre Leistung zu optimieren. Der zweite Aspekt ist, dass je mehr Zeit ein Unternehmen oder eine Einzelperson aufwendet, um das Produkt oder die Dienstleistung bereitzustellen, desto länger ist die "Markteinführungszeit", was sich negativ auf die finanziellen Ergebnisse auswirken kann.
Früher war Zeit Geld. Jetzt ist sie wertvoller als Geld. Eine McKinsey-Studie zeigt, dass Unternehmen im Durchschnitt 33% ihres Gewinns nach Steuern verlieren, wenn sie Produkte sechs Monate zu spät auf den Markt bringen, verglichen mit Verlusten von 3,5%, wenn sie 50% mehr für die Produktentwicklung ausgeben.
Charles H. House und Raymond L. Price, "Die Rückkehrkarte: Produktteams aufspüren"
Es ist schwer, solche Auswirkungen zu messen, aber dein Produkt ist vielleicht nicht mehr bahnbrechend, wenn du "spät" auf den Markt kommst. Du könntest wertvolle Chancen verpassen oder zu spät auf das neue Produkt eines Wettbewerbers reagieren. Deshalb mindern Unternehmen dieses Risiko, indem sie agile Methoden oder Proof of Concept (POC) und Minimal Viable Products (MVP) einsetzen.
Agile und kleinere Iterationen helfen, aber letztendlich versuchen Unternehmen, um schnellere Entwicklungszyklen zu erreichen, auch andere Dinge: Sie vergrößern ihre Teams (stellen mehr Leute ein, gestalten Teams um), vereinfachen das Produkt, automatisieren es stärker oder gehen Partnerschaften ein. Manchmal versuchen sie auch, die Produktqualität zu verringern. Das stolze Motto von Facebook lautete anfangs "Move fast and break things".31 ist es üblich, dass Unternehmen die Softwarequalität in Bereichen wie Code-Wartbarkeit, Zuverlässigkeit und Effizienz herabsetzen, um den Markt zu "schlagen".
Genau darum geht es bei unserem letzten Missverständnis. Die Effizienz deiner Software zu reduzieren, um schneller auf den Markt zu kommen, ist nicht immer die beste Idee. Es ist gut, wenn du die Konsequenzen einer solchen Entscheidung kennst. Kenne zuerst das Risiko.
Optimierung ist ein schwieriger und teurer Prozess. Viele Ingenieure argumentieren, dass dieser Prozess den Markteintritt verzögert und den Gewinn schmälert. Das mag stimmen, aber es ignoriert die Kosten, die mit schlecht funktionierenden Produkten verbunden sind (vor allem, wenn es auf dem Markt Konkurrenz gibt).
Randall Hyde, "Der Irrtum der vorzeitigen Optimierung"
Bugs, Sicherheitsprobleme und schlechte Leistung kommen vor, aber sie können dem Unternehmen schaden. Ohne zu weit auszuholen, schauen wir uns ein Spiel an, das Ende 2020 von CD Projekt, dem größten polnischen Spieleverlag, veröffentlicht wurde. Cyberpunk 2077 war bekannt dafür, dass es eine sehr ehrgeizige, riesige und qualitativ hochwertige Open-World-Produktion ist. Gut vermarktet, von einem Verlag mit einem guten Ruf, kauften begeisterte Spieler/innen auf der ganzen Welt trotz der Verzögerungen acht Millionen Vorbestellungen. Leider hatte das ansonsten hervorragende Spiel bei seiner Veröffentlichung im Dezember 2020 massive Leistungsprobleme. Es hatte Bugs, Abstürze und eine niedrige Bildrate auf allen Konsolen und den meisten PC-Setups. Auf einigen älteren Konsolen wie PS4 oder Xbox One war das Spiel angeblich unspielbar. Natürlich gab es in den darauffolgenden Monaten und Jahren zahlreiche Updates, die das Problem behoben und drastische Verbesserungen brachten.
Leider war es zu spät. Der Schaden war angerichtet. Die für mich eher unbedeutenden Probleme reichten aus, um die finanziellen Perspektiven von CD Projekt zu erschüttern. Fünf Tage nach dem Start verlor das Unternehmen ein Drittel seines Aktienwerts und kostete die Gründer mehr als 1 Milliarde Dollar. Millionen von Spielern verlangten eine Rückerstattung des Spiels. Investoren verklagten CD Projekt wegen Spielproblemen, und berühmte Hauptentwickler verließen das Unternehmen. Vielleicht wird der Verlag überleben und sich erholen. Dennoch kann man sich nur vorstellen, welche Auswirkungen ein beschädigter Ruf auf zukünftige Produktionen haben wird.
Erfahrenere und reifere Unternehmen kennen den kritischen Wert der Softwareleistung, vor allem bei kundenorientierten Unternehmen. Amazon fand heraus, dass das Unternehmen jährlich 1,6 Milliarden Dollar verlieren würde, wenn seine Website eine Sekunde langsamer geladen würde. Amazon berichtete auch, dass 100 ms Latenz 1 % des Gewinns kosten. Google stellte fest, dass die Verlangsamung der Websuche von 400 ms auf 900 ms zu einem Rückgang der Besucherzahlen um 20 % führte. Für manche Unternehmen ist es sogar noch schlimmer. Es wurde geschätzt, dass die elektronische Handelsplattform eines Brokers, die 5 Millisekunden langsamer ist als die der Konkurrenz, 1 % seines Cashflows verlieren kann, wenn nicht sogar mehr. Wenn sie 10 Millisekunden langsamer ist, erhöht sich diese Zahl auf einen Umsatzrückgang von 10 %.
Realistisch betrachtet ist die Millisekunden-Verzögerung in den meisten Softwarefällen nicht von Bedeutung. Nehmen wir zum Beispiel an, wir wollen einen Dateikonverter von PDF nach DOCX implementieren. Spielt es eine Rolle, ob das Ganze 4 Sekunden oder 100 Millisekunden dauert? In vielen Fällen ist das egal. Aber wenn jemand das als Marktwert ansetzt und das Produkt eines Konkurrenten eine Latenzzeit von 200 Millisekunden hat, werden Code-Effizienz und Geschwindigkeit plötzlich zu einer Frage von Kundengewinn oder -verlust. Und wenn es physikalisch möglich ist, Dateien so schnell zu konvertieren, werden die Konkurrenten früher oder später versuchen, das zu erreichen. Das ist auch der Grund, warum so viele Projekte, selbst Open-Source-Projekte, sehr laut über ihre Leistungsergebnisse sprechen. Auch wenn es sich manchmal wie ein billiger Marketingtrick anfühlt, funktioniert das, denn wenn du zwei ähnliche Lösungen mit ähnlichen Funktionen und anderen Eigenschaften hast, wirst du dich für die schnellste entscheiden. Es geht aber nicht nur um die Geschwindigkeit - auch der Ressourcenverbrauch spielt eine Rolle.
Effizienz ist auf dem Markt oft wichtiger als Eigenschaften!
Während meiner Tätigkeit als Berater für Infrastruktursysteme habe ich viele Fälle erlebt, in denen Kunden von Lösungen abgewichen sind, die mehr Arbeitsspeicher oder eine größere Speicherung auf der Festplatte erfordern, auch wenn das einen gewissen Verlust an Funktionen bedeutet.32
Für mich ist das Urteil einfach. Wenn du den Markt gewinnen willst, ist es vielleicht nicht die beste Idee, die Effizienz deiner Software zu vernachlässigen. Warte mit der Optimierung nicht bis zum letzten Moment. Andererseits ist die Zeit bis zur Markteinführung entscheidend. Deshalb ist es wichtig, dass du in deinem Softwareentwicklungsprozess ein ausreichendes Maß an Effizienzarbeit einplanst. Eine Möglichkeit, dies zu tun, besteht darin, die nichtfunktionalen Ziele frühzeitig festzulegen (siehe "Ressourcenbewusste Effizienzanforderungen"). In diesem Buch werden wir uns darauf konzentrieren, ein gesundes Gleichgewicht zu finden und den Aufwand (und damit die Zeit) für die Verbesserung der Effizienz deiner Software zu reduzieren. Schauen wir uns nun an, wie wir pragmatisch über die Leistung unserer Software nachdenken können.
Der Schlüssel zu pragmatischer Code-Performance
In "Hinter der Leistung" haben wir gelernt, dass Leistung in Genauigkeit, Geschwindigkeit und Effizienz unterteilt wird. Wenn ich in diesem Buch das Wort Effizienz verwende, meine ich natürlich den effizienten Ressourcenverbrauch, aber auch die Geschwindigkeit (Latenz) unseres Codes. In dieser Entscheidung verbirgt sich ein praktischer Vorschlag, wie wir die Leistung unseres Codes in der Produktion einschätzen sollten.
Das Geheimnis dabei ist, dass wir uns nicht mehr ausschließlich auf die Geschwindigkeit und Latenz unseres Codes konzentrieren. Bei nicht spezialisierter Software spielt die Geschwindigkeit im Allgemeinen nur eine untergeordnete Rolle; die Verschwendung und der unnötige Verbrauch von Ressourcen sind es, die zu Verzögerungen führen. Und eine hohe Geschwindigkeit bei schlechter Effizienz bringt immer mehr Probleme als Vorteile mit sich. Deshalb sollten wir uns generell auf die Effizienz konzentrieren. Leider wird das oft übersehen.
Angenommen, du willst von Stadt A nach Stadt B über den Fluss fahren. Du kannst dir ein schnelles Auto schnappen und über eine nahe gelegene Brücke fahren, um schnell in die Stadt B zu kommen. Aber wenn du ins Wasser springst und langsam über den Fluss schwimmst, kommst du viel schneller in die Stadt B. Langsamere Aktionen können immer noch schneller sein, wenn sie effizient durchgeführt werden, zum Beispiel indem man eine kürzere Route wählt. Um die Reiseleistung zu verbessern und den Schwimmer zu schlagen, könnten wir ein schnelleres Auto kaufen, den Straßenbelag verbessern, um den Luftwiderstand zu verringern, oder sogar einen Raketenantrieb einbauen. Wir könnten den Schwimmer schlagen, ja, aber diese drastischen Veränderungen könnten teurer sein, als einfach weniger zu arbeiten und stattdessen ein Boot zu mieten.
Ähnliche Muster gibt es auch in der Software. Nehmen wir an, unser Algorithmus sucht nach bestimmten Wörtern, die auf der Festplatte gespeichert sind, und arbeitet langsam. Da wir mit persistenten Daten arbeiten, ist der langsamste Vorgang in der Regel der Datenzugriff, vor allem wenn unser Algorithmus dies ausgiebig tut. Es ist sehr verlockend, nicht an die Effizienz zu denken und stattdessen einen Weg zu finden, die Nutzer/innen davon zu überzeugen, SSD- statt HDD-Speicherung zu verwenden. Auf diese Weise könnten wir die Latenzzeit um das Zehnfache reduzieren. Das würde die Leistung verbessern, indem wir das Geschwindigkeitselement in der Gleichung erhöhen. Wenn wir einen Weg finden, den aktuellen Algorithmus so zu verbessern, dass die Daten nur ein paar Mal statt eine Million Mal gelesen werden, könnten wir sogar noch niedrigere Latenzzeiten erreichen. Das würde bedeuten, dass wir die gleiche oder sogar eine bessere Wirkung erzielen können, ohne die Kosten zu erhöhen.
Ich möchte vorschlagen, dass wir uns auf die Effizienz konzentrieren und nicht nur auf die Ausführungsgeschwindigkeit. Das ist auch der Grund, warum der Titel dieses Buches " Efficient Go" lautet und nicht etwas allgemeineres und eingängigeres33 wie Ultra Performance Go oder Fastest Go Implementations.
Es geht nicht darum, dass Geschwindigkeit weniger wichtig ist. Sie ist wichtig, und wie du in Kapitel 3 lernen wirst, kannst du effizienteren Code haben, der viel langsamer ist, und umgekehrt. Manchmal musst du eben einen Kompromiss eingehen. Sowohl Geschwindigkeit als auch Effizienz sind wichtig. Beide können sich gegenseitig beeinflussen. In der Praxis hat ein Programm, das weniger Arbeit auf dem kritischen Pfad verrichtet, höchstwahrscheinlich eine geringere Latenzzeit. Im Beispiel HDD versus SDD: Wenn du zu einer schnelleren Festplatte wechselst, kannst du vielleicht einen Teil der Caching-Logik entfernen, was zu einer besseren Effizienz führt: weniger Speicher und CPU-Zeit werden benötigt. Manchmal funktioniert es auch andersherum - wie wir in "Hardware wird schneller und billiger" gelernt haben : Je schneller dein Prozess ist, desto weniger Energie verbraucht er, was dieBatterieeffizienz verbessert.
Ich bin der Meinung, dass wir uns bei der Leistungsverbesserung in erster Linie auf die Verbesserung der Effizienz konzentrieren sollten, bevor wir die Geschwindigkeit erhöhen. Wie du im Abschnitt "Optimierung der Latenz" sehen wirst , konnte ich die Latenz nur durch die Verbesserung der Effizienz um das Siebenfache reduzieren, und das mit nur einem CPU-Kern. Du wirst überrascht sein, dass du manchmal, nachdem du die Effizienz verbessert hast, die gewünschte Latenz erreicht hast! Gehen wir noch ein paar weitere Gründe durch, warum die Effizienz besser sein könnte:
- Es ist viel schwieriger, effiziente Software langsam zu machen.
-
Das ist ähnlich wie die Tatsache, dass lesbarer Code leichter zu optimieren ist. Wie ich bereits erwähnt habe, ist effizienter Code in der Regel besser, weil weniger Arbeit anfällt. In der Praxis bedeutet das auch, dass langsame Software oft ineffizient ist.
- Geschwindigkeit ist anfälliger.
-
Wie du in "Zuverlässigkeit von Experimenten" erfährst , hängt die Latenz des Softwareprozesses von einer Vielzahl externer Faktoren ab. Man kann den Code für eine schnelle Ausführung in einer dedizierten und isolierten Umgebung optimieren, aber er kann viel langsamer sein, wenn er über einen längeren Zeitraum läuft. Irgendwann können die CPUs aufgrund von thermischen Problemen des Servers gedrosselt werden. Andere Prozesse (z. B. regelmäßige Backups) können deine Hauptsoftware überraschenderweise verlangsamen. Das Netzwerk könnte gedrosselt sein. Es gibt eine Menge versteckter Unbekannter, die wir berücksichtigen müssen, wenn wir nur auf die Ausführungsgeschwindigkeit programmieren. Deshalb ist die Effizienz das, was wir als Programmierer/innen am besten kontrollieren können.
- Geschwindigkeit ist weniger tragbar.
-
Wenn wir nur auf Geschwindigkeit optimieren, können wir nicht davon ausgehen, dass es genauso funktioniert, wenn wir unsere Anwendung vom Entwicklerrechner auf einen Server oder zwischen verschiedenen Client-Geräten verschieben. Unterschiedliche Hardware, Umgebungen und Betriebssysteme können die Latenzzeit unserer Anwendung diametral verändern. Deshalb ist es wichtig, Software so zu entwickeln, dass sie effizient ist. Erstens gibt es weniger Dinge, die beeinträchtigt werden können. Zweitens: Wenn du auf deinem Entwicklerrechner zwei Aufrufe an die Datenbank machst, ist die Wahrscheinlichkeit groß, dass du die gleiche Anzahl von Aufrufen machst, egal ob du sie auf einem IoT-Gerät in der Raumstation oder einem ARM-basierten Großrechner einsetzt.
Im Allgemeinen ist Effizienz etwas, das wir gleich nach oder zusammen mit der Lesbarkeit tun sollten. Wir sollten schon zu Beginn des Softwaredesigns darüber nachdenken. Ein gesundes Bewusstsein für Effizienz führt, wenn man es nicht übertreibt, zu einer robusten Entwicklungshygiene. Es ermöglicht uns, dumme Leistungsfehler zu vermeiden, die in späteren Entwicklungsphasen nur schwer zu verbessern sind. Wenn wir weniger arbeiten, wird auch die Gesamtkomplexität des Codes reduziert und die Wartbarkeit des Codes und dieErweiterbarkeit verbessert.
Zusammenfassung
Ich glaube, es ist sehr verbreitet, dass Entwickler ihren Entwicklungsprozess mit Kompromissen im Kopf beginnen. Wir setzen uns oft mit der Einstellung hin, dass wir von Anfang an auf bestimmte Softwarequalitäten verzichten müssen. Uns wird oft beigebracht, dass wir Qualitäten unserer Software wie Effizienz, Lesbarkeit, Testbarkeit usw. opfern müssen, um unsere Ziele zu erreichen.
In diesem Kapitel möchte ich dich ermutigen, etwas ehrgeiziger und gieriger zu sein, wenn es um Softwarequalität geht. Halte durch und versuche, keine Abstriche bei der Qualität zu machen, bis du musst - bis sich herausstellt, dass es keinen vernünftigen Weg gibt, um alle deine Ziele zu erreichen. Beginne deine Verhandlungen nicht mit Standardkompromissen im Hinterkopf. Manche Probleme sind ohne Vereinfachungen und Kompromisse schwer zu lösen, aber für viele gibt es mit etwas Mühe und geeigneten Werkzeugen Lösungen.
An dieser Stelle ist dir hoffentlich klar, dass wir über Effizienz nachdenken müssen, am besten schon in den frühen Entwicklungsphasen. Wir haben gelernt, woraus Leistung besteht. Außerdem haben wir gelernt, dass es sich lohnt, viele falsche Vorstellungen zu hinterfragen, wenn es angebracht ist. Wir müssen uns des Risikos einer verfrühten Pessimierung und einer verfrühten Skalierbarkeit ebenso bewusst sein wie der Notwendigkeit, verfrühte Optimierungen zu vermeiden.
Schließlich haben wir gelernt, dass Effizienz in der Leistungsgleichung uns einen Vorteil verschaffen kann. Es ist einfacher, die Leistung zu verbessern, wenn man zuerst die Effizienz steigert. Das hat meinen Schülerinnen und Schülern und mir schon oft geholfen, das Thema Leistungsoptimierung effektiv anzugehen.
Im nächsten Kapitel werden wir eine kurze Einführung in Go geben. Wissen ist der Schlüssel zu mehr Effizienz, aber es ist besonders schwer, wenn wir die Grundlagen der Programmiersprache, die wir verwenden, nicht beherrschen.
1 Ich habe sogar ein kleines Experiment auf Twitter gemacht, um diesen Punkt zu beweisen.
2 Das britische Cambridge Dictionary definiert das Substantiv Leistung als "Wie gut eine Person, eine Maschine usw. eine Arbeit oder eine Aktivität ausführt".
3 Ich würde sogar empfehlen, dass du dich bei deinem Changelog an gängige Standardformate hältst, wie du sie hier sehen kannst. Dieses Material enthält auch wertvolle Tipps für saubere Versionshinweise.
4 Können wir in diesem Satz "weniger leistungsfähig" sagen? Das können wir nicht, denn das Wort "performant" gibt es im englischen Wortschatz nicht. Vielleicht bedeutet es, dass unsere Software nicht "performant" sein kann - es gibt immer Möglichkeiten, Dinge zu verbessern. In der Praxis gibt es Grenzen dafür, wie schnell unsere Software sein kann. H. J. Bremermann schlug 1962 vor, dass es eine rechnerische physikalische Grenze gibt, die von der Masse des Systems abhängt. Wir können schätzen, dass 1 kg des ultimativen Laptops ~1050 Bits pro Sekunde verarbeiten kann, während ein Computer mit der Masse des Planeten Erde maximal 1075 Bits pro Sekunde verarbeiten kann. Auch wenn sich diese Zahlen enorm anfühlen, würde selbst ein so großer Computer ewig brauchen, um alle Schachzüge zu erzwingen , die auf10120 Komplexität geschätzt werden. Diese Zahlen finden in der Kryptografie praktische Anwendung, um die Schwierigkeit des Knackens bestimmter Verschlüsselungsalgorithmen zu beurteilen.
5 Es ist erwähnenswert, dass das Verstecken von Funktionen oder Optimierungen manchmal zu einer schlechteren Lesbarkeit führen kann. Manchmal ist eine explizite Darstellung viel besser und vermeidet Überraschungen.
6 Als Teil des "Vertrags" der Schnittstelle könnte ein Kommentar stehen, der besagt, dass Implementierungen das Ergebnis zwischenspeichern sollten. Der Aufrufer sollte also sicher sein, dass er die Funktion viele Male aufrufen kann. Trotzdem würde ich sagen, dass es besser ist, sich nicht auf etwas zu verlassen, das nicht durch ein Typsystem abgesichert ist, um Überraschungen zu vermeiden.
7 Alle drei Beispiele für die Implementierung von Get
können als kostspielig angesehen werden, wenn sie aufgerufen werden. Input-Output (I/O) Operationen gegen das Dateisystem sind deutlich langsamer als das Lesen oder Schreiben von Daten aus dem Speicher. Etwas, das Mutexe beinhaltet, bedeutet, dass du möglicherweise auf andere Threads warten musst, bevor du darauf zugreifen kannst. Bei einem Datenbankaufruf sind in der Regel alle Threads involviert, plus eine mögliche Kommunikation über das Netzwerk.
8 Dieses berühmte Zitat wird verwendet, um jemanden davon abzuhalten, Zeit in Optimierungsbemühungen zu investieren. Es wird im Allgemeinen überstrapaziert und stammt aus Donald Knuths "Structured Programming with goto
statements" (1974).
9 Diese Art der Schreibweise wird in der Regel als ungarische Notation bezeichnet, die bei Microsoft weit verbreitet ist. Auch von dieser Schreibweise gibt es zwei Arten: App und Systeme. Die Literatur zeigt, dass Apps Ungarisch immer noch viele Vorteile bieten kann.
10 Heutzutage wird empfohlen, den Code so zu schreiben, dass er mit den IDE-Funktionen kompatibel ist, d.h. deine Codestruktur sollte ein "verbundener" Graph sein. Das bedeutet, dass du Funktionen so verbindest, dass die IDE sie unterstützen kann. Dynamisches Dispatching, Code Injection und Lazy Loading schalten diese Funktionen aus und sollten vermieden werden, wenn sie nicht unbedingt notwendig sind.
11 Die kognitive Belastung ist die Menge an "Gehirnverarbeitung und Gedächtnis", die eine Person aufwenden muss, um einen Code oder eine Funktion zu verstehen.
12 Cachability wird oft als die Fähigkeit definiert, gecacht zu werden. Es ist möglich, Informationen zwischenzuspeichern (zu cachen), um sie später schneller abzurufen. Allerdings sind die Daten möglicherweise nur für eine kurze Zeit oder nur für eine kleine Anzahl von Anfragen gültig. Wenn die Daten von externen Faktoren abhängen (z. B. vom Nutzer oder von Eingaben) und sich häufig ändern, sind sie nicht gut cachbar.
13 Das ist natürlich eine Vereinfachung. Der Prozess könnte mehr Speicher verbraucht haben. Die Profile zeigen nicht den Speicher, der von Memory Maps, Stacks und vielen anderen Caches verwendet wird, die für das Funktionieren moderner Anwendungen erforderlich sind. Wir werden in Kapitel 4 mehr darüber erfahren.
14 Cyril Northcote Parkinson war ein britischer Historiker, der das Managementphänomen formulierte, das heute als Parkinsons Gesetz bekannt ist. Es besagt: "Die Arbeit dehnt sich aus, um die Zeit zu füllen, die für ihre Erledigung zur Verfügung steht", und wurde ursprünglich als die Effizienz von Regierungsbüros bezeichnet, die in hohem Maße mit der Anzahl der Beamten im Entscheidungsgremium korreliert.
15 Zumindest sah mein Studium so aus. Dieses Phänomen ist auch als das "Studentensyndrom" bekannt .
16 PB bedeutet Petabyte. Ein Petabyte sind 1.000 TB. Wenn wir davon ausgehen, dass ein durchschnittlicher zweistündiger 4K-Film 100 GB benötigt, bedeutet das, dass wir mit 1 PB 10.000 Filme speichern könnten, was ungefähr zwei bis drei Jahren ständigem Ansehen entspricht.
17 1 Zettabyte sind 1 Million PB, eine Milliarde TB. Ich will gar nicht erst versuchen, diese Datenmenge zu visualisieren :)
18 Robert H. Dennard et al., "Design of Ion-Implanted MOSFET's with Very Small Physical Dimension", IEEE Journal of Solid-State Circuits 9, no. 5 (Oktober 1974): 256-268.
19 MOSFET steht für "Metall-Oxid-Halbleiter-Feldeffekttransistor" und ist, einfach ausgedrückt, ein isoliertes Gate, das elektronische Signale schalten kann. Diese Technologie steckt hinter den meisten Speicherchips und Mikroprozessoren, die zwischen 1960 und heute hergestellt werden. Sie hat sich als äußerst skalierbar und miniaturisierbar erwiesen. Mit 13 Sextillionen Stück zwischen 1960 und 2018 ist es das am häufigsten hergestellte Bauelement der Geschichte.
20 Komischerweise haben die Unternehmen aus Marketinggründen die Unfähigkeit, die Größe der Transistoren effektiv zu reduzieren, dadurch verheimlicht, dass sie die CPU-Generation nicht mehr nach der Gate-Länge der Transistoren, sondern nach der Größe des Prozesses benannt haben. CPUs der 14-nm-Generation haben immer noch 70-nm-Transistoren, ähnlich wie die 10-, 7- und 5-nm-Prozesse.
21 Ich scherze nicht. Microsoft hat bewiesen, dass es eine großartige Idee ist, Server 40 Meter unter Wasser zu betreiben, um die Energieeffizienz zu verbessern.
22 Der M1-Chip ist ein gutes Beispiel für einen interessanten Kompromiss: die Entscheidung für Geschwindigkeit und Energie- und Leistungseffizienz gegenüber der Flexibilität der Hardwareskalierung.
23 RISC-V ist ein offener Standard für die Befehlssatzarchitektur, der die Herstellung von kompatiblen "Reduced Instruction Set Computer"-Chips erleichtert. Ein solcher Befehlssatz ist viel einfacher und ermöglicht optimierte und spezialisierte Hardware als allgemeine CPUs.
24 Um sicherzustellen, dass die Entwickler/innen die Nutzer/innen mit einer langsameren Verbindung verstehen und sich in sie hineinversetzen können, hat Facebook den "2G-Dienstag" eingeführt, an dem der simulierte 2G-Netzwerkmodus in der Facebook-Anwendung aktiviert wird.
25 Diese Option ist nicht so teuer, wie wir vielleicht denken. Der Instanztyp x1e.32xlarge kostet $26,60 pro Stunde, also "nur" $19.418 pro Monat.
26 Auch die Hardwareverwaltung muss für Maschinen mit extrem großer Hardware anders sein. Deshalb gibt es für Linux-Kernel den speziellen hugemem-Typ, der bis zu viermal mehr Arbeitsspeicher und ~achtmal mehr logische Kerne für x86-Systeme verwalten kann.
27 CAP ist ein zentrales Prinzip der Systementwicklung. Sein Akronym steht für Konsistenz, Verfügbarkeit und Partitionstoleranz. Es definiert eine einfache Regel, nach der nur zwei der drei Punkte erreicht werden können.
28 Denial of Service ist ein Systemzustand, bei dem das System nicht mehr reagiert, meist aufgrund eines bösartigen Angriffs. Er kann auch "versehentlich" durch eine unerwartet hohe Last ausgelöst werden.
29 Um 2015 war er der schnellste Supercomputer in Polen mit 1,41 PFlop/s und über 1.600 Knoten, die meisten davon mit dedizierten GPUs.
30 InfiniBand ist ein leistungsstarker Netzwerkkommunikationsstandard, der besonders beliebt war, bevor die Glasfaser erfunden wurde.
31 Lustigerweise kündigte Mark Zuckerberg auf einer F8-Konferenz 2014 eine Änderung des berühmten Mottos an: "Move fast with stable infra".
32 Ein Beispiel, das ich in der Cloud-nativen Welt häufig sehe, ist der Wechsel des Logging-Stacks von Elasticsearch zu einfacheren Lösungen wie Loki. Trotz der fehlenden konfigurierbaren Indizierung bietet das Loki-Projekt eine bessere Logging-Leseleistung bei geringerem Ressourceneinsatz.
33 Es gibt noch einen weiteren Grund. Der Name "Efficient Go" ist sehr nah an einem der besten Dokumentationsstücke, die du über die Programmiersprache Go finden kannst: "Effective Go"! Es ist vielleicht auch eine der ersten Informationen, die ich über Go gelesen habe. Er ist konkret und umsetzbar und ich empfehle dir, ihn zu lesen, falls du ihn noch nicht gelesen hast.
Get Efficient Go now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.