Ein großes Interview mit Cliff Click — dem Vater der JIT-Kompilierung in Java.

Ein großes Interview mit Cliff Click — dem Vater der JIT-Kompilierung in Java.Cliff Click — CTO des Unternehmens 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 seinen ersten Compiler mit 15 Jahren (Pascal für TRS Z-80)! Er ist am bekanntesten für seine Arbeit an C2 in Java (the Sea of Nodes IR). Dieser Compiler zeigte der Welt, dass JIT hochwertigen Code erzeugen kann, was zu einem der Faktoren für die Etablierung von Java als einer der Hauptplattformen für moderne Software wurde. Später half Cliff dem Unternehmen Azul Systems, einen 864-Kern-Mainframe mit Software in reinem Java zu entwickeln, der GC-Pausen auf einem 500-GB-Heap in weniger als 10 Millisekunden unterstützte. Insgesamt hat Cliff an allen Aspekten der JVM gearbeitet.

 
Dieser Hub-Post ist ein großes Interview mit Cliff. Wir werden über folgende Themen sprechen:

  • Übergang zu niedrigstufigen Optimierungen
  • Wie man eine große Refaktorisierung durchführt
  • Kostenmodell
  • Lernen von niedrigstufigen Optimierungen
  • Praktische Beispiele zur Leistungsverbesserung
  • Warum man seine eigene Programmiersprache erstellen sollte
  • Karriere als Performance-Ingenieur
  • Technische Herausforderungen
  • Ein bisschen über Registerzuweisung und Multicore-Verarbeitung
  • Die größte Herausforderung im Leben

Das Interview führen:

  • Andrey Sataryan von Amazon Web Services. In seiner Karriere hat er in ganz unterschiedlichen Projekten gearbeitet: Er testete die verteilte NewSQL-Datenbank bei Yandex, ein cloudbasiertes Erkennungssystem im Kaspersky Lab, ein Mehrspieler-Spiel bei Mail.ru und einen Dienst zur Berechnung von Währungswerten bei der Deutschen Bank. Er interessiert sich für das Testen von großangelegten Backend- und verteilten Systemen.
  • Vladimir Sitnikov von Netcracker. Er arbeitet seit zehn Jahren an der Leistung und Skalierbarkeit von NetCracker OS – Software, die von Kommunikationsanbietern zur Automatisierung von Netzwerk- und Netzwerkausrüstungsmanagementprozessen verwendet wird. Er beschäftigt sich mit Fragen zur Java-Performance und Oracle Database. Er ist der Autor von mehr als einem Dutzend Leistungsverbesserungen im offiziellen PostgreSQL JDBC-Treiber.

Übergang zu niedrigstufigen Optimierungen

Andrey: Sie sind eine bekannte Persönlichkeit in der Welt der JIT-Compilation, in Java und der Arbeit an Performance im Allgemeinen, richtig? 

Cliff: Das stimmt!

Andrey: Lassen Sie uns mit allgemeinen Fragen zur Arbeitsweise bei der Performance beginnen. Was denken Sie über die Wahl zwischen hoch- und niedrigstufigen Optimierungen wie der Arbeit auf Level CPU?

Cliff: Hier ist alles einfach. Der schnellste Code ist der, der nie ausgeführt wird. Daher sollte man immer mit einem hohen Level beginnen und an den Algorithmen arbeiten. Eine bessere O-Notation wird eine schlechtere übertreffen, es sei denn, es kommen große Konstanten ins Spiel. Niedrigstufige Dinge kommen ganz zuletzt. Normalerweise, wenn Sie den gesamten restlichen Stack gut optimiert haben und immer noch etwas Interessantes übrig bleibt – das ist der niedrigste Level. Aber wie fängt man auf hohem Niveau an? Wie erkennt man, dass man genug Arbeit auf hohem Niveau geleistet hat? Nun… eigentlich nicht. Es gibt keine fertigen Rezepte. Man muss das Problem durchdringen, entscheiden, was man tun will (um keine unnötigen Schritte zu machen), und erst dann kann man den Profiler entblößen, der etwas Nützliches sagen kann. Irgendwann merkt man selbst, dass man sich von unnötigen Dingen befreit hat und es an der Zeit ist, sich mit der Feineinstellung auf niedrigem Niveau zu beschäftigen. Das ist definitiv eine besondere Kunstform. Viele Leute erledigen unnötige Dinge, bewegen sich aber so schnell, dass sie keine Zeit haben, sich um die Leistung zu kümmern. Aber das ist, bis das Thema zur Sprache kommt. In der Regel interessiert es 99% der Zeit niemanden, womit ich beschäftigt bin, bis ein wichtiger Aspekt auf dem kritischen Weg auftaucht, der für jemanden von Bedeutung ist. Und dann fangen alle an, dich zu fragen, „warum es von Anfang an nicht perfekt funktioniert hat“. Im Grunde gibt es immer etwas, das die Leistung verbessern kann. Aber 99% der Zeit hast du keine Anhaltspunkte! Du versuchst einfach, etwas zum Laufen zu bringen und verstehst im Prozess, was wichtig ist. Man kann niemals im Voraus wissen, dass genau dieses Stück perfekt sein muss, daher muss man im Wesentlichen in allem perfekt sein. Und das ist unmöglich und so macht man es nicht. Es gibt immer unzählige Dinge zu reparieren – und das ist ganz normal.

Wie man eine große Refaktorisierung durchführt

Andrey: Wie arbeiten Sie an der Leistung? Das ist doch ein durchgängiges Problem. Hatten Sie beispielsweise schon einmal mit Problemen zu tun, die aus der Überlappung einer großen Menge bereits vorhandener Funktionalität resultieren?

Cliff: Ich versuche, das zu vermeiden. Wenn ich weiß, dass die Leistung ein Problem werden könnte, denke ich nach, bevor ich mit dem Programmieren beginne, insbesondere über Datenstrukturen. Aber oft entdeckt man all das erst viel später. Und dann muss man extreme Maßnahmen ergreifen und das tun, was ich „schreibe neu und herrsche“ nenne: Man muss sich ein ausreichend großes Stück schnappen. Einen Teil des Codes muss man so oder so neu schreiben, sei es wegen Leistungproblemen oder aus einem anderen Grund. Welcher Grund auch immer für das Neuschreiben des Codes vorliegt, es ist fast immer besser, ein größeres Stück neu zu schreiben als ein kleineres Stück. In diesem Moment fangen alle an zu zittern aus Angst: „Oh Gott, man kann so viel Code nicht anfassen!“ Aber in der Tat funktioniert dieser Ansatz fast immer viel besser. Man muss sich sofort eines großen Problems annehmen, einen großen Kreis darum ziehen und sagen: Alles, was innerhalb des Kreises ist, schreibe ich neu. Die Grenze ist schließlich viel kleiner als der Inhalt, der darin ersetzt werden soll. Und wenn eine solche Grenzziehung die Arbeit darin perfekt macht – hast du freie Hand, mach, was du willst. Sobald du das Problem verstanden hast, verläuft der Neuschreibprozess viel einfacher, also beiße in ein großes Stück!
Gleichzeitig, wenn du ein großes Stück neu schreibst und verstehst, dass die Leistung ein Problem werden könnte, kannst du sofort anfangen, dir darüber Gedanken zu machen. Normalerweise wird das zu einfachen Dingen wie „Kopiere keine Daten, verwalte Daten so einfach wie möglich, mache sie kleiner“. Bei großen Neuschreibungen gibt es Standards zur Verbesserung der Leistung. Diese drehen sich fast immer um die Daten.

Kostenmodell

Andrey: In einem der Podcasts habt ihr über Kostenmodelle im Kontext der Leistung gesprochen. Könnt ihr erklären, was damit gemeint war?

Cliff: Natürlich. Ich wurde in einer Zeit geboren, in der die Prozessorleistung extrem wichtig war. Und diese Ära kehrt wieder zurück – das Schicksal kennt keinen Mangel an Ironie. Ich begann zu leben, als es noch 8-Bit-Maschinen gab, mein erster Computer arbeitete mit 256 Bytes. Genau, mit Bytes. Alles war sehr klein. Man musste die Anweisungen zählen, und sobald wir anfingen, in die höheren Programmiersprachen aufzusteigen, übernahmen die Sprachen immer mehr und mehr. Es gab Assembler, dann Basic, dann C, und C übernahm die Arbeit mit vielen Details, wie Registerzuweisungen und das Finden von Anweisungen. Aber dort war alles ziemlich klar, und wenn ich einen Zeiger auf eine Instanz einer Variable machte, bekam ich einen Load, und die Kosten für diese Anweisung sind bekannt. Die Hardware liefert eine bekannte Anzahl von Maschinenzyklen, sodass die Ausführungsgeschwindigkeit verschiedener Dinge einfach berechnet werden kann, indem man alle Anweisungen zusammenzählt, die man ausführen möchte. Jede compare/test/branch/call/load/store konnte zusammengezählt werden, und man konnte sagen: hier ist die Ausführungszeit. Wenn man an der Leistungsverbesserung arbeitet, fällt einem auf, welche Zahlen mit den kleinen heißen Zyklen übereinstimmen. 
Aber sobald du auf Java, Python und ähnliche Dinge umschaltest, entfernst du dich sehr schnell vom Low-Level-Hardware. Wie hoch sind die Kosten für den Aufruf eines Getters in Java? Wenn JIT in HotSpot alles richtig inlined, dann wird es ein Load sein, aber wenn er das nicht gemacht hat – wird es ein Funktionsaufruf sein. Da der Aufruf in einem heißen Zyklus liegt, wird er alle anderen Optimierungen in diesem Zyklus zunichte machen. Daher werden die tatsächlichen Kosten viel höher sein. Und du verlierst sofort die Fähigkeit, auf einen Codeabschnitt zu schauen und zu verstehen, dass wir ihn in Bezug auf die Taktfrequenz des Prozessors, den verwendeten Speicher und den Cache ausführen sollten. All das wird nur interessant, wenn man wirklich in die Performance eintaucht.
Heute befinden wir uns in einer Situation, in der die Prozessorgeschwindigkeiten seit einem Jahrzehnt kaum gewachsen sind. Die alten Zeiten kehren zurück! Sie können nicht mehr auf gute einzelne Thread-Leistung zählen. Aber wenn Sie sich zufällig mit paralleler Verarbeitung befassen – das ist wahnsinnig kompliziert, alle schauen Sie an wie James Bond. Zehnfache Beschleunigungen entstehen hier normalerweise an Stellen, wo jemand etwas verpasst hat. Parallelität erfordert viel Arbeit. Um diese zehnfache Beschleunigung zu erreichen, muss man das Preis-Leistungs-Modell verstehen. Was kostet was. Und dazu muss man verstehen, wie die Sprache auf die zugrunde liegende Hardware abgestimmt ist.
Martin Thompson hat ein passendes Wort für seinen Blog gefunden Mechanische Sympathie! Man muss verstehen, was die Hardware vorhat, wie genau sie es macht und warum sie überhaupt das tut, was sie tut. Mit diesem Wissen ist es ziemlich einfach, die Anweisungen zu zählen und herauszufinden, wo die Ausführungszeit verloren geht. Wenn Sie jedoch nicht die richtige Vorbildung haben, suchen Sie einfach die schwarze Katze im dunklen Raum. Ich sehe ständig Menschen, die die Leistung optimieren, ohne die geringste Ahnung zu haben, was sie eigentlich tun. Sie kämpfen sehr und kommen nicht wirklich voran. Und wenn ich denselben Code nehme, ein paar kleine Hacks einfüge und fünf- oder zehnfache Beschleunigung erziele, sagen sie: „Nun, das ist nicht fair, wir wussten sowieso, dass du besser bist.“ Erstaunlich. Worüber rede ich… das Preis-Leistungs-Modell – das betrifft, welchen Code man schreibt und wie schnell er im Durchschnitt im Gesamtkontext arbeitet.

Andrey: Und wie hält man so viel im Kopf? Das erreicht man mit viel Erfahrung, oder? Wo bekommt man diese Erfahrung her?

Cliff: Nun, meine Erfahrung habe ich auf nicht gerade einfache Weise gesammelt. Ich habe noch zu Zeiten in Assembler programmiert, als man jede einzelne Anweisung verstehen konnte. Es mag dumm klingen, aber seitdem ist mir der Befehlssatz des Z80 fest im Gedächtnis geblieben. Ich erinnere mich nicht an die Namen von Menschen schon eine Minute nach dem Gespräch, aber ich erinnere mich an den Code, den ich vor 40 Jahren geschrieben habe. Komisch, das sieht aus wie das Syndrom der "schlechten Wissenschaftler».

Lernen von niedrigstufigen Optimierungen

Andrey": Gibt es einen einfacheren Weg, da reinzukommen?

Cliff: Ja und nein. Das Metall, das wir alle benutzen, hat sich in dieser Zeit nicht so sehr verändert. Alle verwenden x86, außer Smartphones mit Arm. Wenn du dich nicht mit hartem Embedded Programming beschäftigst, hast du alles genau so wie vorher. Gut, weiter. Die Instruktionen haben sich seit Jahrhunderten nicht geändert. Du musst losgehen und etwas in Assembly schreiben. Ein wenig, aber genug, um zu anfangen, es zu verstehen. Ihr lächelt, und ich meine das ganz ernst. Es ist wichtig, das Verhältnis zwischen Sprache und Hardware zu verstehen. Danach musst du versuchen, ein kleines Spielzeug-Compiler für eine kleine Spielzeug-Sprache zu schreiben. „Spielzeug“ bedeutet, dass es in einem vernünftigen Zeitrahmen gemacht werden muss. Es kann super einfach sein, aber es muss Anweisungen generieren. Der Akt der Generierung von Anweisungen wird helfen, das Kostenmodell für die Brücke zwischen dem Hochsprachen-Code, an dem alle arbeiten, und dem Maschinencode, der auf der Hardware ausgeführt wird, zu verstehen. Diese Beziehung wird sich im Kopf festsetzen, während du den Compiler schreibst. Auch bei dem einfachsten Compiler. Danach kannst du anfangen, dir Java anzusehen und feststellen, dass ihr semantischer Abgrund viel tiefer ist, und dass es viel schwieriger ist, Brücken darüber zu bauen. 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 du brauchst einen Ausgangspunkt, wenn du den Code ansiehst und verstehst: „Ah, dieser Getter sollte immer inline gesetzt werden.“ Und dann stellt sich heraus, dass dies manchmal der Fall ist, außer wenn die Methode zu groß wird und der JIT alles mögliche inline setzt. Die Performance solcher Stellen kann man sofort vorhersagen. Normalerweise funktionieren Getter gut, aber dann siehst du dir große heiße Schleifen an und verstehst, dass dort einige Funktionsaufrufe enthalten sind, deren Zweck unklar ist. Das ist das Problem mit der weit verbreiteten Verwendung von Gettern, der Grund, warum sie nicht inline gesetzt werden: Es ist unklar, ob es sich um einen Getter handelt. Wenn du eine super kleine Codebasis hast, kannst du sie einfach auswendig lernen und dann sagen: „Das ist ein Getter, das ist ein Setter.“ In einer großen Codebasis hat jede Funktion ihre eigene Geschichte, die im Grunde niemand kennt. Der Profiler sagt, dass wir 24 % der Zeit mit einer bestimmten Schleife verloren haben, und um zu verstehen, was diese Schleife tut, musst du dir jede Funktion darin ansehen. Es ist unmöglich, das zu verstehen, ohne die Funktion zu studieren, und das verlangsamt den Verständnisprozess erheblich. Deshalb benutze ich keine Getter und Setter, ich bin auf ein neues Level gekommen!
Woher bekomme ich ein Preis-Modell? Nun, ich kann natürlich etwas lesen... Aber ich denke, der beste Weg ist zu handeln. Ein kleiner Compiler zu erstellen ist der beste Weg, das Preis-Modell zu verstehen und es sich selbst klarzumachen. Ein kleiner Compiler, der sich für das Programmieren einer Mikrowelle eignet – das ist eine Aufgabe für Anfänger. Ich meine, wenn du bereits Programmierkenntnisse hast, sollte es ausreichen. All diese Sachen wie das Parsen einer Zeile, die ein algebraischer Ausdruck sein wird, die Anweisungen der mathematischen Operationen in der richtigen Reihenfolge herauszuziehen, die richtigen Werte aus den Registern zu nehmen – das alles ist einfach. Während du das machst, wird es sich in deinem Gedächtnis festsetzen. Ich denke, jeder weiß, was ein Compiler macht. Und das wird ein Verständnis für das Preis-Modell schaffen.

Praktische Beispiele zur Leistungsverbesserung

Andrey: Worauf sollte man beim Arbeiten an der Leistungsfähigkeit noch achten?

Cliff: Datenstrukturen. Übrigens, ich habe schon lange keinen Unterricht mehr gegeben... Rocket School. Es machte Spaß, verlangte aber so viel Einsatz, und ich habe schließlich auch noch ein Leben! Also, bei einer der großen und interessanten Klassen, "Wohin geht eure Leistung", gab ich den Studenten ein Beispiel: Zweieinhalb Gigabyte Fintech-Daten wurden aus einer CSV-Datei gelesen und dann musste die Anzahl der verkauften Produkte berechnet werden. Gewöhnliche Tick-Marktdaten. UDP-Pakete, die seit den 70er Jahren in ein Textformat umgewandelt wurden. Chicago Mercantile Exchange – Sachen wie Öl, Mais, Sojabohnen und dergleichen. Man musste diese Produkte zählen, die Anzahl der Transaktionen, das durchschnittliche Volumen der Geld- und Warenbewegungen usw. Das ist ziemlich einfache Handelsmathematik: Den Produktcode finden (das sind 1-2 Zeichen in einer Hash-Tabelle), die Summe erhalten, sie in einen der Satz von Transaktionen hinzufügen, das Volumen, die Kosten und ein paar andere Dinge hinzufügen. Sehr einfache Mathematik. Die Spielzeug-Implementierung war sehr geradlinig: Alles liegt in einer Datei, ich lese die Datei und arbeite mich durch sie, teile die einzelnen Einträge in Java-Strings auf, suche darin die benötigten Dinge und addiere sie gemäß der oben beschriebenen Mathematik. Und es funktioniert mit einer gewissen Geschwindigkeit.

Mit diesem Ansatz ist klar, was passiert, und paralleles Rechnen wird hier nicht helfen, richtig? Es stellt sich heraus, dass man durch die Wahl der richtigen Datenstrukturen eine fünffache Leistungssteigerung erzielen kann. Das überrascht sogar erfahrene Programmierer! In meinem speziellen Fall lag der Fokus darauf, dass man im heißen Schleifen keinen Speicher allokieren sollte. Naja, das ist nicht die gesamte Wahrheit, aber insgesamt – man sollte nicht „einmal pro X“ allokieren, wenn X groß genug ist. Wenn X zwei und einen halben Gigabyte beträgt, sollte man nichts „einmal für einen Buchstaben“, „einmal für eine Zeile“ oder „einmal für ein Feld“ allokieren, nichts in dieser Art. Genau dafür wird die Zeit benötigt. Wie funktioniert das eigentlich? Stellen Sie sich vor, ich mache einen Aufruf String.split() oder BufferedReader.readLine(). Readline macht eine Zeichenkette aus einem Satz von Bytes, die über das Netzwerk empfangen werden, einmal für jede Zeile, für jede der Hundert Millionen Zeilen. Ich nehme diese Zeile, analysiere sie und werfe sie weg. Warum ich sie wegwerfe – nun, ich habe sie bereits verarbeitet, das ist es. Daher wird für jedes Byte, das aus diesen 2.7G gelesen wird, zwei Zeichen in der Zeichenkette geschrieben, das sind bereits 5.4G, und die brauche ich danach nicht mehr, also werden sie weggeworfen. Wenn man sich die Durchsatzleistung des Speichers ansieht, laden wir 2.7G, die durch den Speicher und den Speicherbus in die CPU gehen, und dann werden doppelt so viele in die im Speicher liegende Zeichenkette gesendet, und all das wird beim Erstellen jeder neuen Zeichenkette überschrieben. Aber ich muss sie lesen, die Hardware liest sie, selbst wenn danach alles überschrieben wird. Und ich muss sie schreiben, weil ich eine Zeichenkette erstellt habe und die Caches überlaufen – der Cache kann nicht 2.7G aufnehmen. Insgesamt lese ich für jedes gelesene Byte noch zwei zusätzliche Bytes und schreibe zwei zusätzliche Bytes, und am Ende haben wir ein Verhältnis von 4:1 – mit so einem Verhältnis verschwenden wir ineffizient die Speicherbandbreite. Und dann stellt sich heraus, dass, wenn ich String.split() – das mache, mache ich das bei weitem nicht zum letzten Mal, darin können noch 6-7 Felder vorhanden sein. Daher führt der klassische CSV-Lese-Code mit anschließender Zeilenanalyse zu einem Verlust der Speicherbandbreite im Bereich von 14:1 im Vergleich zu dem, was man tatsächlich gerne hätte. Wenn man diese Allokationen eliminiert, kann man eine fünfmal schnellere Verarbeitung erreichen.

Und das ist nicht wirklich sehr kompliziert. Wenn man den Code aus dem richtigen Winkel betrachtet, wird alles ziemlich einfach, sobald man das Wesen des Problems erkannt hat. Man sollte überhaupt nicht aufhören, Speicher zuzuweisen: Das Problem ist nur, dass man etwas zuweist und es sofort stirbt, wodurch wichtige Ressourcen verbrannt werden, die in diesem Fall – die Speicherkapazität – sind. Und das führt zu einem Rückgang der Leistung. Auf x86 muss man normalerweise aktiv Taktzyklen des Prozessors verbrauchen, hier jedoch hat man den gesamten Speicher viel früher verbrannt. Die Lösung besteht darin, die Anzahl der Zuweisungen zu reduzieren. 
Ein weiterer Teil des Problems ist, dass, wenn man einen Profiler startet, wenn der Speicherplatz erschöpft ist, genau in dem Moment, in dem das passiert, man normalerweise auf die Rückkehr des Caches wartet, weil er mit Müll gefüllt ist, den man gerade erzeugt hat, mit all diesen Zeilen. Daher wird jede Load- oder Store-Operation langsam, da sie zu Cache-Fehlgriffen führt – der gesamte Cache ist langsam geworden und wartet darauf, dass der Müll abtransportiert wird. Daher wird der Profiler nur ein warmes zufälliges Rauschen zeigen, das sich über den gesamten Zyklus erstreckt – es wird keine einzelne heiße Anweisung oder Stelle im Code geben. Nur Rauschen. Und wenn man sich die GC-Zyklen ansieht, werden sie alle aus der Young Generation stammen und super schnell sein – Mikrosekunden oder höchstens Millisekunden. Denn der gesamte Speicher stirbt sofort. Man weist Milliarden von Gigabyte zu, und er schneidet sie ab, schneidet sie ab und schneidet sie erneut ab. Das alles passiert sehr schnell. Man hat also billige GC-Zyklen, warmes Rauschen über den gesamten Zyklus, aber man möchte eine 5-fache Beschleunigung erzielen. In diesem Moment sollte in einem der Kopf etwas „Warum ist das so?!“ zusammenschlagen. Ein Überlauf des Speicherbereichs wird im klassischen Debugger nicht angezeigt; man muss den Debugger der Hardware-Leistungszähler starten und es selbst direkt sehen. Und indirekt kann man dies aus diesen drei Symptomen vermuten. Das dritte Symptom ist, wenn man schaut, was man zuweist, den Profiler fragt, und er antwortet: „Du hast eine Milliarde Zeilen erzeugt, aber der GC hat kostenlos gearbeitet.“ Sobald das passiert ist, begreift man, dass man zu viele Objekte erzeugt hat und den gesamten Speicher verbrannt hat. Es gibt einen Weg, dies zu klären, aber er ist nicht offensichtlich. 

Das Problem liegt in der Datenstruktur: eine bloße Struktur, die all dem zugrunde liegt, sie ist zu groß, sie belegt 2,7 GB auf der Festplatte, daher ist es sehr unerwünscht, eine Kopie davon zu erstellen – man möchte sie direkt aus dem Netzwerk-Byte-Puffer in die Register laden, um nicht fünfmal hin und her zwischen dort und zurück zu lesen oder zu schreiben. Leider stellt Java standardmäßig keine solche Bibliothek im JDK zur Verfügung. Aber das ist doch trivial, oder? Im Grunde sind das 5-10 Zeilen Code, die für die Implementierung eines eigenen gepufferten Zeilenladers benötigt werden, der das Verhalten der String-Klasse nachahmt, während er gleichzeitig eine Wrapper um den darunterliegenden Byte-Puffer bildet. Infolgedessen scheint es, dass man fast mit Strings arbeitet, tatsächlich jedoch bewegen sich die Zeiger im Puffer, und rohe Bytes werden nirgendwo kopiert, wodurch immer wieder dieselben Puffer wiederverwendet werden und das Betriebssystem gerne die Dinge übernimmt, für die es vorgesehen ist, wie die versteckte doppelte Pufferung dieser Byte-Puffer, sodass man selbst nicht mehr einen endlosen Strom unnötiger Daten verarbeitet. Übrigens, Sie verstehen doch, dass beim Arbeiten mit GC garantiert wird, dass jede Speicherzuweisung nach dem letzten GC-Zyklus nicht mehr vom Prozessor gesehen wird? Daher kann all dies nicht im Cache sein, und was darauf folgt, ist ein 100%-garantierter Fehlschlag. Wenn man mit einem Zeiger arbeitet, dauert es auf x86 1-2 Takte, um ein Register aus dem Speicher zu lesen, und sobald das passiert, bezahlt man, bezahlt man, bezahlt man, denn der gesamte Speicher ist in NINE Caches – und das ist die Kosten für die Speicherzuweisung. Die wahre Kosten.

Mit anderen Worten, Datenstrukturen sind das, was am schwierigsten zu ändern ist. Und sobald Sie erkennen, dass Sie die falsche Datenstruktur gewählt haben, die später die Leistung beeinträchtigen wird, ist in der Regel erheblicher Aufwand erforderlich, aber wenn Sie das nicht tun, wird es später schlimmer. Zunächst sollten Sie über Datenstrukturen nachdenken, das ist wichtig. Die Hauptkosten hier liegen bei den komplexen Datenstrukturen, die oft im Stil "Ich habe die Datenstruktur X in die Datenstruktur Y kopiert, weil mir Y optisch besser gefällt" verwendet werden. Aber die Kopieroperation (die kostengünstig erscheint) verbraucht tatsächlich Speicherbandbreite, und hier liegt alles verlorene Rechenzeit begraben. Wenn ich eine riesige Zeichenkette mit JSON habe und sie in einen strukturierten DOM-Baum von POJO oder etwas Ähnlichem umwandeln möchte, wird die Parsing-Operation dieser Zeichenkette und die Erstellung von POJO, gefolgt von einem weiteren Zugriff auf das POJO, zusätzliche Kosten verursachen – das ist nicht billig. Es sei denn, Sie greifen viel häufiger auf das POJO zu als auf die Zeichenkette. Alternativ könnte man auch versuchen, die Zeichenkette zu entschlüsseln und nur das Nötigste herauszuziehen, ohne sie in POJO umzuwandeln. Wenn das alles auf einem Weg geschieht, der maximale Leistung erfordert, gibt es keine POJO – man muss direkt in der Zeichenkette arbeiten.

Warum man seine eigene Programmiersprache erstellen sollte

Andrey: Sie haben gesagt, dass Sie für das Verständnis des Kostenmodells eine eigene kleine Sprache schreiben müssen…

Cliff: Nicht eine Sprache, sondern einen Compiler. Sprache und Compiler sind verschiedene Dinge. Der größte Unterschied liegt im eigenen Kopf. 

Andrey: Übrigens, soweit ich weiß, experimentieren Sie mit dem Erstellen eigener Sprachen. Warum?

Cliff: Weil ich kann! Ich bin zur Hälfte im Ruhestand, also ist das mein Hobby. Mein ganzes Leben lang habe ich die Sprachen anderer Leute umgesetzt. Außerdem habe ich viel an der Codierstilistik gearbeitet. Und auch, weil ich Probleme in anderen Sprachen sehe. Ich sehe, dass es bessere Wege gibt, um alltägliche Dinge zu tun. Und ich würde sie nutzen. Ich habe einfach die Nase voll, die Probleme in mir, in Java, in Python und in jeder anderen Sprache zu sehen. Momentan schreibe ich in React Native, JavaScript und Elm als Hobby, das nichts mit dem Ruhestand zu tun hat, sondern mit aktiver Arbeit. Ich schreibe auch in Python und werde wahrscheinlich weiterhin an maschinellem Lernen für Java-Backends arbeiten. Es gibt viele beliebte Sprachen und jede hat interessante Besonderheiten. Jede ist in ihrem eigenen Bereich gut und man kann versuchen, all diese Features zu vereinen. Also beschäftige ich mich mit dem Lernen interessanter Dinge für mich, dem Verhalten der Sprache, und versuche, eine sinnvolle Semantik zu erfinden. Und bisher gelingt es mir gut! Momentan kämpfe ich mit der Semantik des Speichers, weil ich sie wie in C und Java haben möchte und ein starkes Speicher-Modell sowie eine Semantik für Loads und Stores bekommen möchte. Dabei will ich eine automatische Typableitung wie in Haskell haben. Hier, ich versuche, eine Haskell-ähnliche Typableitung mit Speicher zu mischen, der wie in C und Java funktioniert. Damit beschäftige ich mich seit 2-3 Monaten, zum Beispiel.

Andrey: Wenn Sie eine Sprache erstellen, die die besten Aspekte anderer Sprachen übernimmt, haben Sie darüber nachgedacht, dass jemand das Gegenteil macht: Ihre Ideen nimmt und sie bei sich selbst einsetzt?

Cliff: Genau 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ß, um Typsicherheit, Array-Grenzkontrollen, GC hinzuzufügen und einige Dinge aus C zu verbessern. Sie haben ihre eigenen hinzugefügt. Aber sie haben sich stark inspirieren lassen, oder? Alle stehen auf den Schultern von Riesen, die vor einem waren – so funktioniert der Fortschritt.

Andrey: Soweit ich verstehe, wird Ihre Sprache sicher im Umgang mit dem Speicher sein. Haben Sie darüber nachgedacht, etwas wie den Borrow Checker aus Rust zu implementieren? Haben Sie ihn angesehen, wie gefällt er Ihnen?

Cliff: Also, ich programmiere schon eine Ewigkeit in C, mit all diesen malloc und free, und verwalte die Lebensdauer manuell. Wissen Sie, 90-95% der manuell verwalteten Lebensdauer haben die gleiche Struktur. Und es ist sehr, sehr mühsam, das von Hand zu machen. Ich wünschte, der Compiler würde einfach sagen, was da passiert und was Sie mit Ihren Aktionen erreicht haben. Für gewisse Dinge macht der Borrow Checker das standardmäßig. Außerdem sollte er automatisch Informationen ableiten, alles verstehen und mich nicht mit der Notwendigkeit belasten, dieses Verständnis zu formulieren. Er sollte mindestens eine lokale Escape-Analyse durchführen, und nur wenn das nicht funktioniert, sollten Typanmerkungen hinzugefügt werden, die die Lebensdauer beschreiben – und ein solches Schema ist viel komplizierter als der Borrow Checker oder überhaupt jeder existierende Speicherprüfer. Die Wahl zwischen „alles in Ordnung“ und „ich habe nichts verstanden“ – nein, es sollte etwas Besseres geben. 
Als jemand, der viel Code in C geschrieben hat, halte ich die Unterstützung von automatischer Speicherverwaltung für eine der wichtigsten Dinge. Zudem stört mich, wie viel Speicher Java verbraucht, und die größte Beschwerde betrifft den GC. Bei der Speicherzuweisung in Java erhältst du den Speicher, der im letzten GC-Zyklus lokal war, nicht zurück. In Sprachen mit präziserer Speicherverwaltung ist das anders. Wenn du malloc aufrufst, erhältst du sofort Speicher, der normalerweise gerade verwendet wurde. Meistens machst du mit dem Speicher temporäre Dinge und gibst ihn sofort zurück. Und er geht sofort in den malloc-Pool zurück, und der nächste malloc-Zyklus zieht ihn wieder heraus. Deshalb reduziert sich die tatsächliche Speichernutzung auf die Menge der lebenden Objekte zu einem bestimmten Zeitpunkt plus Lecks. Und wenn du nicht völlig unangemessen leckst, bleibt der größte Teil des Speichers in Caches und Prozessoren, und das funktioniert schnell. Aber es erfordert viel manuelle Speicherverwaltung mit malloc und free, die in der richtigen Reihenfolge und am richtigen Ort aufgerufen werden müssen. Rust kann das eigenständig gut bewältigen und in vielen Fällen sogar eine höhere Leistung bieten, da der Speicherverbrauch nur auf die aktuellen Berechnungen beschränkt ist – im Gegensatz zur Erwartung des nächsten GC-Zyklus, der Speicher freigibt. Am Ende haben wir einen sehr interessanten Weg gefunden, die Leistung zu steigern. Und ziemlich leistungsstark – ich habe mich mit solchen Sachen bei der Datenverarbeitung für Fintech beschäftigt, und das konnte eine Beschleunigung um das Fünffache bringen. Das ist eine erhebliche Beschleunigung, besonders in einer Welt, in der die Prozessoren nicht schneller werden und wir weiterhin auf Verbesserungen warten.

Karriere als Performance-Ingenieur

Andrey: Ich würde auch gerne allgemein zur Karriere fragen. Sie wurden berühmt durch Ihre Arbeit an JIT in HotSpot, und sind dann zu Azul gewechselt – das ist auch ein JVM-Unternehmen. Aber dort haben Sie sich mehr mit Hardware als mit Software beschäftigt. Und dann haben Sie plötzlich auf Big Data und Machine Learning umgeschaltet, und später auf Betrugserkennung. Wie ist das passiert? Das sind sehr unterschiedliche Entwicklungsbereiche.

Cliff: Ich beschäftige mich schon seit geraumer Zeit mit Programmierung und konnte mich in sehr unterschiedlichen Bereichen auszeichnen. Und wenn die Leute sagen: „Oh, du bist der, der JIT für Java gemacht hat!“, ist das immer amüsant. Dabei habe ich zuvor an einem Klon von PostScript gearbeitet – jener Sprache, die Apple einst für seine Laserdrucker verwendet hat. Davor habe ich eine Implementierung der Sprache Forth entwickelt. 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 coolen Programme schreiben. Aber ich habe mich auch mit der Entwicklung von Betriebssystemen, Treibern, Kernel-Debuggern und Programmiersprachen für die Entwicklung von Betriebssystemen befasst, die anfänglich trivial waren, aber im Laufe der Zeit immer komplizierter wurden. Aber das Hauptthema bleibt die Entwicklung von Werkzeugen. Ein großer Teil meines Lebens lag zwischen Azul und Sun, und es ging um Java. Doch 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 hier passiert eine Menge interessanter Dinge und es gibt Menschen, die etwas tun.“ Es ist ein großartiger Weg für die Entwicklung, den es wert ist, beschritten zu werden.

Ja, ich liebe verteilte Berechnungen sehr. Mein erster Job war während des Studiums in C, an einem Werbeprojekt. Es handelte sich um verteilte Berechnungen auf Zilog Z80-Chips, die Daten für eine analoge optische Texterkennung sammelten, die von einem echten analogen Analysator durchgeführt wurde. Das war ein cooles und absolut verrücktes Thema. Aber es gab Probleme, ein Teil wurde nicht richtig erkannt, weshalb das Bild herausgeholt und einer Person gezeigt werden musste, die es bereits mit den Augen gelesen hatte und mitteilte, was dort gesagt wird. Daher gab es Jobs mit Daten, und diese Jobs hatten ihre eigene Sprache. Es gab ein Backend, das all dies bearbeitete – parallel laufende Z80 mit aktiven vt100-Terminals – jeweils eines pro Person, und es gab ein Modell für parallele Programmierung auf Z80. Ein gewisser 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 verwendet wurde. Eine sinnvoll komplexe parallele verteilte Systemarchitektur mit geteilter... teilweise geteilter Speicher. Wann das war... Ich kann mich schon nicht mehr erinnern, irgendwo in den 80ern. Das ist schon eine Weile her. 
Ja, lassen Sie uns sagen, dass 30 Jahre eine ganz schön lange Zeit ist. Die Herausforderungen, die mit verteilten Berechnungen verbunden sind, gibt es schon seit geraumer Zeit, die Menschen kämpfen seit langem mit Beowulf-Clustern. Solche Cluster sehen aus wie… Zum Beispiel: Es gibt Ethernet und dein schneller x86 ist mit diesem Ethernet verbunden, und jetzt möchtest du einen fake shared memory haben, denn damals konnte niemand sich mit dem Codieren von verteilten Berechnungen auseinandersetzen, das war viel zu kompliziert, und deshalb gab es den fake shared memory mit Seitenschutz im Speicher auf x86. Wenn du in diese Seite geschrieben hast, haben wir den anderen Prozessoren gesagt, dass, wenn sie auf denselben shared memory zugreifen wollten, sie das von dir neu laden müssen. So entstand eine Art Protokoll zur Unterstützung der Kohärenz von Caches und die Software dafür. Ein interessantes Konzept. Das eigentliche Problem lag natürlich woanders. Das Ganze funktionierte, aber du hattest schnell Performance-Probleme, denn niemand verstand das Leistungsmodell auf einem ausreichend guten Niveau – welche Zugriffsarten auf den Speicher es gab, wie man verhindern kann, dass die Knoten sich endlos gegenseitig pingen und so weiter.

In H2O habe ich mir folgendes überlegt: Die Entwickler sind selbst dafür verantwortlich, wo Parallelität versteckt ist und wo nicht. Ich habe ein Kodierungsmodell entwickelt, das es einfach und unkompliziert macht, leistungsstarken Code zu schreiben. Langsame Codes zu schreiben, ist dagegen schwierig – sie werden schlecht aussehen. Man muss sich ernsthaft anstrengen, um langsamen Code zu schreiben, man muss unkonventionelle Methoden einsetzen. Bremsender Code ist sofort sichtbar. Folglich wird normalerweise schneller Code geschrieben, aber man muss herausfinden, was zu tun ist, wenn es um gemeinsamen Speicher geht. Das Ganze hängt von großen Arrays ab, und das Verhalten dort ähnelt nicht-volatilen großen Arrays in parallelem Java. Stellen Sie sich vor, dass zwei Threads in ein paralleles Array schreiben, einer von ihnen gewinnt, der andere verliert und man nicht weiß, wer wer ist. Wenn sie nicht volatil sind, kann die Reihenfolge beliebig sein – und das funktioniert wirklich gut. Die Menschen kümmern sich um die Reihenfolge der Operationen, setzen volatile richtig ein und erwarten an den richtigen Stellen Probleme mit der Leistung, die mit dem Speicher zusammenhängen. Andernfalls würden sie einfach Code in Form von Schleifen von 1 bis N schreiben, wobei N einige Billionen sind, in der Hoffnung, dass alle komplexen Fälle automatisch parallel werden – und dort funktioniert es nicht. Aber in H2O ist es weder Java noch Scala, man kann es als „Java minus minus“ betrachten, wenn man möchte. Das ist ein sehr verständlicher Programmierstil und ähnelt dem Schreiben von einfachem Code in C oder Java mit Schleifen und Arrays. Gleichzeitig kann man den Speicher mit Terabytes verarbeiten. Ich verwende immer noch H2O. Gelegentlich setze ich es in verschiedenen Projekten ein – und es ist immer noch das schnellste Tool, das die Konkurrenz um ein Vielfaches übertrifft. Wenn Sie Big Data mit Spalten-Daten machen, ist es sehr schwierig, H2O zu übertreffen.

Technische Herausforderungen

Andrey: Was war die größte Herausforderung in Ihrer gesamten Karriere?

Cliff: Diskutieren wir den technischen oder den nicht-technischen Teil der Frage? Ich würde sagen, die größten Herausforderungen sind nicht technischer Natur. 
Was die technischen Herausforderungen betrifft. Ich habe sie einfach überwunden. Ich weiß nicht einmal, was das größte war, aber es gab einige ziemlich interessante, die viel Zeit und mentale Anstrengung in Anspruch genommen haben. Als ich zu Sun ging, war ich überzeugt, dass ich einen schnellen Compiler entwickeln würde, und eine Menge Seniore sagten mir, dass ich das nie schaffen würde. Aber ich bin diesen Weg gegangen, habe einen Compiler entwickelt, bis hin zum Register-Allocator, und zwar ziemlich schnell. Er war so schnell wie das moderne C1, aber damals war der Allocator viel langsamer, und rückblickend war das ein Problem der großen Datenstruktur. Ich brauchte sie, um einen grafischen Register-Allocator zu schreiben, und ich verstand das Dilemma zwischen Ausdrucksstärke des Codes und Geschwindigkeit, das in dieser Zeit existierte und sehr wichtig war. Es stellte sich heraus, dass die Datenstruktur normalerweise die Cache-Größe der damaligen x86-Architektur überschreitet, und daher, wenn ich ursprünglich annahm, dass der Register-Allocator 5-10 Prozent der gesamten Just-in-time-Kompilierung ausmachen würde, stellte sich in der Realität heraus, dass es 50 Prozent waren.

Die Zeit verging, der Compiler wurde immer klarer und leistungsfähiger, hörte auf, in einer größeren Anzahl von Fällen schlechten Code zu generieren, und die Leistung kam immer näher an das, was ein C-Compiler liefert. Es sei denn, du schreibst natürlich einen solchen Schrott, den auch C nicht beschleunigen kann. Wenn du Code wie in C schreibst, bekommst du in einer größeren Anzahl von Fällen auch die Leistung wie in C. Und je weiter es ging, desto öfter entstand Code, der asymptotisch auf dem Niveau von C lag, und der Register-Allocator begann, etwas Vollendetes zu werden … unabhängig davon, ob dein Code schnell oder langsam lief. Ich arbeitete weiterhin am Allocator, um bessere Zuweisungen zu machen. Er wurde immer langsamer, lieferte aber immer bessere Leistungen in den Fällen, in denen niemand mehr klarkam. Ich konnte in den Register-Allocator eintauchen, dort einen Monat Arbeit investieren, und plötzlich begann der gesamte Code 5% schneller zu laufen. Das geschah immer wieder, und der Register-Allocator wurde so etwas wie ein Kunstwerk – alle mochten oder hassten ihn, und die Leute von der Akademie stellten Fragen wie „Warum wird das alles so gemacht?“ lineares Scannen, und was ist der Unterschied. Die Antwort bleibt die gleiche: der Allokator, der auf Graphfärbung basiert, plus sehr sorgfältige Arbeit mit dem Puffer-Code ergibt ein Werkzeug des Sieges, die beste Kombination, die niemand schlagen kann. Und das ist eine ziemlich unauffällige Sache. Alles andere, was der Compiler dort macht, sind relativ gut erforschte Dinge, die jedoch ebenfalls auf ein Kunstniveau gebracht wurden. Ich habe immer Dinge gemacht, die den Compiler in ein Kunstwerk verwandeln sollten. Aber nichts davon war außergewöhnlich – mit Ausnahme des Register-Allokators. Der Trick ist, dass man sorgfältig abschneiden muss, und wenn das passiert (ich kann das näher erklären, wenn es interessiert), bedeutet das, dass man aggressiver inlinieren kann, ohne das Risiko einzugehen, über den Knick der Leistungskurve hinauszuschießen. Damals gab es viele vollwertige Compiler, die mit Spielereien und Schnickschnack ausgestattet waren und Register-Allokatoren enthielten, aber niemand konnte das so wie ich tun.

Das Problem ist, dass, wenn man Methoden hinzufügt, die inline gehen sollen, und den Bereich des Inlinings immer weiter vergrößert, der Satz der verwendeten Werte die Anzahl der Register sofort übersteigt und man ausgelagert werden muss. Der kritische Punkt tritt normalerweise ein, wenn der Allokator nachgibt, und ein guter Kandidat für das Auslagern steht im Vergleich zu einem anderen, sodass man plötzlich wilde Dinge auslagert. Der Nutzen des Inlinings liegt darin, dass man einen Teil des Overheads, den Overhead für den Aufruf und die Speicherung, verliert; man kann die Werte innerhalb des Codes sehen und man kann sie weiter optimieren. Die Kosten des Inlinings bestehen darin, dass eine große Anzahl lebender Werte entsteht, und wenn dein Register-Allokator mehr auslagert, als nötig ist, verlierst du sofort. Daher haben die meisten Allokatoren das Problem, dass, wenn das Inlining einen bestimmten Punkt überschreitet, alles auf der Welt ausgelagert wird und die Leistung in den Mülleimer gespült werden kann. Die Compilerentwickler fügen einige Heuristiken hinzu: zum Beispiel das Inlining ab einer bestimmten, ausreichend großen Größe zu stoppen, da die Allokationen alles verderben. So entsteht ein Bruch im Leistungsdiagramm – du inline, inline, die Leistung steigt allmählich – und dann bam! – fällt sie plötzlich steil ab, weil du zu viel inlined. So hat es bis zur Einführung von Java funktioniert. Java erfordert viel mehr Inlining, deshalb musste ich meinen Allokator viel aggressiver gestalten, damit er sich stabilisiert und nicht abstürzt; und wenn du zu viel inlined, beginnt er auszulagern, aber irgendwann kommt dennoch der Moment, an dem es heißt: „Kein Auslagern mehr“. Das ist eine interessante Beobachtung und sie ist mir irgendwie auf die Idee gekommen; nicht offensichtlich, aber gut rentabel. Ich habe mich auf aggressives Inlining eingelassen und das hat mich an Stellen geführt, wo die Leistung von Java und C gleichauf ist. Sie sind tatsächlich sehr nah beieinander – ich kann Java-Code schreiben, der deutlich schneller ist als C-Code und so weiter, aber insgesamt, im großen Bild der Dinge, sind sie ungefähr vergleichbar. Ich denke, ein Teil des Verdienstes liegt beim Register-Allokator, der es mir erlaubt, so dumm wie möglich zu inlinen. Ich inline einfach alles, was ich sehe. Die Frage ist, ob der Allokator gut funktioniert und ob der resultierende Code sinnvoll arbeitet. Das war eine große Herausforderung: das alles zu verstehen und zum Laufen zu bringen.

Ein bisschen über Registerzuweisung und Multicore-Verarbeitung

Wladimir: Probleme wie die Registerallokation scheinen ein ewiges Thema zu sein. Interessiert es, ob irgendeine Idee vielversprechend war und dann in der Praxis gescheitert ist?

Cliff: Natürlich! Die Registerallokation ist ein Bereich, in dem du versuchst, einige Heuristiken zu finden, um ein NP-vollständiges Problem zu lösen. Und du wirst niemals in der Lage sein, eine perfekte Lösung zu erreichen, richtig? Das ist einfach unmöglich. Schau mal, die Ahead of Time-Kompilierung funktioniert auch schlecht. Es geht hier um Durchschnittsfälle. Um typisches Leistungsvermögen, so dass du etwas messen kannst, das du für eine gute typische Leistung hältst – schließlich arbeitest du ja daran, sie zu verbessern! Die Registerallokation ist ein Thema, das ganz der Leistung gewidmet ist. Sobald du den ersten Prototyp hast, der funktioniert und das Notwendige macht, kommt die Arbeit an der Performance. Du musst lernen, gut zu messen. Warum ist das wichtig? Wenn es klare Daten gibt, kannst du dir verschiedene Bereiche ansehen und sehen: Aha, das hat hier geholfen, aber dort ist alles kaputtgegangen! 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 vom vorherigen Allokator unterschieden haben. Und jedes Mal sieht es так aus: Irgendwo gewonnen, irgendwo verloren. Wenn du gute Performance-Analysetools hast, kannst du die verlierenden Ideen finden und verstehen, warum sie verlieren. Vielleicht sollte man alles so lassen, wie es ist, oder vielleicht ernsthaft in die Feinabstimmung investieren, oder vielleicht etwas anderes reparieren. Das ist ein ganzes Set von Dingen! Ich habe diesen coolen Hack gemacht, aber ich brauche auch diesen, und diesen, und diesen – und die kombinierte Summe bringt einige Verbesserungen. Einzelaktionen können scheitern. So ist die Natur der Arbeit an der Leistung NP-vollständiger Probleme.

Wladimir: Es entsteht der Eindruck, dass Dinge wie das Färben in Allokatoren bereits gelöst sind. Nun, für euch gelöst, judging by what you say, should it be…

Cliff: Sie ist nicht als solche gelöst. Du musst sie in eine „gelöste“ verwandeln. Es gibt schwierige Aufgaben, die gelöst werden müssen. Wenn dies erledigt ist, kommt die Zeit, sich mit der Leistung zu befassen. Du solltest diese Arbeit entsprechend angehen – Benchmarks durchführen, Metriken sammeln, Situationen erklären, in denen dein alter Hack wieder funktioniert hat (oder umgekehrt, aufgehört hat zu funktionieren), wenn du zu einer früheren Version zurückgehst. Und nicht aufgeben, bis du etwas erreicht hast. Wie ich bereits gesagt habe, wenn es coole Ideen gibt, die nicht funktioniert haben, gibt es im Bereich der Registerallokierung praktisch unendlich viele Ideen. Man könnte zum Beispiel wissenschaftliche Veröffentlichungen lesen. Obwohl dieses Gebiet sich jetzt langsamer entwickelt und klarer ist als in seiner Jugend. Dennoch arbeiten in diesem Bereich unzählige Menschen, und all ihre Ideen sollten ausprobiert werden, sie warten alle auf ihre Chance. Und du kannst nicht sagen, wie gut sie sind, wenn du es nicht ausprobierst. Wie gut sie sich mit allem anderen in deinem Allocator integrieren, denn der Allocator macht viele Dinge, und einige Ideen funktionieren in deinem speziellen Allocator nicht, während sie in einem anderen Allocator problemlos funktionieren können. Der Hauptweg zum Sieg für den Allocator besteht darin, langsame Dinge vom Hauptweg zu entfernen und gezwungenen Split nach den Grenzen langsamer Wege zu machen. Deshalb, wenn du GC starten willst, den langsamen Weg gehen, deoptimieren, eine Ausnahme werfen, all solche Sachen – du weißt, dass diese Dinge relativ selten sind. Und sie sind wirklich selten, das habe ich überprüft. Du machst 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 genutzt. Zum Beispiel ein Nullzeiger – er tritt nie auf, oder? Man muss mehrere Wege für verschiedene Dinge haben, aber sie sollten sich nicht im Hauptweg stören. 

Wladimir: Was denkst du über Mehrkernigkeit, wenn es Tausende von Kernen gibt? Ist das eine nützliche Sache?

Cliff: Der Erfolg von GPUs zeigt, dass es ziemlich nützlich ist!

Wladimir: Sie sind ziemlich spezialisiert. Und wie wäre es mit Allzweckprozessoren?

Cliff: Nun, das war das Geschäftsmodell von Azul. Die Antwort kam noch aus einer Ära, in der die Menschen sehr an vorhersehbarer Leistung interessiert waren. Damals war es schwierig, parallelen Code zu schreiben. Das H2O-Codierungsmodell lässt sich gut skalieren, ist aber kein universelles Modell. Es ist nur geringfügig allgemeiner als die Verwendung von GPU. Sprechen wir über die Komplexität der Entwicklung einer solchen Sache oder die Komplexität ihrer Nutzung? Zum Beispiel hat mir Azul eine interessante Lektion erteilt, die nicht ganz offensichtlich ist: kleine Caches sind in Ordnung. 

Die größte Herausforderung im Leben

Wladimir: Was ist mit den nicht-technischen Herausforderungen?

Cliff: Die größte Herausforderung war es, nicht… freundlich und nett zu den Menschen zu sein. Folglich fand ich mich ständig in hochgradig konfliktbeladenen Situationen wieder. In solchen, in denen ich wusste, dass alles schiefgeht, aber nicht wusste, wie ich mit den Problemen vorankommen sollte und es nicht schaffte, sie zu bewältigen. Zahlreiche langanhaltende Probleme, die Jahrzehnte dauerten, entstanden genau so. Dass es in Java die Compiler C1 und C2 gibt, ist die direkte Folge davon. Dass es in Java über ein ganzes Jahrzehnt hinweg keine mehrstufige Kompilierung gab – das ist ebenfalls eine direkte Folge. Offensichtlich brauchten wir ein solches System, aber es ist nicht klar, warum es das nicht gab. Ich hatte Probleme mit einem Ingenieur… oder einer Gruppe von Ingenieuren. Vor langer Zeit, als ich anfing, bei Sun zu arbeiten, war ich… Gut, nicht nur damals, ich habe im Allgemeinen immer meine eigene Meinung zu allem. Und ich hielt es für eine Wahrheit, dass ich einfach meine Sichtweise direkt äußern kann. Zumal ich die meiste Zeit erschreckend recht hatte. Und wenn dir dieser Ansatz nicht gefällt… besonders wenn du offensichtlich im Unrecht bist und Unsinn redest… Nun, nur wenige Menschen konnten eine solche Kommunikationsform langmütig ertragen. Obwohl einige es konnten, zum Beispiel ich. Ich habe mein ganzes Leben auf meritokratischen Prinzipien aufgebaut. Wenn du mir etwas Falsches zeigst, drehe ich mich um und sage: Du hast Unsinn gesagt. Dabei entschuldige ich mich natürlich und all das, erkenne Verdienste an, falls vorhanden, und handle entsprechend. Auf der anderen Seite habe ich erschreckend oft recht, erschreckend viel Zeit. Und das funktioniert in zwischenmenschlichen Beziehungen nicht wirklich gut. Ich strebe nicht danach, nett zu sein, sondern stelle die Dinge klar. „Das wird nie funktionieren, weil eins, zwei und drei“. Und sie antworten: „Oh!“. Es gab auch andere Konsequenzen, die ich besser auslassen sollte: zum Beispiel die, die zu meiner Scheidung von meiner Frau führten und zu einem Jahrzehnt der Depression danach.

Der Challenge ist ein Kampf mit den Menschen und ihrer Wahrnehmung dessen, was du tun kannst oder nicht, was wichtig ist und was nicht. Es gab viele Challenges zum Thema Programmierstil. Ich schreibe immer noch viel Code, aber damals musste ich sogar langsamer werden, weil ich zu viele Aufgaben parallel erledigte und sie schlecht machte, anstatt mich auf eine einzige zu konzentrieren. Wenn ich zurückblicke, habe ich die Hälfte des Codes für das Java JIT Team, das C2 Team, geschrieben. Der nächste schnellste Programmierer schrieb halb so schnell, der darauf folgende noch einmal halb so schnell und das war ein exponentieller Rückgang. Die siebte Person in dieser Reihe war sehr, sehr langsam – das passiert immer! Ich habe viel Code angesehen. Ich habe beobachtet, wer was schreibt, ohne Ausnahmen, ich habe mir ihren Code angeschaut, jede/r von ihnen wurde überprüft und trotzdem habe ich immer noch mehr geschrieben als jeder von ihnen. Mit Menschen funktioniert dieser Ansatz nicht gut. Einige mögen sowas nicht. Und wenn sie nicht damit umgehen können, fangen allerlei Beschwerden an. Zum Beispiel sagte man mir einmal, ich solle aufhören, Code zu schreiben, weil ich zu viel Code schreibe und das das Team gefährdet, und das klang für mich wie ein Scherz: Alter, wenn das gesamte Team verschwindet und ich weiterhin Code schreibe, verlierst du nur die Hälfte des Teams. Auf der anderen Seite, wenn ich weiterhin Code schreibe und du verlierst die Hälfte des Teams – das klingt nach sehr schlechtem Management. Ich habe nie besonders darüber nachgedacht, nie darüber gesprochen, aber es war trotzdem irgendwo in meinem Kopf. In den Hintergedanken schwirrte der Gedanke: „Macht ihr ernsthaft Scherze?“ Also war ich das größte Problem und meine Beziehung zu den Menschen. Jetzt verstehe ich mich viel besser, ich habe lange als Teamleiter bei Programmierern gearbeitet, und jetzt sage ich den Leuten direkt: Weißt du, ich bin, wie ich bin, und ihr müsst damit umgehen – ist es in Ordnung, wenn ich hier stehe? Und als sie damit umzugehen begannen, hat alles funktioniert. Denn ich bin eigentlich weder schlecht noch gut, ich habe keine schlechten Absichten oder egoistischen Bestrebungen, das ist einfach mein Wesen, und damit muss man irgendwie leben.

Andrey: Vor kurzem haben alle über das Selbstbewusstsein für Introvertierte und generell über soziale Fähigkeiten gesprochen. Was kann man dazu sagen?

Cliff: Ja, das war das Verständnis und die Lektion, die ich aus der Scheidung von meiner Frau gezogen habe. Was ich aus der Scheidung gelernt habe, ist, mich selbst zu verstehen. So begann ich, andere Menschen zu verstehen. Zu verstehen, wie diese Interaktion funktioniert. Das führte zu einer Entdeckung nach der anderen. Es entstand das Bewusstsein, wer ich bin und was ich darstelle. Was ich tue: Entweder bin ich mit einer Aufgabe beschäftigt, oder ich vermeide Konflikte, oder ich mache etwas anderes – und dieses Niveau des Selbstbewusstseins hilft wirklich, sich im Griff zu halten. Danach läuft alles viel einfacher. Eine Sache, die ich nicht nur bei mir, sondern auch bei anderen Programmierern festgestellt habe, ist die Unfähigkeit, Gedanken zu verbalisieren, wenn man in einem emotionalen Stresszustand ist. Zum Beispiel, du sitzt dort und programmierst, bist im Fluss, und plötzlich kommen sie zu dir gerannt und schreien hysterisch, dass etwas kaputt ist und jetzt drastische Maßnahmen gegen dich ergriffen werden. Und du kannst kein Wort sagen, weil du in einem emotionalen Stresszustand bist. Das erlernte Wissen ermöglicht es, sich auf diesen Moment vorzubereiten, ihn zu überstehen und zu einem Rückzugsplan überzugehen, nach dem man bereits etwas tun kann. Also ja, wenn du anfängst zu realisieren, wie das alles funktioniert – das ist ein großes, lebensveränderndes Ereignis. 
Ich selbst konnte die richtigen Worte nicht finden, aber ich erinnerte mich an die Abfolge der Handlungen. Die Essenz ist, dass diese Reaktion ebenso physisch wie verbal ist, und du brauchst Raum. Solchen Raum im zenbuddhistischen Sinn. Genau das muss erklärt werden, und dann musst du dich sofort physisch zurückziehen. Wenn ich innerlich schweige, kann ich die Situation in Bezug auf Emotionen verarbeiten. Während das Adrenalin das Gehirn erreicht, dich in den „Kampf oder Flucht“-Modus schaltet, kannst du nichts mehr sagen, nein – jetzt bist du ein Idiot, ein Prügel, unfähig, würdig zu antworten oder wenigstens den Angriff zu stoppen, und der Angreifer kann frei immer wieder angreifen. Zuerst musst du wieder du selbst werden, die Kontrolle zurückgewinnen und aus dem „Kampf oder Flucht“-Modus herauskommen.

Und dafür benötigt man ein verbales Raum. Einfach freien Raum. Wenn man überhaupt etwas sagen möchte, kann man genau das festhalten und dann wirklich „Raum“ für sich finden: im Park spazieren gehen, sich in die Dusche sperren - egal. Das Wichtigste ist, sich vorübergehend von der Situation zu trennen. Sobald du sogar für ein paar Sekunden abschaltest, kommt die Kontrolle zurück und du beginnst klar zu denken. "Gut, ich bin doch kein Idiot oder so, ich mache keine dummen Dinge, ich bin ein ziemlich nützlicher Mensch." Sobald du dich selbst überzeugen konntest, ist es an der Zeit, zum nächsten Schritt überzugehen: zu verstehen, was geschehen ist. Du wurdest angegriffen, der Angriff kam von unerwarteter Seite, es war eine unfaire, hinterhältige Falle. Das ist schlecht. Der nächste Schritt ist zu verstehen, warum der Angreifer das nötig hatte. Warum eigentlich? 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 sollte man die gesamte Situation behutsam verarbeiten. Aber dafür braucht man Spielraum, verbalen Raum. Der erste Schritt besteht darin, den verbalen Kontakt zu kappen. Weg von verbalen Diskussionen. Es abzubrechen, so schnell wie möglich wegzugehen. Wenn es ein Telefonat ist – 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 lege 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 deine Fähigkeit, klar zu denken, zurückkommt, bist du etwas abgekühlt und es wird möglich, zu überlegen, was überhaupt passiert ist und was als Nächstes kommt. Und beginne, eine durchdachte Antwort zu formulieren, anstatt einfach emotional zu reagieren. Für mich war der Durchbruch im Selbstbewusstsein genau das, dass ich im Falle emotionalen Stresses nicht sprechen kann. Aus diesem Zustand herauszukommen, zu überlegen und zu planen, wie man antworten und Probleme ausgleichen kann – das sind die richtigen Schritte, wenn man nicht sprechen kann. Der einfachste Weg ist, aus der Situation zu entkommen, in der emotionaler Stress auftritt, und einfach nicht mehr daran teilzunehmen. Danach gewinnst du die Fähigkeit zu denken, wenn du denken kannst, entsteht die Möglichkeit zu sprechen und so weiter.

Übrigens versucht der Anwalt der Gegenseite im Gericht, dies mit dir zu tun – jetzt ist klar, warum. Weil er die Möglichkeit hat, dich in einen Zustand zu versetzen, in dem du sogar deinen eigenen Namen nicht aussprechen kannst, zum Beispiel. Im wahrsten Sinne des Wortes, du kannst nicht sprechen. Wenn dir das widerfährt und wenn du weißt, dass du an einem Ort sein wirst, wo es verbale Kämpfe gibt, an einem Ort wie dem Gericht, dann kannst du mit deinem Anwalt kommen. Der Anwalt wird dich verteidigen und den verbalen Angriff stoppen, und das auf ganz legale Weise, und du wirst deinen verlorenen Zen-Raum zurückbekommen. Zum Beispiel musste ich ein paar Mal meine Familie anrufen, der Richter war damit ganz freundlich, aber der Anwalt der Gegenseite schrie und schrie mich an, ich konnte kein Wort einfügen. In solchen Fällen funktioniert für mich am besten der Einsatz eines Vermittlers. Der Vermittler beendet all den Druck, der ununterbrochen auf dich einströmt, du entdeckst den notwendigen Zen-Raum, mit ihm kehrt die Fähigkeit zum Sprechen zurück. Es ist ein ganzes Wissensgebiet, in dem man viel lernen und vieles in sich selbst entdecken muss, und all dies verwandelt sich in hochgradige strategische Entscheidungen, die für verschiedene Menschen unterschiedlich sind. Einige haben nicht die oben beschriebenen Probleme, normalerweise haben Menschen, die beruflich im Verkauf tätig sind, keine. All diese Menschen, die mit Worten ihren Lebensunterhalt verdienen – bekannte Sänger, Dichter, religiöse Führer und Politiker, sie haben immer etwas zu sagen. Sie haben solche Probleme nicht, ich jedoch schon.

Andrey: Das war… unerwartet. Gut, wir haben schon ziemlich viel gesprochen und es ist Zeit, dieses Interview zu beenden. Wir werden uns auf der Konferenz treffen und diesen Dialog fortsetzen können. Wir sehen uns auf der Hydra!

Die Kommunikation mit Cliff kann auf der Konferenz Hydra 2019 fortgesetzt werden, die am 11. und 12. Juli 2019 in St. Petersburg stattfindet. Er wird mit einem Vortrag kommen „The Azul Hardware Transactional Memory experience“. Tickets können erworben werden auf der offiziellen Webseite.

Quelle: habr.com

60GB SSD 8Gb DDR4