Cliff Click – CTO von Cratus (IoT-Sensoren zur Verbesserung von Prozessen), Gründer und Mitgründer mehrerer Startups (darunter Rocket Realtime School, Neurensic und H2O.ai) mit mehreren erfolgreichen Exits. Cliff schrieb mit 15 Jahren seinen ersten Compiler (Pascal für den TRS Z-80)! Er ist am bekanntesten für seine Arbeit an C2 in Java (dem Sea of Nodes IR). Dieser Compiler zeigte der Welt, dass JIT hochwertigen Code erzeugen kann, was zu einem der Faktoren wurde, die Java zu einer der Hauptplattformen der modernen Software machten. Anschließend half Cliff dem Unternehmen Azul Systems, einen 864-kernigen Mainframe mit Software in reinem Java zu entwickeln, der GC-Pausen auf einem 500-Gigabyte-Haufen innerhalb von 10 Millisekunden unterstützte. Insgesamt hat Cliff in allen Aspekten der JVM gearbeitet.
Dieser Hub-Post ist ein ausführliches Interview mit Cliff. Wir werden die folgenden Themen besprechen:
- Übergang zu Low-Level-Optimierungen
- Wie man eine große Umstrukturierung vornimmt
- Kostenmodell
- Einführung in Low-Level-Optimierungen
- Praktische Beispiele zur Verbesserung der Leistung
- Warum man eine eigene Programmier sprache erstellen sollte
- Karriere als Performance-Ingenieur
- Technische Herausforderungen
- Ein bisschen über Registerzuweisung und Multikerntechnik
- Die größte Herausforderung im Leben
Interviewer:
- Andrei Satarygin von Amazon Web Services. In seiner Karriere hat er an ganz unterschiedlichen Projekten gearbeitet: Er testete die verteilte NewSQL-Datenbank bei Yandex, ein Cloud-Detektionssystem im Kaspersky-Labor, ein Mehrspieler-Spiel bei Mail.ru und einen Währungsberechnungsdienst bei der Deutschen Bank. Er interessiert sich für die Testung großangelegter Backend- und verteilter Systeme.
- Wladimir Sitnikow von Netcracker. Seit zehn Jahren arbeitet er an der Leistungsfähigkeit und Skalierbarkeit des NetCracker OS – der Software, die von Telekommunikationsanbietern zur Automatisierung von Netzwerkmanagementprozessen und Netzwerkhardware verwendet wird. Er ist fasziniert von Fragen zur Java-Leistung und Oracle Database. Autor von mehr als einem Dutzend Leistungsverbesserungen im offiziellen PostgreSQL JDBC-Treiber.
Übergang zu Low-Level-Optimierungen
Andrei: Sie sind eine bekannte Persönlichkeit in der Welt der JIT-Compilation, in Java und allgemein in der Leistungsverbesserung, richtig?
Cliff: Ganz genau!
Andrei: Lassen Sie uns mit allgemeinen Fragen zur Leistungsoptimierung beginnen. Was halten Sie von der Wahl zwischen High-Level- und Low-Level-Optimierungen, wie zum Beispiel der Arbeit auf CPU-Ebene?
Cliff: Hier ist alles ganz einfach. Der schnellste Code ist derjenige, der nie ausgeführt wird. Deshalb sollte man immer mit einem hohen Niveau beginnen und an den Algorithmen arbeiten. Eine bessere O-Notation wird eine schlechtere übertreffen, es sei denn, es kommen einige ausreichend große Konstanten ins Spiel. Niedrigstufige Dinge kommen am Schluss. Normalerweise, wenn man den gesamten Rest des Stacks gut genug optimiert hat und immer noch etwas Interessantes übrig bleibt – das ist der niedrigere Level. Aber wie fängt man auf hohem Niveau an? Wie weiß man, dass genügend Arbeit auf hohem Niveau geleistet wurde? Nun... es gibt keine fertigen Rezepte. Man muss das Problem verstehen, entscheiden, was man tun möchte (damit man keine unnötigen Schritte unternehmen muss) und dann den Profiler einsetzen, der nützliche Informationen liefern kann. Irgendwann merkt man selbst, dass man unnötige Dinge losgeworden ist und es an der Zeit ist, sich um die Feinabstimmung auf niedrigem Niveau zu kümmern. Das ist eindeutig eine besondere Art von Kunst. Viele Menschen machen unnötige Dinge, bewegen sich aber so schnell, dass sie keine Zeit für Performance-Optimierung haben. Aber das bleibt so, bis die Frage drängend wird. In der Regel interessiert es 99 % der Zeit niemanden, was ich mache, bis zum Zeitpunkt, wo auf dem kritischen Pfad eine wichtige Sache auftritt, die jemandem wichtig ist. Und dann fangen alle an, dich zu fragen, warum es von Anfang an nicht perfekt funktioniert hat. Generell gibt es immer etwas zu verbessern, wenn es um Performance geht. Aber 99 % der Zeit fehlen dir die Ansatzpunkte! Du versuchst einfach, dass etwas funktioniert, und währenddessen verstehst du, was wichtig ist. Man kann nie im Voraus wissen, dass dieses spezifische Stück perfekt sein muss, also muss man im Grunde in allem perfekt sein. Und das ist unmöglich und so macht man es nicht. Es gibt immer eine Menge Dinge zu reparieren – und das ist vollkommen normal.
Wie man eine große Umstrukturierung vornimmt
Andrei: Wie arbeiten Sie an der Performance? Das ist schließlich ein durchgängiges Problem. Hatten Sie zum Beispiel schon einmal mit Herausforderungen zu kämpfen, die durch die Interaktion mit einer Vielzahl bereits bestehender Funktionen entstanden sind?
Cliff: Ich versuche, das zu vermeiden. Wenn ich weiß, dass die Leistung ein Problem werden könnte, denke ich schon nach, bevor ich mit dem Programmieren beginne, insbesondere bei den Datenstrukturen. Aber oft erkennt man all das erst viel später. Und dann muss man extremere Maßnahmen ergreifen und das tun, was ich „Schreiben und Herrschen“ nenne: Man muss einen ausreichend großen Abschnitt erfassen. Teile des Codes müssen ohnehin aufgrund von Performance-Problemen oder aus anderen Gründen neu geschrieben werden. Welche auch immer der Anlass für das Neuschreiben des Codes ist, es ist fast immer besser, einen größeren Abschnitt als einen kleineren zu überarbeiten. In diesem Moment geraten alle in Panik: „Oh Gott, man kann nicht so viel Code anfassen!“ Aber in der Tat funktioniert dieser Ansatz fast immer viel besser. Man sollte sich sofort dem größeren Problem widmen, einen großen Kreis darum ziehen und sagen: Alles, was innerhalb dieses Kreises ist, werde ich neu schreiben. Die Grenze ist schließlich viel kleiner als der Inhalt innerhalb, der ersetzt werden soll. Und wenn eine solche Abgrenzung es ermöglicht, die Arbeit im Inneren perfekt zu gestalten – hast du freie Hand, mach, was du willst. Sobald du das Problem verstanden hast, verläuft der Neuschreibungsprozess viel einfacher, also beiße in einen großen Abschnitt!
Während man an größeren Umstellungen arbeitet und erkennt, dass die Performance ein Problem werden könnte, beginnt man oft, sich darüber Gedanken zu machen. Das führt häufig zu einfachen Überlegungen wie: "Kopiere keine Daten, sondern verwalte sie so effizient wie möglich, halte sie klein." Bei umfangreichen Anpassungen gibt es bewährte Methoden zur Leistungsverbesserung, die in der Regel immer um die Datenorganisation kreisen.
Kostenmodell
Andrei: In einem Ihrer Podcasts haben Sie über Preismodelle im Kontext der Performance gesprochen. Können Sie erläutern, was Sie damit gemeint haben?
Cliff: Natürlich. Ich bin in einer Zeit geboren, in der die Prozessorleistung von entscheidender Bedeutung war. Und diese Ära kehrt zurück – das Schicksal ist nicht ohne Ironie. Ich habe in einer Zeit von 8-Bit-Maschinen gelebt, mein erster Computer hatte 256 Byte. Genau, Byte. Alles war sehr klein. Man musste die Anweisungen zählen, und je mehr wir in der Programmierung vorankamen, desto mehr übernahmen die Sprachen. Zuerst gab es Assembler, dann Basic, dann C, und C übernahm viele Details wie das Register-Management und die Anweisungsauswahl. Aber es war alles recht klar, und wenn ich einen Zeiger auf eine Variable machte, bekam ich ein Load, und die Kosten dieser Anweisung sind bekannt. Die Hardware gibt eine bekannte Anzahl von Maschinenzyklen aus, sodass die Ausführungsgeschwindigkeit verschiedener Operationen einfach berechnet werden kann, indem man alle Anweisungen, die man ausführen möchte, zusammenzählt. Jedes Compare/Test/Branch/Call/Load/Store konnte summiert werden, und man konnte sagen: Hier ist die Ausführungszeit. Wenn man die Performance verbessert, fällt einem sicherlich auf, welche Zahlen den kleinen heißen Zyklen entsprechen.
Sobald Sie jedoch auf Java, Python und ähnliche Sprachen umschalten, entfernen Sie sich schnell von der Hardware auf niedriger Ebene. Wie hoch sind die Kosten für den Aufruf eines Getters in Java? Wenn der JIT im HotSpot es korrekt optimiert hat, , aber wenn dies nicht der Fall ist, wird es zu einem Funktionsaufruf. Da sich der Aufruf in einer heißen Schleife befindet, hebt dies alle anderen Optimierungen in dieser Schleife auf. Daher werden die tatsächlichen Kosten viel höher sein. Sie verlieren sofort die Fähigkeit, einen Codeabschnitt zu betrachten und zu verstehen, was es uns kosten wird, dies in Bezug auf CPU-Taktfrequenz, verwendeten Speicher und Cache auszuführen. All dies wird nur interessant, wenn man wirklich in die Performance eintaucht.
Wir befinden uns jetzt in einer Situation, in der die Taktraten der Prozessoren seit einem Jahrzehnt kaum gewachsen sind. Die alten Zeiten kehren zurück! Sie können nicht mehr auf eine gute Einzelthread-Leistung zählen. Aber wenn es um paralleles Rechnen geht – das ist unglaublich komplex, und alle schauen Sie an, als wären Sie James Bond. Zehnfache Beschleunigungen treten hier normalerweise dort auf, wo jemand etwas übersehen hat. Parallelität erfordert viel Arbeit. Um diese zehnfache Beschleunigung zu erreichen, ist es wichtig, das Kostensystem zu verstehen. Was kostet was? Um das zu begreifen, müssen Sie verstehen, wie die Programmiersprache auf die zugrunde liegende Hardware wirkt.
Martin Thompson hat ein großartiges Wort für seinen Blog gewählt ! Es ist wichtig zu verstehen, was die Hardware tun wird, wie sie es tun wird und warum sie überhaupt das tut, was sie tut. Mit diesem Wissen ist es relativ einfach, Anleitungen zu erstellen und herauszufinden, wo die Ausführungszeit verloren geht. Wenn du jedoch nicht die entsprechende Vorbereitung hast, suchst du einfach nach einer schwarzen Katze im dunklen Raum. Ich sehe ständig Leute, die die Leistung optimieren, ohne die geringste Ahnung zu haben, was sie eigentlich tun. Sie strengen sich sehr an und kommen nicht wirklich voran. Wenn ich denselben Code nehme, ein paar kleine Hacks einfüge und dadurch eine fünf- oder zehnfache Beschleunigung erziele, sagen sie: 'Das ist unfair, wir wussten schon, dass du besser bist.' Unglaublich. Worüber rede ich … das Kostenmodell – es geht darum, welchen Code du schreibst und wie schnell er im Durchschnitt in das Gesamtbild passt.
Andrei: Und wie behält man ein solches Volumen im Kopf? Erreicht man das durch viel Erfahrung, oder? Wo wird diese Erfahrung gesammelt?
Cliff: Nun, meine Erfahrungen habe ich nicht auf die einfachste Weise gesammelt. Ich habe in Assembler programmiert, als man noch jede einzelne Anleitung verstehen konnte. Das klingt dumm, aber seitdem ist mir eine Menge Z80-Anweisungen im Kopf geblieben. Ich kann mir die Namen von Menschen schon eine Minute nach dem Gespräch nicht mehr merken, aber den Code, den ich vor 40 Jahren geschrieben habe, schon. Lustig, es sieht aus wie das Syndrom von».
Einführung in Low-Level-Optimierungen
Andrei: Gibt es einen einfacheren Weg, um in die Materie einzusteigen?
Cliff: Ja und nein. Die Hardware, die wir alle nutzen, hat sich in dieser Zeit nicht wesentlich verändert. Alle verwenden x86, mit Ausnahme von Smartphones, die auf Arm basieren. Wenn man sich nicht mit extremem Embedded-Development beschäftigt, hat man genau das Gleiche. Gut, weiter. Die Anweisungen haben sich auch über Jahrhunderte nicht verändert. Man muss hingehen und etwas in Assembly schreiben. Ein wenig, aber genug, um zu beginnen, es zu verstehen. Sie lächeln, aber ich meine das ganz ernst. Es ist wichtig, die Beziehung zwischen der Programmiersprache und der Hardware zu verstehen. Danach sollten Sie schreiben, ein wenig, und einen kleinen Spielcompiler für eine kleine Spielsprache erstellen. „Spiel“ bedeutet, dass es in einem angemessenen Zeitrahmen geschehen muss. Er kann sehr einfach sein, aber er sollte Anweisungen generieren. Die Akt der Generierung von Anweisungen erlaubt es, das Kostenmodell für die Brücke zwischen dem hochgradigen Code, in dem alle schreiben, und dem Maschinen-Code, der auf der Hardware ausgeführt wird, zu verstehen. Diese Beziehung wird sich in den Köpfen festsetzen, während man den Compiler schreibt. Selbst den einfachsten Compiler. Danach kann man anfangen, sich Java anzusehen und zu verstehen, dass ihre semantische Kluft viel tiefer ist, und dass es viel schwieriger ist, Brücken darüber zu schlagen. In Java ist es viel schwieriger zu verstehen, ob unsere Brücke gut oder schlecht ist, was sie zum Einsturz bringt und was nicht. Aber man braucht einen Ausgangspunkt, wenn man den Code betrachtet und denkt: „Aha, dieser Getter sollte jedes Mal in-line gesetzt werden.“ Und dann stellt sich heraus, dass das manchmal passiert, mit Ausnahme der Fälle, in denen die Methode zu groß wird und JIT anfängt, alles Mögliche in-line zu setzen. Die Leistung solcher Stellen kann man sofort vorhersagen. Normalerweise funktionieren Getter gut, aber dann schaut man sich große heiße Schleifen an und erkennt, dass dort einige Funktionsaufrufe herumschwirren, deren Verhalten unklar ist. Das ist das Problem mit dem weit verbreiteten Gebrauch von Gettern; der Grund, warum sie nicht in-line gesetzt werden, ist, dass unklar ist, ob es sich um einen Getter handelt. Wenn man eine super kleine Codebasis hat, kann man sie einfach auswendig lernen und sagen: Das ist ein Getter und das ist ein Setter. In einer großen Codebasis hat jede Funktion ihre eigene Geschichte, die niemand wirklich kennt. Der Profiler sagt, dass wir 24% Zeit in einer bestimmten Schleife verloren haben, und um zu verstehen, was diese Schleife tut, muss man sich jede Funktion dort drinnen ansehen. Es ist unmöglich, dies zu verstehen, ohne die Funktion zu studieren, und das verlangsamt den Prozess des Verstehens erheblich. Deshalb benutze ich keine Getter und Setter – ich habe eine neue Stufe erreicht!
Woher bekommt man ein Preismodell? Nun, man kann sicherlich etwas lesen… Aber ich denke, der beste Weg ist, aktiv zu werden. Erstelle einen einfachen Compiler, und das wird der beste Weg sein, um das Preismodell zu verstehen und es sich selbst klarzumachen. Ein kleiner Compiler, der sich gut für die Programmierung einer Mikrowelle eignen würde – das ist eine Aufgabe für Anfänger. Ich meine, wenn du bereits Programmierkenntnisse hast, sollten diese ausreichen. All diese Dinge, wie das Parsen eines Strings, der irgendeinen algebraischen Ausdruck enthält, die Anweisungen für mathematische Operationen in der richtigen Reihenfolge herauszuziehen, und die richtigen Werte aus Registern zu nehmen – das ist alles ganz einfach. Und während du das tust, wird es sich in deinem Gehirn einprägen. Ich denke, jeder weiß, was ein Compiler macht. Und das wird Verständnis für das Preismodell schaffen.
Praktische Beispiele zur Verbesserung der Leistung
Andrei: Worauf sollte man noch achten, wenn man an der Leistung arbeitet?
Cliff: Datenstrukturen. Übrigens, ich habe schon lange keine Kurse mehr dazu gegeben… . Es war unterhaltsam, aber es erforderte so viel Einsatz, und ich habe schließlich auch ein Leben! Nun, eines der großen und interessanten Themen, "Wohin geht Ihre Leistung", habe ich den Studenten ein Beispiel gegeben: zweieinhalb Gigabyte an Fintech-Daten wurden aus einer CSV-Datei gelesen, und dann musste die Anzahl der verkauften Produkte berechnet werden. Gewöhnliche Tick-Daten des Marktes. UDP-Pakete, die seit den 70er Jahren in ein Textformat umgewandelt wurden. Chicago Mercantile Exchange – verschiedene Dinge wie Öl, Mais, Sojabohnen und dergleichen. Es galt, diese Produkte zu zählen, die Anzahl der Transaktionen, das durchschnittliche Volumen von Mitteln und Waren usw. zu berechnen. Das ist ziemlich einfache Handelsmathematik: Den Produktcode finden (das sind 1-2 Zeichen in einer Hash-Tabelle), die Summe erhalten, sie in eines der Transaktionssets hinzufügen, das Volumen hinzufügen, den Preis hinzufügen und ein paar andere Dinge. Sehr einfache Mathematik. Die einfache Implementierung war sehr geradlinig: Alles befindet sich in der Datei, ich lese die Datei und gehe durch sie hindurch, teile einzelne Datensätze in Java-Strings auf, suche nach den benötigten Informationen und addiere sie gemäß der oben beschriebenen Mathematik. Und das geschieht mit einem gewissen Tempo.
Mit diesem Ansatz ist offensichtlich, was passiert, und paralleles Rechnen wird hier nicht helfen, oder? Es stellt sich heraus, dass man die fünfmal höhere Leistungsfähigkeit nur durch die Wahl der richtigen Datenstrukturen erreichen kann. Sogar erfahrene Programmierer sind darüber erstaunt! In meinem speziellen Fall lag der Fokus darin, dass man keine Speicherzuweisungen in einer heißen Schleife tätigen sollte. Nun, das ist nicht die ganze Wahrheit, aber im Großen und Ganzen – man sollte nicht 'einmal in X' zuweisen, wenn X groß genug ist. Wenn X zweieinhalb Gigabyte beträgt, sollte man nichts 'einmal pro Zeichen', 'einmal pro Zeile' oder 'einmal für ein Feld' zuweisen, nichts dergleichen. Genau das kostet Zeit. Wie funktioniert das überhaupt? Stellen Sie sich vor, ich mache einen Aufruf String.split() oder BufferedReader.readLine(). Readline stellt eine Zeichenkette aus einer Menge von Bytes zusammen, die über das Netzwerk kommen, einmal für jede Zeile, für jede der Millionen von Zeilen. Ich nehme diese Zeile, analysiere sie und verwende sie nicht mehr. Warum nicht? Nun, ich habe sie bereits verarbeitet, das ist alles. Also, für jedes Byte, das aus diesen 2,7 GB gelesen wird, werden zwei Zeichen in der Zeichenkette geschrieben, das sind bereits 5,4 GB, und ich brauche sie danach nicht mehr, deshalb werden sie verworfen. Wenn wir die Speicherdurchsatzrate betrachten, laden wir 2,7 GB, die durch den Speicher und die Speicherschnittstelle in die CPU gelangen, und dann wird doppelt so viel in eine Zeichenkette im Speicher geschrieben, und all das wird bei der Erstellung jeder neuen Zeichenkette überarbeitet. Aber ich muss sie lesen, die Hardware liest sie, auch wenn sie danach alle überarbeitet werden. Und ich muss sie schreiben, weil ich eine Zeichenkette erstellt habe und die Caches überlaufen sind – der Cache kann nicht 2,7 GB aufnehmen. Insgesamt lese ich für jedes gelesene Byte zwei zusätzliche Bytes und schreibe zwei zusätzliche Bytes, und somit haben wir ein Verhältnis von 4:1 – in diesem Verhältnis verschwenden wir die Speicherdurchsatzrate. Und es stellt sich heraus, dass wenn ich das mache String.split() – das werde ich ganz bestimmt nicht zum letzten Mal tun, denn darin können noch 6-7 Felder enthalten sein. Deshalb führt der klassische CSV-Lese-Code mit anschließender Zeilenanalyse zu einem Speicherbandbreitenverlust von etwa 14:1 im Vergleich zu dem, was Sie tatsächlich haben möchten. Wenn Sie diese Zuweisungen weglassen, können Sie eine fünffache Beschleunigung erreichen.
Und das ist nicht besonders kompliziert. Wenn Sie den Code aus dem richtigen Blickwinkel betrachten, wird alles ziemlich einfach, sobald Sie das Wesen des Problems erkannt haben. Es ist nicht notwendig, das Speichermanagement vollständig einzustellen: Die einzige Schwierigkeit besteht darin, dass Sie etwas zuweisen und es sofort wieder verloren geht, wobei es wichtige Ressourcen verbrennt, die in diesem Fall – die Speicherbandbreite – sind. All dies führt zu einem Leistungsabfall. Bei x86 muss man normalerweise intensiv Taktzyklen verbrauchen, aber hier haben Sie den gesamten Speicher viel früher verbrannt. Die Lösung besteht darin, die Anzahl der Zuweisungen zu verringern.
Ein weiteres Problem ist, dass, wenn Sie den Profiler starten, wenn der Speicher voll ist, genau in dem Moment, in dem es passiert, normalerweise darauf warten, dass der Cache zurückkommt, weil er mit Müll gefüllt ist, den Sie gerade erzeugt haben, mit all diesen Objekten. Daher wird jede Load- oder Store-Operation langsam, da sie Cache-Misses verursachen – der gesamte Cache wird langsam, während er darauf wartet, dass der Müll entfernt wird. Der Profiler zeigt daher lediglich einen warmen, zufälligen Rauschen, der sich über den gesamten Zyklus erstreckt – es wird keine separate heiße Anweisung oder Stelle im Code geben. Nur Rauschen. Und wenn Sie sich die GC-Zyklen ansehen, werden diese alle zum Young Generation zählen und extrem schnell sein – Mikrosenden oder maximal Millisekunden. Denn all dieser Speicher stirbt sofort. Sie allocieren Milliarden von Gigabyte, und er schneidet sie ab, und schneidet sie ab, und schneidet sie wieder ab. All das geht sehr schnell. Es gibt also wenig kostspielige GC-Zyklen, warmes Rauschen über den gesamten Zyklus, aber wir möchten eine 5-fache Beschleunigung. An diesem Punkt sollte etwas in Ihrem Kopf sich schließen und zu der Frage führen: "Warum ist das so?!". Speicherüberläufe werden im klassischen Debugger nicht angezeigt; Sie müssen den Debugger für Hardware-Leistungszähler starten und es selbst direkt sehen. Indirekt kann man dies aus diesen drei Symptomen heraus vermuten. Das dritte Symptom ist, wenn Sie sehen, was Sie allocieren, den Profiler fragen, und er antwortet: "Sie haben eine Milliarde Objekte erstellt, aber der GC hat kostenlos gearbeitet". Sobald dies passiert, verstehen Sie, dass Sie zu viele Objekte produziert haben und den gesamten Speicher verbrannt haben. Es gibt Möglichkeiten, das herauszufinden, aber sie sind nicht offensichtlich.
Ein Problem in der Datenstruktur: eine nackte Struktur, die dem gesamten Geschehen zugrunde liegt, ist zu groß, es sind 2,7 GB auf der Festplatte, daher ist es sehr unerwünscht, diese zu kopieren – man möchte sie direkt aus dem Netzwerk-Byte-Puffer in die Register laden, um nicht ständig hin und her lesen und schreiben zu müssen. Leider bietet Java standardmäßig keine solche Bibliothek im JDK an. Aber das ist doch trivial, oder? Faktisch sind das 5-10 Zeilen Code, die zur Implementierung eines eigenen gepufferten String-Loaders führen, der das Verhalten der String-Klasse nachahmt und dabei eine Wrapper um den zugrunde liegenden Byte-Puffer ist. Infolgedessen arbeitet man fast wie mit Strings, aber tatsächlich bewegen sich dabei nur Zeiger auf den Puffer, und rohe Bytes werden nirgendwo kopiert, wodurch immer dieselben Puffer wiederverwendet werden, immer wieder. Und das Betriebssystem ist froh, sich um Dinge zu kümmern, für die es vorgesehen ist, wie das versteckte doppelte Pufferung dieser Byte-Puffer, sodass man selbst keinen endlosen Strom unerwünschter Daten mehr verarbeiten muss. Übrigens, versteht man denn nicht, dass bei der Arbeit mit GC garantiert wird, dass jede Speicherzuweisung nach dem letzten GC-Zyklus nicht mehr vom Prozessor gesehen wird? Deshalb kann all das nicht im Cache sein, und dann passiert ein 100% garantierter Missgriff. Bei der Arbeit mit einem Zeiger dauert das Auslesen eines Registers aus dem Speicher auf x86 1-2 Takte, und sobald das geschieht, zahlt man, zahlt man, zahlt man, denn der gesamte Speicher ist auf – und das ist der Preis für die Zuweisung von Speicher. Der tatsächliche Preis.
Mit anderen Worten, Datenstrukturen sind das, was am schwierigsten zu ändern ist. Und sobald Sie realisieren, dass Sie die falsche Datenstruktur gewählt haben, die letztendlich die Leistung beeinträchtigt, ist es normalerweise erforderlich, erhebliche Arbeiten zu leisten. Wenn dies nicht getan wird, wird es nur noch schlimmer. Zunächst müssen Sie über Datenstrukturen nachdenken, denn das ist wichtig. Die Hauptkosten liegen in den komplexen Datenstrukturen, die in der Form "Ich habe Datenstruktur X in Datenstruktur Y kopiert, weil ich Y ansprechender finde" verwendet werden. Aber der Kopiervorgang (der als günstig erscheint) verbraucht tatsächlich Speicherbandbreite, und hier steckt die verlorene Zeit. Wenn ich also eine riesige JSON-Zeichenkette habe und sie in einen strukturierten DOM-Baum aus POJO oder Ähnlichem umwandeln möchte, wird der Parsing-Prozess dieser Zeichenkette und das Erstellen von POJO sowie der spätere Zugriff auf das POJO erhebliche Kosten verursachen – das ist nicht günstig. Es sei denn, Sie greifen viel häufiger auf das POJO zu als auf die Zeichenkette. Stattdessen könnte man versuchen, die Zeichenkette zu dechiffrieren und nur die benötigten Elemente herauszuziehen, ohne sie in ein POJO umzuwandeln. Wenn all dies zu einem Zeitpunkt geschieht, an dem maximale Leistung erforderlich ist, sollten Sie sich von POJOs fernhalten – und direkt mit der Zeichenkette arbeiten.
Warum man eine eigene Programmier sprache erstellen sollte
Andrei: Sie haben gesagt, dass man für ein besseres Verständnis des Kostenmodells eine eigene kleine Sprache schreiben sollte…
Cliff: Nicht eine Sprache, sondern einen Compiler. Sprache und Compiler sind zwei verschiedene Dinge. Der wichtigste Unterschied liegt im eigenen Kopf.
Andrei: Übrigens, soweit ich weiß, experimentieren Sie mit der Erstellung eigener Sprachen. Warum?
Cliff: Weil ich es kann! Ich bin zur Hälfte im Ruhestand, also ist das mein Hobby. Ich habe mein ganzes Leben lang die Programmiersprachen anderer Menschen umgesetzt. Außerdem habe ich viel an meinem Codierungsstil gearbeitet. Und auch, weil ich Probleme in anderen Sprachen sehe. Ich erkenne, dass es bessere Möglichkeiten gibt, gewohnte Dinge zu tun. Und ich würde sie nutzen. Ich habe einfach genug davon, Probleme in mir selbst, in Java, in Python oder in jeder anderen Sprache zu sehen. Momentan schreibe ich als Hobby in React Native, JavaScript und Elm; das hat nichts mit meinem Ruhestand zu tun, sondern mit aktiver Arbeit. Ich programmiere auch in Python und werde wahrscheinlich weiterhin im Bereich Maschinelles Lernen für Java-Backends arbeiten. Es gibt viele beliebte Sprachen, und jede hat ihre eigenen interessanten Besonderheiten. Jede ist auf ihre Weise gut und es könnte spannend sein, all diese Features zu kombinieren. Daher beschäftige ich mich mit dem Studium interessanter Dinge für mich, dem Verhalten von Sprachen, und versuche, eine sinnvolle Semantik zu entwickeln. Und bisher gelingt mir das! Momentan kämpfe ich mit der Semantik des Speichers, denn ich möchte, dass sie so funktioniert wie in C und Java, und ich möchte ein starkes Speicher- und Semantikmodell für Loads und Stores erhalten. Gleichzeitig möchte ich eine automatische Typableitung wie in Haskell haben. Also versuche ich, eine Haskell-ähnliche Typableitung mit einem Speichermodell zu kombinieren, das wie in C und Java funktioniert. Daran arbeite ich seit den letzten 2-3 Monaten, zum Beispiel.
Andrei: Wenn Sie eine Sprache entwickeln, die die besten Aspekte anderer Sprachen aufgreift, haben Sie dann darüber nachgedacht, dass jemand Ihre Ideen aufgreifen und für sich nutzen könnte?
Cliff: So entstehen neue Sprachen! Warum ähnelt Java C? Weil C eine gute Syntax hatte, die jeder verstand, und Java sich von dieser Syntax inspirieren ließ, indem es Typensicherheit, Array-Grenzprüfungen, GC hinzufügte und einige Dinge aus C verbesserte. Sie haben eigene Ideen beigefügt. Aber sie waren ziemlich stark inspiriert, oder? Alle stehen auf den Schultern von Riesen, die vor dir waren – so entsteht Fortschritt.
Andrei: Soweit ich verstehe, wird Ihre Sprache sicher im Hinblick auf die Speichernutzung sein. Haben Sie darüber nachgedacht, etwas wie den Borrow Checker aus Rust zu implementieren? Haben Sie ihn sich angeschaut? Was halten Sie davon?
Cliff: Ich programmiere schon seit einer Ewigkeit in C, mit all diesen malloc und free, und ich verwalte die Lebensdauer manuell. Sie wissen, 90-95% des manuell verwalteten Lebenszyklus haben eine ähnliche Struktur. Und es ist sehr, sehr schmerzhaft, sich damit manuell auseinanderzusetzen. Ich wünschte, der Compiler könnte einfach sagen, was da passiert und was ich mit meinen Aktionen erreicht habe. In einigen Fällen erledigt der Borrow Checker das von Haus aus. Außerdem sollte er automatisch Informationen bereitstellen, alles verstehen und mich nicht mit der Aufgabe belasten, dieses Verständnis zu formulieren. Er sollte mindestens eine lokale Escape-Analyse durchführen, und nur wenn das nicht funktioniert, sollten Typannotationen hinzugefügt werden, die die Lebensdauer beschreiben – und dieses Schema ist viel komplizierter als der Borrow Checker oder überhaupt irgendein existierender Speicherprüfer. Die Wahl zwischen „alles in Ordnung“ und „ich habe nichts verstanden“ – das sollte besser sein.
Als jemand, der viel C-Code geschrieben hat, halte ich die Unterstützung für das automatische Management des Lebenszyklus für äußerst wichtig. Außerdem ärgert es mich, wie sehr Java Speicher verbraucht, mit der Hauptbeschwerde im Zusammenhang mit dem Garbage Collector. Bei der Speicherzuweisung in Java erhält man den Speicher, der im letzten GC-Zyklus lokal war, nicht zurück. In Sprachen mit präziserer Speicherverwaltung ist das nicht der Fall. Wenn man malloc aufruft, erhält man sofort Speicher, der gerade verwendet wurde. Normalerweise führt man mit dem Speicher einige temporäre Operationen durch und gibt ihn sofort zurück. Und er wird sofort wieder in den malloc-Pool zurückgegeben, sodass der nächste malloc-Zyklus ihn erneut verwenden kann. Daher reduziert sich der tatsächliche Speicherverbrauch auf die Menge an lebenden Objekten zu einem bestimmten Zeitpunkt, plus eventuelle Speicherlecks. Wenn es keine gravierenden Lecks gibt, wird der Großteil des Speichers in Caches und CPU gespeichert, was schnell funktioniert. Allerdings erfordert dies viel manuelles Speichermanagement durch malloc und free, die in der richtigen Reihenfolge und am richtigen Ort aufgerufen werden müssen. Rust kann dies selbst richtig handhaben und in vielen Fällen sogar eine bessere Leistung bieten, da der Speicherverbrauch nur auf die aktuellen Berechnungen beschränkt ist – im Gegensatz zur Erwartung des nächsten GC-Zyklus, der den Speicher freigibt. Letztendlich haben wir einen sehr interessanten Weg gefunden, um die Leistung zu verbessern. Und es ist ziemlich mächtig – ich habe an solchen Dingen bei der Datenverarbeitung für Fintech gearbeitet, und das hat eine Beschleunigung von etwa fünf ermöglicht. Das ist eine erhebliche Beschleunigung, besonders in einer Welt, in der die Prozessoren nicht schneller werden, während wir weiterhin auf Verbesserungen warten.
Karriere als Performance-Ingenieur
Andrei: Ich würde auch gerne insgesamt etwas über die Karriere fragen. Sie wurden durch Ihre Arbeit bei JIT in HotSpot berühmt und wechselten dann zu Azul – und das ist auch ein JVM-Unternehmen. Dort beschäftigten Sie sich jedoch mehr mit Hardware als mit Software. Und dann wechselten Sie plötzlich zu Big Data und Machine Learning und danach zur Betrugserkennung. Wie kam es dazu? Das sind sehr unterschiedliche Bereiche der Entwicklung.
Cliff: Ich beschäftige mich schon seit geraumer Zeit mit Programmierung und habe in den verschiedensten Bereichen Erfahrungen gesammelt. Wenn Menschen sagen: „Oh, du bist derjenige, der JIT für Java gemacht hat!“, finde ich das immer amüsant. Vorher habe ich an einem Klon von PostScript gearbeitet – der Sprache, die Apple einst für seine Laserdrucker verwendet hat. Davor habe ich das Forth-Sprache umgesetzt. Ich denke, das gemeinsame Thema für mich ist die Entwicklung von Werkzeugen. Mein ganzes Leben lang habe ich Werkzeuge geschaffen, mit denen andere Menschen ihre großartigen Programme schreiben. Aber ich habe auch Betriebssysteme, Treiber, Kernel-Debugger und Programmiersprachen für Betriebssysteme entwickelt, die ursprünglich trivial waren, aber mit der Zeit immer komplexer wurden. Aber das Hauptthema ist dennoch die Entwicklung von Werkzeugen. Ein großer Teil meines Lebens verging zwischen Azul und Sun, und es ging dabei um Java. Aber als ich mich mit Big Data und Machine Learning beschäftigte, setzte ich wieder meinen feierlichen Hut auf und sagte: „Oh, jetzt haben wir ein nicht triviales Problem, und es passiert hier eine Menge interessanter Dinge mit Menschen, die etwas tun.“ Das ist ein hervorragender Entwicklungsweg, den man einschlagen sollte.
Ja, ich liebe verteilte Rechnersysteme sehr. Mein erster Job während des Studiums war in C, an einem Werbeprojekt. Dabei ging es um verteilte Berechnungen auf Zilog Z80-Chips, die Daten für die analoge optische Texterkennung sammelten, die von einem echten analogen Analysator durchgeführt wurde. Das war ein spannendes und völlig verrücktes Thema. Aber es gab Probleme: Ein Teil der Daten wurde nicht korrekt erkannt, weshalb die Bilder angezeigt werden mussten, um sie von jemandem lesen zu lassen, der sie bereits visuell erfasst hatte, um Feedback zu geben. Dadurch entstanden Jobs mit Daten, die ihre eigene Sprache hatten. Es gab ein Backend, das alles verarbeitete – parallel arbeitende Z80 mit laufenden vt100-Terminals – jeweils eines pro Person, und es gab ein Modell für paralleles Programmieren auf Z80. Ein gemeinsamer Speicherbereich, den alle Z80 innerhalb einer Sternkonfiguration teilten; auch das Backplane wurde geteilt, und die Hälfte des RAM wurde innerhalb des Netzwerks geteilt, während die andere Hälfte privat war oder für etwas anderes genutzt wurde. Eine bedeutend komplexe parallele verteilte Systemarchitektur mit geteilter... halbgeteilter Speicher. Wann das war... das ist schon so lange her, irgendwo in den 80ern. Ziemlich lange her.
Ja, nehmen wir an, dass 30 Jahre eine beträchtliche Zeitspanne sind. Die Herausforderungen im Bereich der verteilten Datenverarbeitung bestehen schon lange, und die Menschen haben sich bereits früher damit auseinandergesetzt. -Cluster. Solche Cluster sehen so aus: Es gibt Ethernet, und dein leistungsstarker x86 ist mit diesem Ethernet verbunden. Jetzt möchtest du eine gefälschte Shared Memory haben, weil es damals zu schwierig war, sich mit der Programmierung verteilter Systeme zu beschäftigen. Daher gab es eine gefälschte Shared Memory mit Speicherschutz auf x86. Wenn du in diesen Speicher geschrieben hast, haben wir den anderen Prozessoren mitgeteilt, dass sie die gleiche Shared Memory von dir laden müssen, wenn sie darauf zugreifen wollten. So entstand eine Art Protokoll zur Unterstützung der Cache-Kohärenz und die entsprechende Software. Eine interessante Konzeptualisierung. Das echte Problem lag natürlich woanders. Das Ganze funktionierte, aber man bekam schnell Leistungsprobleme, da niemand das Leistungsmodell auf einem ausreichenden Niveau verstand – welche Zugriffszenarien es gab, wie man verhindern konnte, dass die Knoten sich endlos gegenseitig anpingen, und so weiter.
In H2O habe ich Folgendes ausgeklügelt: Die Entwickler selbst sind dafür verantwortlich, den Parallelismus zu erkennen und zu wissen, wo er nicht vorhanden ist. Ich habe ein Codierungsmodell entwickelt, das es einfach macht, leistungsstarken Code zu schreiben. Andererseits ist es eine Herausforderung, ineffizienten Code zu erstellen, und dieser wird schlecht aussehen. Es erfordert erhebliche Anstrengungen, um langsamen Code zu produzieren; man muss unkonventionelle Methoden anwenden. Langsame Codeabschnitte sind auf den ersten Blick erkennbar. In der Regel wird also Code geschrieben, der schnell funktioniert, jedoch muss man sich mit dem Umgang mit gemeinsam genutztem Speicher auseinandersetzen. Dies ist alles mit großen Arrays verbunden, und das Verhalten ist vergleichbar mit nicht-volatilen großen Arrays in parallelem Java. Stellen Sie sich vor, zwei Threads schreiben in ein paralleles Array, wobei einer gewinnt und der andere entsprechend verliert, ohne dass man weiß, welcher welcher ist. Wenn sie nicht volatil sind, kann die Reihenfolge beliebig sein – und das funktioniert tatsächlich gut. Die Menschen kümmern sich wirklich um die Reihenfolge der Operationen, sie platzieren volatile richtig und erwarten an den richtigen Stellen Performance-Probleme im Zusammenhang mit dem Speicher. Andernfalls würden sie einfach Code in Form von Schleifen von 1 bis N schreiben, wobei N eine unvorstellbar große Zahl ist, und hoffen, dass alle komplexen Fälle automatisch parallelisiert werden – und das klappt nicht. Aber in H2O, das ist weder Java noch Scala; man könnte es als "Java minus minus" betrachten, wenn man möchte. Es ist ein sehr verständlicher Programmierstil und ähnelt dem Schreiben von einfachem Code in C oder Java mit Schleifen und Arrays. Dennoch kann der Speicher in Terabytes verarbeitet werden. Ich verwende H2O immer noch. Gelegentlich setze ich es in verschiedenen Projekten ein – und es ist nach wie vor das schnellste Tool, das seine Konkurrenten um ein Vielfaches übertrifft. Wenn Sie Big Data mit spaltenbasierten Daten verarbeiten, ist es sehr schwierig, H2O zu übertreffen.
Technische Herausforderungen
Andrei: Was war die größte Herausforderung in Ihrer gesamten Karriere?
Cliff: Sprechen wir über den technischen oder den nicht-technischen Teil? Ich würde sagen, die größten Herausforderungen sind nicht technischer Natur.
Was die technischen Herausforderungen betrifft, habe ich sie einfach gemeistert. Ich kann nicht einmal sagen, welche am größten war, aber es gab einige ziemlich interessante, die viel Zeit und geistige Auseinandersetzung erforderten. Als ich zu Sun ging, war ich überzeugt, einen schnellen Compiler zu entwickeln, während viele meiner Senior-Kollegen sagten, dass ich damit niemals Erfolg haben würde. Aber ich verfolgte diesen Weg, schrieb den Compiler bis hin zum Register-Allocator, und er war ziemlich schnell. Er war so schnell wie das moderne C1, aber damals war der Allocator viel langsamer, und rückblickend war das eine Herausforderung aufgrund der großen Datenstrukturen. Ich benötigte diese, um einen grafischen Register-Allocator zu schreiben, und ich verstand die Dilemmata zwischen Ausdruckskraft des Codes und Geschwindigkeit, die in dieser Ära sehr wichtig waren. Es stellte sich heraus, dass die Datenstruktur normalerweise die Cache-Größe der damaligen x86-Prozessoren überschreitet und daher, während ich ursprünglich annahm, dass der Register-Allocator 5-10 Prozent der gesamten JIT-Zeit in Anspruch nehmen würde, letztendlich 50 Prozent ausmachten.
Die Zeit verging, der Compiler wurde immer präziser und leistungsfähiger, hörte auf, in den meisten Fällen schlechten Code zu generieren, und die Leistung ähnelte zunehmend dem, was ein C-Compiler liefert. Es sei denn, man schreibt solchen Mist, den nicht einmal C schneller macht. Wenn du Code schreibst, der wie C aussieht, erhältst du auch in den meisten Fällen eine Leistung wie bei C. Und immer häufiger produzierte der Code asymptotisch ähnliche Ergebnisse wie C, der Register-Allocator wurde zu etwas, das vollendet wirkte... unabhängig davon, ob dein Code schnell oder langsam lief. Ich arbeitete weiterhin am Allocator, um bessere Speicherzuweisungen zu erreichen. Er wurde immer langsamer, lieferte aber immer bessere Leistungen in Fällen, in denen niemand mehr damit zurechtkam. Ich konnte in den Register-Allocator eintauchen, dort einen Monat Arbeit vergraben, und plötzlich begann der gesamte Code, 5 % schneller zu laufen. Das geschah immer wieder, und der Register-Allocator wurde zu etwas wie einem Kunstwerk – alle mochten oder hassten ihn, und die Leute aus der Akademie stellten Fragen wie: 'Warum wird alles genau so gemacht?' Warum nicht , und wo der Unterschied liegt. Die Antwort bleibt dieselbe: Ein Allocator auf Basis der Graphfärbung, kombiniert mit sehr präziser Arbeit am Puffer-Code, ist das Werkzeug des Sieges, die beste Kombination, die niemand schlagen kann. Und das ist ziemlich unauffällig. Alles andere, was der Compiler macht, sind recht gut erforschte Dinge, obwohl sie ebenfalls auf ein Kunstniveau gebracht wurden. Ich habe immer versucht, Dinge zu machen, die den Compiler zu einem Kunstwerk machen sollten. Aber nichts davon war außergewöhnlich – mit Ausnahme des Register-Allocators. Der Trick liegt darin, dass man sorgfältig unter Last, und wenn das passiert (ich kann das gerne näher erklären, falls es interessiert), bedeutet das, dass man aggressiver inline gehen kann, ohne das Risiko einzugehen, über den Performance-Grafikbruchpunkt hinauszukommen. Damals gab es eine Menge vollwertiger Compiler, die mit Spielereien und Extras vollgepackt waren, in denen Register-Allocatoren enthalten waren, aber niemand konnte so etwas mehr erreichen.
Das Problem ist, dass, wenn Sie Inline-Methoden hinzufügen und den Bereich des Inlinings immer weiter vergrößern, die Anzahl der verwendeten Werte sofort die Anzahl der Register übersteigt, und Sie mit dem Spilling konfrontiert werden. Ein kritischer Punkt tritt normalerweise ein, wenn der Allocator versagt, und ein guter Kandidat für das Spilling kostet den anderen, sodass Sie teilweise wirklich verrückte Dinge spillen. Der Wert des Inlinings liegt darin, dass Sie einen Teil des Overheads, den Overhead für Aufrufe und Speicherungen, verlieren; Sie können die Werte intern sehen und weiter optimieren. Die Kosten des Inlinings liegen darin, dass eine große Anzahl lebender Werte entsteht, und wenn Ihr Register-Allocator mehr spillt, als notwendig ist, verlieren Sie sofort. Daher haben die meisten Allocatoren ein Problem: Wenn das Inlining einen bestimmten Punkt überschreitet, beginnt alles zu spillen, und die Leistung kann dann ins Klo gespült werden. Compiler-Entwickler fügen einige Heuristiken hinzu: Beispielsweise wird das Inlining ab einer ausreichend großen Größe gestoppt, da alle Allokationen den Prozess ruinieren können. So entsteht ein Bruch im Leistungskurve – Sie inline, inline, die Leistung wächst langsam – und dann bam! – fällt sie abrupt ab, weil Sie zu viel inlined haben. So lief alles bis zur Einführung von Java. Java erfordert deutlich mehr Inlining, daher musste ich meinen Allocator viel aggressiver gestalten, damit er sich anpasst und nicht abstürzt. Wenn Sie zu viel inlined haben, beginnt er zu spillen, aber irgendwann kommt der Punkt, an dem "kein Spilling mehr" stattfinden kann. Dies ist eine interessante Beobachtung, die mir einfach aus dem Nichts kam, nicht offensichtlich, aber gut bezahlt. Ich habe mich ans aggressive Inlining gewagt, und das führte mich zu Territorien, in denen die Leistung von Java und C Hand in Hand geht. Sie sind tatsächlich sehr nah beieinander – ich kann Java-Code schreiben, der signifikant schneller ist als C-Code und ähnliche Dinge, aber im Durchschnitt, im großen Bild, sind sie ungefähr vergleichbar. Ich denke, ein Teil dieses Erfolges liegt am Register-Allocator, der mir erlaubt, maximal unreflektiert inlined. Ich inline einfach alles, was ich sehe. Die Frage ist, ob der Allocator gut funktioniert und ob sich am Ende sinnvoll funktionierender Code ergibt. Das war eine große Herausforderung: das alles zu verstehen und zum Laufen zu bringen.
Ein bisschen über Registerzuweisung und Multikerntechnik
Wladimir: Probleme wie die Allokation von Registern erscheinen als ein immerwährendes Thema. Interessant wäre, ob eine Idee einmal vielversprechend schien und dann in der Praxis gescheitert ist?
Cliff: Natürlich! Die Registerallokation ist ein Bereich, in dem du versuchst, einige Heuristiken zur Lösung eines NP-vollständigen Problems zu finden. Und du wirst niemals eine perfekte Lösung erreichen können, richtig? Das ist einfach unmöglich. Sieh dir die Ahead-of-Time-Kompilierung an – die funktioniert auch nicht gut. Hier geht es um durchschnittliche Fälle. Um die typischen Leistungswerte, sodass du etwas messen kannst, das du für eine gute, typische Leistung hältst – schließlich arbeitest du an deren Verbesserung! Die Registerallokation ist ein Thema, das sich ganz auf die Leistung konzentriert. Sobald du einen ersten Prototyp hast, der funktioniert und das macht, was nötig ist, beginnt die Arbeit an der Performance. Du musst lernen, gut zu messen. Warum ist das wichtig? Wenn es klare Daten gibt, kannst du verschiedene Bereiche betrachten und sehen: Aha, das hat hier geholfen, aber dort ist alles kaputt! Es kommen einige gute Ideen auf, du fügst eine neue Heuristik hinzu, und plötzlich funktioniert alles im Durchschnitt ein wenig besser. Oder es funktioniert nicht. Ich hatte viele Fälle, in denen wir um fünf Prozent Leistung gekämpft haben, die unsere Entwicklung von dem vorherigen Allokator unterschieden. Und jedes Mal sieht es so aus: irgendwo gewonnene, irgendwo verlorene. Wenn du gute Werkzeuge zur Leistungsanalyse hast, kannst du die schlechten Ideen finden und verstehen, warum sie verlieren. Vielleicht solltest du alles so belassen, wie es ist, oder vielleicht ernsthaft an der Feinabstimmung arbeiten, oder etwas anderes reparieren. Das ist ein ganzes Set von Dingen! Ich habe diesen coolen Hack gemacht, aber du brauchst auch diesen, und diesen, und diesen – und deren Kombination führt zu bestimmten Verbesserungen. Einzelne können scheitern. So ist die Natur der Arbeit an der Leistung NP-vollständiger Probleme.
Wladimir: Es scheint, als ob Themen wie das Lackieren in Allokatoren bereits gelöst sind. Nun, für Sie scheint es gelöst zu sein, wenn man Ihren Erzählungen glaubt. Ist es dann überhaupt sinnvoll…
Cliff: Sie ist nicht als solche gelöst. Du musst sie in eine „gelöste“ umwandeln. Es gibt schwierige Aufgaben, und die müssen gelöst werden. Wenn das erledigt ist, ist es Zeit, sich auf die Leistung zu konzentrieren. Diese Arbeit sollte mit dem entsprechenden Ernst angegangen werden – Benchmarks durchführen, Metriken sammeln, Situationen erläutern, in denen beim Zurücksetzen auf die vorherige Version dein alter Hack wieder funktioniert hat (oder umgekehrt, nicht mehr funktioniert). Und nicht aufgeben, bis du etwas erreicht hast. Wie ich schon sagte, es gibt viele großartige Ideen, die nicht funktioniert haben, aber im Bereich der Registerallokierung sind die Ideen nahezu unbegrenzt. Du kannst beispielsweise wissenschaftliche Veröffentlichungen lesen. Obwohl sich dieser Bereich mittlerweile langsamer entwickelt hat und klarer geworden ist als in seinen Anfangszeiten. Trotzdem arbeiten in diesem Bereich unzählige Menschen, und es lohnt sich, all ihre Ideen auszuprobieren, sie warten alle auf ihre Gelegenheit. Und du kannst nicht beurteilen, wie gut sie sind, wenn du es nicht ausprobierst. Wie gut sie sich in dein gesamtes Allokator-Setup integrieren, denn ein Allokator macht viele Dinge, und einige Ideen funktionieren in deinem spezifischen Allokator nicht, während sie in einem anderen problemlos funktionieren können. Der Hauptansatz zum Erfolg für einen Allokator besteht darin, langsame Dinge aus dem Hauptweg zu entfernen und zwanghaft an den Grenzen langsamer Wege zu splitten. Wenn du also GC auslösen, den langsamen Weg gehen, deoptimieren und Ausnahmen werfen möchtest, all das – du weißt, dass diese Dinge relativ selten sind. Und sie sind wirklich selten, ich habe es überprüft. Du leistest zusätzliche Arbeit, und dadurch verschwinden viele Einschränkungen auf diesen langsamen Wegen, aber das ist nicht sehr wichtig, denn sie sind langsam und werden selten benutzt. Zum Beispiel, ein Nullzeiger – der passiert nie, richtig? Man muss verschiedene Wege für unterschiedliche Dinge haben, aber sie sollten sich nicht im Hauptweg stören.
Wladimir: Was halten Sie von Multithreading, wenn Tausende von Kernen vorhanden sind? Ist das nützlich?
Cliff: Der Erfolg von GPUs zeigt, dass es ziemlich nützlich ist!
Wladimir: Sie sind ziemlich spezialisiert. Wie sieht es mit allgemeinen Prozessoren aus?
Cliff: Nun, das war das Geschäftsmodell von Azul. Die Antwort kam noch aus einer Zeit, als die Menschen sehr auf vorhersehbare Leistung wertlegten. Damals war es schwierig, parallelen Code zu schreiben. Das H2O-Coding-Modell skaliert gut, ist aber kein allgemeines Modell. Vielleicht etwas allgemeiner als bei der Nutzung von GPUs. Sprechen wir über die Komplexität der Entwicklung solcher Systeme oder über die Nutzungskomplexität? Zum Beispiel, eine interessante Lektion, die ich von Azul gelernt habe, ist: Kleine Caches sind in Ordnung.
Die größte Herausforderung im Leben
Wladimir: Wie steht es um nicht-technische Herausforderungen?
Cliff: Die größte Herausforderung bestand darin, nicht nett und freundlich zu den Menschen zu sein. Infolgedessen fand ich mich ständig in extrem konfliktreichen Situationen wieder. Situationen, in denen ich wusste, dass alles schiefgeht, aber nicht wusste, wie ich mit diesen Problemen vorankommen sollte und nicht in der Lage war, sie zu bewältigen. Viele langandauernde Probleme, die über Jahrzehnte hinweg entstanden sind, haben sich genau so entwickelt. Die Tatsache, dass es in Java die Compiler C1 und C2 gibt, ist eine direkte Folge davon. Ebenso, dass es in Java über ein Jahrzehnt lang keine mehrstufige Kompilierung gab. Es war offensichtlich, dass wir ein solches System benötigten, aber es war nicht klar, warum es nicht vorhanden war. Ich hatte Probleme mit einem Ingenieur... oder einer Ingenieursgruppe. Vor langer Zeit, als ich bei Sun anfing, war ich... Nun, nicht nur damals, ich hatte stets meine eigene Meinung zu allem. Und ich hielt es für wahr, dass man einfach seine eigene Wahrheit direkt aussprechen kann. Zumal ich die meiste Zeit erschreckend oft recht hatte. Und wenn dir dieser Ansatz nicht gefällt... insbesondere wenn du offensichtlich falsch liegst und Unsinn machst... Insgesamt konnten nur wenige Menschen mit dieser Kommunikationsform tolerant umgehen. Obwohl einige es konnten, zum Beispiel ich. Mein ganzes Leben habe ich auf meritokratischen Prinzipien aufgebaut. Wenn du mir etwas Falsches zeigst, werde ich mich sofort umdrehen und sagen: Du hast Unsinn gesagt. Dabei entschuldige ich mich natürlich und all das, erkenne die Verdienste an, wenn es welche gibt, und handel dann entsprechend. Auf der anderen Seite habe ich erstaunlich oft recht, was in zwischenmenschlichen Beziehungen nicht sehr gut funktioniert. Ich versuche nicht, nett zu sein, sondern präsentiere die Sache direkt. „Das wird niemals funktionieren, weil eins, zwei und drei“. Und sie sagen: „Oh!“. Es gab auch andere Konsequenzen, die ich wahrscheinlich besser auslasse: zum Beispiel die, die zu meiner Scheidung von meiner Frau geführt haben und zu einem Jahrzehnt Depressionen danach.
Eine Herausforderung besteht darin, mit den Vorstellungen der Menschen darüber zu kämpfen, was man kann und was nicht, was wichtig ist und was nicht. Es gab viele Herausforderungen zum Thema Programmierstil. Ich schreibe weiterhin viel Code, und damals musste ich sogar langsamer werden, weil ich zu viele parallele Aufgaben gemacht habe und sie schlecht erledigte, anstatt mich auf eine einzige zu konzentrieren. Rückblickend habe ich die Hälfte des Codes für das Java JIT-Team oder das C2-Team geschrieben. Der schnellste Programmierer nach mir war schon halb so langsam, der nächste war nochmals halb so langsam, und das war ein exponentieller Rückgang. Die siebte Person in dieser Reihe war extrem langsam – das ist oft der Fall! Ich habe viel Code durchgesehen. Ich habe beobachtet, wer was schreibt, ohne Ausnahmen. Ich habe mir ihren Code angesehen, alle von ihnen bewertet und immer noch mehr geschrieben als jeder von ihnen. Mit Menschen funktioniert so ein Ansatz oft nicht gut. Einige mögen das nicht. Und wenn sie damit nicht umgehen können, entstehen allerhand Beschwerden. Zum Beispiel wurde mir einmal gesagt, ich solle aufhören, Code zu schreiben, weil ich zu viel schreibe und das die Teamdynamik gefährdet, was sich für mich wie ein Witz anhörte: Wenn das gesamte restliche Team verschwinden würde und ich weiterhin Code schreibe, würdest du nur die Hälfte des Teams verlieren. Auf der anderen Seite, wenn ich weiterschreibe und du die Hälfte des Teams verlierst – das klingt nach sehr schlechtem Management. Ich habe nie wirklich darüber nachgedacht oder es angesprochen, aber es war immer irgendwo in meinem Kopf. Irgendwo in den Hintergründen meines Bewusstseins kreiste der Gedanke: „Macht ihr Witze?“ Also war mein größtes Problem ich und meine Beziehungen zu den Menschen. Heute verstehe ich mich viel besser. Ich habe lange Zeit als Teamleiter für Programmierer gearbeitet, und jetzt sage ich den Leuten direkt: „Wisst ihr, so bin ich, und ihr müsst damit umgehen – ist es in Ordnung, wenn ich hier stehe?“ Und als sie damit klarkamen, hat alles funktioniert. Tatsächlich bin ich weder schlecht noch gut; ich habe keine schlechten Absichten oder egoistischen Bestrebungen, es ist einfach mein Wesen, und man muss irgendwie damit leben.
Andrei: Vor kurzem wurde viel über Selbstbewusstsein für Introvertierte und generell über Soft Skills gesprochen. Was kann man dazu sagen?
Cliff: Ja, das war ein Verständnis und eine Lektion, die ich aus meiner Scheidung mit meiner Frau gezogen habe. Was ich aus der Scheidung gewonnen habe, ist das Verständnis meiner selbst. Dadurch begann ich, andere Menschen besser zu verstehen. Ich lernte, wie diese Interaktionen funktionieren. Das führte zu Einsichten, eine nach der anderen. Es entstand das Bewusstsein, wer ich bin und was ich darstelle. Was ich tue: Entweder bin ich mit der Aufgabe beschäftigt, oder ich vermeide Konflikte, oder etwas anderes – und ein solches Maß an Selbstbewusstsein hilft wirklich, sich selbst zu kontrollieren. Danach geht alles viel einfacher. Eine Sache, die ich nicht nur bei mir, sondern auch bei anderen Programmierern entdeckt habe, ist die Unfähigkeit, Gedanken zu verbalisieren, wenn man sich in einem Zustand emotionalen Stresses befindet. Zum Beispiel, man sitzt da und programmiert, ist im Flow, und plötzlich kommen Leute angehetzt und schreien in Panik, dass etwas kaputt ist und sofortige Maßnahmen ergriffen werden müssen. Und man kann kein Wort sagen, weil man sich in einem emotionalen Stresszustand befindet. Das erworbene Wissen hilft, sich auf diesen Moment vorzubereiten, ihn zu überstehen und zu einem Rückzugsplan überzugehen, nach dem man etwas unternehmen kann. Also ja, wenn man anfängt zu verstehen, wie all das funktioniert – es ist ein riesiges, lebensveränderndes Ereignis.
Ich konnte die richtigen Worte nicht finden, erinnerte mich jedoch an die Abfolge der Schritte. Der Punkt ist, dass diese Reaktion sowohl physisch als auch verbal ist und dass du Raum brauchst. Einen solchen Raum im zenartigen Sinne. Genau das muss erklärt werden, und dann musst du sofort einen Schritt zurücktreten – physisch zurücktreten. Wenn ich in Worten schweige, kann ich die Situation emotional verarbeiten. Während das Adrenalin das Gehirn erreicht und dich in den "Kampf oder Flucht"-Modus versetzt, kannst du nichts mehr sagen; du bist jetzt ein Idiot, ein Prügelknabe, unfähig, eine würdige Antwort zu geben oder wenigstens die Attacke zu stoppen, und der Angreifer kann immer wieder angreifen. Zuerst musst du wieder du selbst werden, die Kontrolle zurückerlangen und aus dem "Kampf oder Flucht"-Modus herauskommen.
Und dafür ist verbalem Raum erforderlich. Einfach nur Raum, der frei ist. Wenn du überhaupt etwas sagen möchtest, kannst du genau das behaupten, und dann gehen und tatsächlich deinen "Raum" finden: spaziere durch den Park, sperre dich unter der Dusche ein – ganz gleich. Wichtig ist, sich vorübergehend von der Situation zu lösen. Sobald du auch nur für ein paar Sekunden abschaltest, kehrt die Kontrolle zurück, und du beginnst, klar zu denken. "Gut, ich bin ja kein Idiot, ich mache keine dummen Dinge, ich bin ein ziemlich nützlicher Mensch." Sobald du dich selbst davon überzeugen konntest, ist es Zeit, zum nächsten Schritt überzugehen: herauszufinden, was passiert ist. Du wurdest angegriffen, der Angriff kam aus unerwarteter Richtung, es war ein hinterhältiger Schlag. Das ist schlecht. Der nächste Schritt besteht darin, zu verstehen, warum der Angreifer das nötig hatte. Tatsächlich, warum? Vielleicht, weil er selbst wütend ist? Warum ist er wütend? Zum Beispiel, weil er selbst versagt hat und die Verantwortung nicht übernehmen kann? So muss man vorsichtig die gesamte Situation aufarbeiten. Aber dafür braucht man Raum für Manöver, verbalem Raum. Der erste Schritt besteht darin, den verbalen Kontakt abzubrechen. Sich von der Wortdiskussion zu entfernen. Dies abzubrechen, so schnell wie möglich wegzugehen. Wenn es sich um ein Telefongespräch handelt – lege einfach auf – das ist eine Fähigkeit, die ich aus der Kommunikation mit meiner Ex-Frau gelernt habe. Wenn das Gespräch zu nichts Gutem führt, sag einfach "Auf Wiedersehen" und leg auf. Auf der anderen Seite des Telefons: "bla-bla-bla", du antwortest: "hm, tschüss!" und legst auf. Du brichst einfach das Gespräch ab. Fünf Minuten später, wenn du wieder klar denken kannst, hast du dich ein wenig abgekühlt, und es wird möglich, über das, was passiert ist und was als Nächstes kommt, nachzudenken. Und du beginnst, eine durchdachte Antwort zu formulieren, statt nur emotional zu reagieren. Für mich war der Durchbruch im Selbstbewusstsein genau das, dass ich in Zeiten emotionalen Stresses nicht sprechen kann. Aus diesem Zustand herauszukommen, nachzudenken und zu planen, wie man antwortet und Probleme ausgleicht – das sind die richtigen Schritte, wenn man nicht sprechen kann. Der einfachste Weg ist, aus der Situation zu entfliehen, in der emotionaler Stress entsteht, und einfach nicht mehr an diesem Stress teilzunehmen. Danach gewinnst du die Fähigkeit, zu denken, und wenn du denken kannst, wird das Sprechen ebenfalls möglich und so weiter.
Übrigens versucht der Anwalt der Gegenseite im Gericht, dies mit dir zu tun – jetzt wird klar, warum. Denn er hat die Möglichkeit, dich so zu überwältigen, dass du nicht einmal in der Lage bist, deinen eigenen Namen auszusprechen. Im wahrsten Sinne des Wortes kannst du nicht sprechen. Wenn dir das passiert und du weißt, dass du an einem Ort sein wirst, wo verbale Kämpfe toben, wie im Gerichtssaal, dann solltest du mit deinem Anwalt kommen. Der Anwalt wird dich vertreten und die verbale Attacke stoppen, und das auf ganz legale Weise, sodass du dein verlorenes Zen-Gefühl zurückgewinnst. Zum Beispiel musste ich ein paar Mal meine Familie anrufen, der Richter war dabei durchaus freundlich, doch der Anwalt der Gegenseite schrie und schrie mich an, ich konnte nicht einmal ein Wort einfügen. In solchen Fällen funktioniert für mich am besten, einen Vermittler zu nutzen. Der Vermittler stoppt den konstanten Druck, der ununterbrochen auf dir lastet; du findest den notwendigen Raum für dein Zen, und mit ihm kommt auch die Fähigkeit zurück zu sprechen. Das ist ein ganzes Wissensgebiet, in dem man viel lernen und in sich selbst entdecken muss, und all das führt zu hochgradigen strategischen Entscheidungen, die für verschiedene Menschen unterschiedlich sind. Manche Menschen haben die oben beschriebenen Probleme nicht, normalerweise haben sie keine, wenn sie professionell im Verkauf tätig sind. All diese Menschen, die ihren Lebensunterhalt mit Worten verdienen – berühmte Sänger, Dichter, religiöse Führer und Politiker, sie haben immer etwas zu sagen. Sie haben keine solchen Probleme, während ich sie habe.
Andrei: Es war… unerwartet. Gut, wir haben bereits viel gesprochen und es ist Zeit, dieses Interview zu beenden. Wir werden uns auf der Konferenz treffen und diesen Dialog fortsetzen. Treffen wir uns auf der Hydra!
Die Unterhaltung mit Cliff wird auf der Hydra 2019 Konferenz fortgesetzt, die am 11. und 12. Juli 2019 in Sankt Petersburg stattfinden wird. Er wird mit einem Vortrag kommen . Tickets sind erhältlich .
Quelle: habr.com
