Tolles Interview mit Cliff Click, dem Vater der JIT-Kompilierung in Java

Tolles Interview mit Cliff Click, dem Vater der JIT-Kompilierung in JavaCliff Click — CTO von Cratus (IoT-Sensoren zur Prozessverbesserung), Gründer und Mitbegründer mehrerer Startups (darunter Rocket Realtime School, Neurensic und H2O.ai) mit mehreren erfolgreichen Exits. Cliff schrieb seinen ersten Compiler im Alter von 15 Jahren (Pascal für den TRS Z-80)! Er ist vor allem für seine Arbeit zu C2 in Java (dem Sea of ​​​​Nodes IR) bekannt. Dieser Compiler zeigte der Welt, dass JIT qualitativ hochwertigen Code erzeugen kann, was einer der Faktoren für die Entstehung von Java als eine der wichtigsten modernen Softwareplattformen war. Dann half Cliff Azul Systems beim Aufbau eines 864-Core-Mainframes mit reiner Java-Software, der GC-Pausen auf einem 500-Gigabyte-Heap innerhalb von 10 Millisekunden unterstützte. Im Allgemeinen gelang es Cliff, an allen Aspekten der JVM zu arbeiten.

 
Dieser Habrapost ist ein großartiges Interview mit Cliff. Wir werden über folgende Themen sprechen:

  • Übergang zu Low-Level-Optimierungen
  • So führen Sie ein großes Refactoring durch
  • Kostenmodell
  • Low-Level-Optimierungstraining
  • Praxisbeispiele zur Leistungssteigerung
  • Warum eine eigene Programmiersprache erstellen?
  • Karriere als Leistungsingenieur
  • Technische Herausforderungen
  • Ein wenig über Registerzuteilung und Multicores
  • Die größte Herausforderung im Leben

Das Interview wird geführt von:

  • Andrej Satarin von Amazon Web Services. Im Laufe seiner Karriere gelang es ihm, in völlig unterschiedlichen Projekten zu arbeiten: Er testete die verteilte NewSQL-Datenbank in Yandex, ein Cloud-Erkennungssystem in Kaspersky Lab, ein Multiplayer-Spiel in Mail.ru und einen Dienst zur Berechnung von Devisenpreisen in der Deutschen Bank. Interessiert am Testen großer Backend- und verteilter Systeme.
  • Wladimir Sitnikow von Netcracker. Zehn Jahre Arbeit an der Leistung und Skalierbarkeit von NetCracker OS, einer Software, die von Telekommunikationsbetreibern zur Automatisierung von Netzwerk- und Netzwerkgeräteverwaltungsprozessen verwendet wird. Interessiert an Leistungsproblemen bei Java- und Oracle-Datenbanken. Autor von mehr als einem Dutzend Leistungsverbesserungen im offiziellen PostgreSQL-JDBC-Treiber.

Übergang zu Low-Level-Optimierungen

Andrew: Sie sind ein großer Name in der Welt der JIT-Kompilierung, Java und Performance-Arbeit im Allgemeinen, oder? 

Cliff: Es ist wie es ist!

Andrew: Beginnen wir mit einigen allgemeinen Fragen zur Performance-Arbeit. Was halten Sie von der Wahl zwischen High-Level- und Low-Level-Optimierungen wie der Arbeit auf CPU-Ebene?

Cliff: Ja, hier ist alles einfach. Der schnellste Code ist der, der nie ausgeführt wird. Daher müssen Sie immer auf einem hohen Niveau beginnen und an Algorithmen arbeiten. Eine bessere O-Notation wird eine schlechtere O-Notation schlagen, sofern nicht einige ausreichend große Konstanten eingreifen. Low-Level-Dinge kommen zuletzt. Wenn Sie den Rest Ihres Stacks gut genug optimiert haben und noch einige interessante Dinge übrig sind, ist das normalerweise ein niedriges Niveau. Aber wie fängt man von einem hohen Niveau an? Woher wissen Sie, dass genügend hochrangige Arbeit geleistet wurde? Nun... auf keinen Fall. Es gibt keine fertigen Rezepte. Sie müssen das Problem verstehen, entscheiden, was Sie tun werden (um in Zukunft keine unnötigen Schritte zu unternehmen) und dann können Sie den Profiler aufdecken, der etwas Nützliches sagen kann. Irgendwann merkt man selbst, dass man unnötige Dinge losgeworden ist und es an der Zeit ist, ein wenig Feinabstimmung vorzunehmen. Das ist definitiv eine besondere Art von Kunst. Es gibt viele Menschen, die unnötige Dinge tun, dabei aber so schnell vorgehen, dass sie keine Zeit haben, sich um die Produktivität zu kümmern. Aber das ist so lange, bis die Frage unverblümt auftaucht. Normalerweise kümmert es in 99 % der Fälle niemanden, was ich tue, bis zu dem Moment, in dem eine wichtige Sache auf dem kritischen Pfad auftaucht, die niemanden interessiert. Und hier nörgeln alle, „warum es nicht von Anfang an perfekt geklappt hat.“ Im Allgemeinen gibt es immer etwas, was die Leistung verbessern kann. Aber in 99 % der Fälle haben Sie keine Leads! Sie versuchen einfach, etwas zum Laufen zu bringen und finden dabei heraus, worauf es ankommt. Man kann nie im Voraus wissen, dass dieses Stück perfekt sein muss, also muss man tatsächlich in allem perfekt sein. Aber das ist unmöglich und Sie tun es nicht. Es gibt immer viel zu reparieren – und das ist völlig normal.

So führen Sie ein großes Refactoring durch

Andrew: Wie arbeitet man an einer Aufführung? Hierbei handelt es sich um ein Querschnittsproblem. Mussten Sie beispielsweise jemals an Problemen arbeiten, die sich aus der Überschneidung vieler vorhandener Funktionen ergeben?

Cliff: Ich versuche es zu vermeiden. Wenn ich weiß, dass die Leistung ein Problem sein wird, denke ich darüber nach, bevor ich mit dem Codieren beginne, insbesondere bei Datenstrukturen. Aber oft entdeckt man das alles erst sehr spät. Und dann müssen Sie extreme Maßnahmen ergreifen und das tun, was ich „Umschreiben und Erobern“ nenne: Sie müssen sich ein ausreichend großes Stück schnappen. Ein Teil des Codes muss aufgrund von Leistungsproblemen oder aus anderen Gründen noch neu geschrieben werden. Was auch immer der Grund für das Umschreiben von Code sein mag, es ist fast immer besser, einen größeren Teil als einen kleineren neu zu schreiben. In diesem Moment zittern alle vor Angst: „Oh mein Gott, so viel Code darfst du doch nicht anfassen!“ Tatsächlich funktioniert dieser Ansatz jedoch fast immer viel besser. Sie müssen sich sofort einem großen Problem stellen, einen großen Kreis darum ziehen und sagen: Ich werde alles innerhalb des Kreises neu schreiben. Der Rand ist viel kleiner als der darin enthaltene Inhalt, der ersetzt werden muss. Und wenn eine solche Abgrenzung es Ihnen ermöglicht, die Arbeit im Inneren perfekt zu erledigen, haben Sie die Hände frei und können tun, was Sie wollen. Sobald Sie das Problem verstanden haben, ist das Umschreiben viel einfacher, also nehmen Sie einen großen Bissen!
Wenn Sie gleichzeitig eine umfassende Neufassung durchführen und feststellen, dass die Leistung ein Problem sein wird, können Sie sich sofort darüber Gedanken machen. Dies führt normalerweise zu einfachen Dingen wie „Keine Daten kopieren, Daten so einfach wie möglich verwalten, klein machen.“ Bei großen Umschreibungen gibt es Standardmethoden zur Leistungsverbesserung. Und sie drehen sich fast immer um Daten.

Kostenmodell

Andrew: In einem der Podcasts haben Sie über Kostenmodelle im Kontext der Produktivität gesprochen. Können Sie erklären, was Sie damit gemeint haben?

Cliff: Sicherlich. Ich wurde in einer Zeit geboren, in der die Prozessorleistung äußerst wichtig war. Und diese Ära kehrt wieder zurück – das Schicksal ist nicht ohne Ironie. Ich begann in der Zeit der 256-Bit-Maschinen zu leben; mein erster Computer arbeitete mit XNUMX Bytes. Genau Bytes. Alles war sehr klein. Anweisungen mussten gezählt werden, und als wir begannen, den Programmiersprachenstapel nach oben zu verschieben, nahmen die Sprachen immer mehr zu. Es gab Assembler, dann Basic, dann C, und C kümmerte sich um viele Details, wie Registerzuweisung und Befehlsauswahl. Aber dort war alles ganz klar, und wenn ich einen Zeiger auf eine Instanz einer Variablen machen würde, würde ich laden, und die Kosten dieser Anweisung sind bekannt. Die Hardware erzeugt eine bestimmte Anzahl von Maschinenzyklen, sodass die Ausführungsgeschwindigkeit verschiedener Dinge einfach durch Addition aller auszuführenden Anweisungen berechnet werden kann. Jeder Vergleich/Test/Zweig/Aufruf/Laden/Speichern könnte addiert werden und lauten: Das ist die Ausführungszeit für Sie. Wenn Sie an der Verbesserung der Leistung arbeiten, werden Sie auf jeden Fall darauf achten, welche Zahlen kleinen Heißzyklen entsprechen. 
Aber sobald man auf Java, Python und ähnliches umsteigt, entfernt man sich sehr schnell von Low-Level-Hardware. Was kostet der Aufruf eines Getters in Java? Wenn JIT in HotSpot korrekt ist inline, wird es geladen, aber wenn dies nicht der Fall ist, handelt es sich um einen Funktionsaufruf. Da sich der Aufruf in einer Hot-Loop befindet, überschreibt er alle anderen Optimierungen in dieser Schleife. Daher werden die tatsächlichen Kosten viel höher sein. Und Sie verlieren sofort die Fähigkeit, einen Codeabschnitt anzusehen und zu verstehen, dass wir ihn im Hinblick auf die Prozessortaktrate, den verwendeten Speicher und den verwendeten Cache ausführen sollten. Interessant wird das alles erst, wenn man sich wirklich auf die Aufführung einlässt.
Jetzt befinden wir uns in einer Situation, in der die Prozessorgeschwindigkeiten seit einem Jahrzehnt kaum gestiegen sind. Die alten Zeiten sind zurück! Sie können sich nicht mehr auf eine gute Single-Threaded-Leistung verlassen. Aber wenn man plötzlich ins Parallelrechnen einsteigt, ist das unheimlich schwer, alle sehen einen wie James Bond. Zehnfache Beschleunigungen treten hier meist an Stellen auf, an denen jemand etwas vermasselt hat. Parallelität erfordert viel Arbeit. Um diese XNUMX-fache Beschleunigung zu erreichen, müssen Sie das Kostenmodell verstehen. Was und wie viel kostet es? Und dazu müssen Sie verstehen, wie die Zunge auf die darunter liegende Hardware passt.
Martin Thompson hat für seinen Blog ein tolles Wort gewählt Mechanische Sympathie! Sie müssen verstehen, was die Hardware tun wird, wie genau sie es tun wird und warum sie überhaupt das tut, was sie tut. Auf diese Weise ist es ziemlich einfach, mit dem Zählen von Anweisungen zu beginnen und herauszufinden, wie lange die Ausführung dauert. Wer nicht über die entsprechende Ausbildung verfügt, sucht einfach in einem dunklen Raum nach einer schwarzen Katze. Ich sehe ständig Leute, die ihre Leistung optimieren und keine Ahnung haben, was zum Teufel sie tun. Sie leiden sehr und machen keine großen Fortschritte. Und wenn ich denselben Code nehme, ein paar kleine Hacks einfüge und eine fünf- oder zehnfache Beschleunigung erhalte, sagen sie: Nun, das ist nicht fair, wir wussten bereits, dass Sie besser sind. Toll. Worüber rede ich? Beim Kostenmodell geht es darum, welche Art von Code Sie schreiben und wie schnell er im Großen und Ganzen durchschnittlich läuft.

Andrew: Und wie kann man so eine Lautstärke im Kopf behalten? Geht das mit mehr Erfahrung, oder? Woher kommt diese Erfahrung?

Cliff: Nun, ich habe meine Erfahrung nicht auf die einfachste Art und Weise gemacht. Ich habe damals in Assembly programmiert, als man noch jede einzelne Anweisung verstehen konnte. Es klingt dumm, aber seitdem ist der Z80-Befehlssatz immer in meinem Kopf, in meiner Erinnerung geblieben. Ich erinnere mich nicht innerhalb einer Minute nach dem Gespräch an die Namen der Leute, aber ich erinnere mich an Code, der vor 40 Jahren geschrieben wurde. Es ist lustig, es sieht aus wie ein Syndrom.Idiotischer Wissenschaftler".

Low-Level-Optimierungstraining

Andrew: Gibt es einen einfacheren Weg hineinzukommen?

Cliff: Ja und nein. Die Hardware, die wir alle verwenden, hat sich im Laufe der Zeit nicht allzu sehr verändert. Jeder verwendet x86, mit Ausnahme von Arm-Smartphones. Wenn Sie keine Hardcore-Einbettung durchführen, tun Sie dasselbe. Okay, als nächstes. Auch die Anweisungen haben sich seit Jahrhunderten nicht verändert. Sie müssen etwas in Assembly schreiben. Nicht viel, aber genug, um es zu verstehen. Du lächelst, aber ich spreche ganz ernst. Sie müssen den Zusammenhang zwischen Sprache und Hardware verstehen. Danach müssen Sie ein wenig schreiben und einen kleinen Spielzeug-Compiler für eine kleine Spielzeugsprache erstellen. Spielzeugartig bedeutet, dass es in angemessener Zeit hergestellt werden muss. Es kann supereinfach sein, aber es muss Anweisungen generieren. Das Generieren einer Anweisung hilft Ihnen, das Kostenmodell für die Brücke zwischen dem High-Level-Code, den jeder schreibt, und dem Maschinencode, der auf der Hardware ausgeführt wird, zu verstehen. Diese Korrespondenz wird zum Zeitpunkt des Schreibens des Compilers ins Gehirn eingebrannt. Sogar der einfachste Compiler. Danach können Sie sich mit Java und der Tatsache befassen, dass seine semantische Kluft viel tiefer ist und 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 geworden ist, was dazu führt, dass sie auseinanderfällt und was nicht. Aber Sie brauchen eine Art Ausgangspunkt, an dem Sie sich den Code ansehen und verstehen: „Ja, dieser Getter sollte jedes Mal inline sein.“ Und dann stellt sich heraus, dass dies manchmal passiert, außer in der Situation, in der die Methode zu groß wird und die JIT anfängt, alles zu inlinen. Die Leistung solcher Orte kann sofort vorhergesagt werden. Normalerweise funktionieren Getter gut, aber wenn man sich dann große Hot-Loops ansieht, stellt man fest, dass dort einige Funktionsaufrufe im Umlauf sind, die nicht wissen, was sie tun. Dies ist das Problem bei der weit verbreiteten Verwendung von Gettern. Der Grund, warum sie nicht inline sind, liegt darin, dass nicht klar ist, ob es sich um Getter handelt. Wenn Sie eine sehr kleine Codebasis haben, können Sie sich diese einfach merken und dann sagen: Das ist ein Getter und das ist ein Setter. In einer großen Codebasis lebt jede Funktion ihre eigene Geschichte, die im Allgemeinen niemandem bekannt ist. Der Profiler sagt, dass wir bei einer Schleife 24 % der Zeit verloren haben und um zu verstehen, was diese Schleife tut, müssen wir uns jede einzelne Funktion darin ansehen. Es ist unmöglich, dies zu verstehen, ohne die Funktion zu studieren, und dies verlangsamt den Prozess des Verstehens erheblich. Deshalb verwende ich keine Getter und Setter, ich habe ein neues Level erreicht!
Wo bekomme ich das Kostenmodell? Nun, man kann natürlich etwas lesen ... Aber ich denke, der beste Weg ist, zu handeln. Die Erstellung eines kleinen Compilers ist der beste Weg, das Kostenmodell zu verstehen und in Ihren eigenen Kopf zu integrieren. Ein kleiner Compiler, der sich zum Programmieren einer Mikrowelle eignet, ist eine Aufgabe für Anfänger. Nun ja, ich meine, wenn Sie bereits über Programmierkenntnisse verfügen, sollte das ausreichen. All diese Dinge wie das Parsen einer Zeichenfolge, die Sie als eine Art algebraischen Ausdruck haben, das Extrahieren von Anweisungen für mathematische Operationen in der richtigen Reihenfolge von dort, das Entnehmen der richtigen Werte aus Registern – all dies wird auf einmal erledigt. Und während Sie es tun, wird es sich in Ihr Gehirn einprägen. Ich denke, jeder weiß, was ein Compiler tut. Und dies wird ein Verständnis für das Kostenmodell vermitteln.

Praxisbeispiele zur Leistungssteigerung

Andrew: Worauf sollten Sie sonst noch achten, wenn Sie an der Produktivität arbeiten?

Cliff: Datenstrukturen. Übrigens, ja, ich habe diese Kurse schon lange nicht mehr gegeben ... Raketenschule. Es hat Spaß gemacht, war aber mit viel Aufwand verbunden und ich habe auch ein Leben! OK. In einem der großen und interessanten Kurse „Wohin geht deine Leistung?“ habe ich den Schülern ein Beispiel gegeben: Zweieinhalb Gigabyte Fintech-Daten wurden aus einer CSV-Datei gelesen und sie mussten dann die Anzahl der verkauften Produkte berechnen . Regelmäßige Tick-Marktdaten. Seit den 70er Jahren werden UDP-Pakete in das Textformat umgewandelt. Chicago Mercantile Exchange – alle möglichen Dinge wie Butter, Mais, Sojabohnen und ähnliches. Es war notwendig, diese Produkte, die Anzahl der Transaktionen, das durchschnittliche Volumen der Geld- und Warenbewegungen usw. zu zählen. Es ist eine ziemlich einfache Handelsmathematik: Finden Sie den Produktcode (das sind 1-2 Zeichen in der Hash-Tabelle), ermitteln Sie den Betrag, fügen Sie ihn zu einem der Handelssätze hinzu, erhöhen Sie das Volumen, erhöhen Sie den Wert und ein paar andere Dinge. Sehr einfache Mathematik. Die Implementierung des Spielzeugs war sehr einfach: Alles ist in einer Datei, ich lese die Datei und gehe durch sie, teile einzelne Datensätze in Java-Strings auf, suche darin nach den notwendigen Dingen und addiere sie gemäß der oben beschriebenen Mathematik. Und es funktioniert bei etwas niedriger Geschwindigkeit.

Bei diesem Ansatz ist es offensichtlich, was los ist, und paralleles Rechnen wird nicht helfen, oder? Es zeigt sich, dass allein durch die Wahl der richtigen Datenstrukturen eine Leistungssteigerung um das Fünffache erreicht werden kann. Und das überrascht selbst erfahrene Programmierer! In meinem speziellen Fall bestand der Trick darin, dass Sie keine Speicherzuweisungen in einer Hot-Loop vornehmen sollten. Nun, das ist nicht die ganze Wahrheit, aber im Allgemeinen sollten Sie „einmal in X“ nicht hervorheben, wenn X groß genug ist. Wenn X zweieinhalb Gigabyte beträgt, sollten Sie nichts „einmal pro Buchstabe“, „einmal pro Zeile“ oder „einmal pro Feld“ oder ähnliches zuweisen. Hier wird Zeit verbracht. Wie funktioniert das überhaupt? Stellen Sie sich vor, ich rufe an String.split() oder BufferedReader.readLine(). Readline Erstellt eine Zeichenfolge aus einer Reihe von Bytes, die über das Netzwerk gesendet wurden, einmal für jede Zeile, für jede der Hunderten Millionen Zeilen. Ich nehme diese Zeile, analysiere sie und werfe sie weg. Warum werfe ich es weg? Nun ja, ich habe es bereits verarbeitet, das ist alles. Für jedes aus diesen 2.7G gelesene Byte werden also zwei Zeichen in die Zeile geschrieben, also bereits 5.4G, und ich brauche sie für nichts weiter, also werden sie weggeworfen. Wenn wir uns die Speicherbandbreite ansehen, laden wir 2.7 G, die über den Speicher und den Speicherbus im Prozessor gehen, und dann wird doppelt so viel an die im Speicher liegende Leitung gesendet, und das alles wird ausgefranst, wenn jede neue Leitung erstellt wird. Aber ich muss es lesen, die Hardware liest es, auch wenn später alles ausgefranst ist. Und ich muss es aufschreiben, weil ich eine Zeile erstellt habe und die Caches voll sind – der Cache kann keine 2.7 G aufnehmen. Für jedes Byte, das ich lese, lese ich also zwei weitere Bytes und schreibe zwei weitere Bytes, und am Ende haben sie ein Verhältnis von 4:1 – in diesem Verhältnis verschwenden wir Speicherbandbreite. Und dann stellt sich heraus, dass wenn ich es tue String.split() – Dies ist nicht das letzte Mal, dass ich das mache, es könnten noch 6-7 Felder drin sein. Der klassische Code, der CSV liest und dann die Zeichenfolgen analysiert, führt also zu einer Speicherbandbreitenverschwendung von etwa 14:1 im Vergleich zu dem, was Sie tatsächlich haben möchten. Wenn Sie diese Auswahlmöglichkeiten wegwerfen, können Sie eine fünffache Beschleunigung erzielen.

Und es ist nicht so schwierig. Wenn Sie den Code aus dem richtigen Blickwinkel betrachten, wird alles ganz einfach, sobald Sie das Problem erkennen. Sie sollten nicht ganz aufhören, Speicher zuzuweisen: Das einzige Problem besteht darin, dass Sie etwas zuweisen und es sofort abstürzt und nebenbei eine wichtige Ressource verbrennt, in diesem Fall die Speicherbandbreite. Und all dies führt zu einem Rückgang der Produktivität. Auf x86 müssen Sie normalerweise Prozessorzyklen aktiv brennen, aber hier haben Sie den gesamten Speicher viel früher verbraucht. Die Lösung besteht darin, die Ausflussmenge zu reduzieren. 
Der andere Teil des Problems besteht darin, dass Sie, wenn Sie den Profiler ausführen, wenn der Speicherstreifen erschöpft ist, genau dann, wenn es passiert, normalerweise darauf warten, dass der Cache zurückkommt, weil er voller Müll ist, den Sie gerade produziert haben, all diese Zeilen. Daher wird jeder Lade- oder Speichervorgang langsam, da er zu Cache-Fehlern führt – der gesamte Cache ist langsam geworden und wartet darauf, dass Müll ihn verlässt. Daher zeigt der Profiler nur warmes Zufallsrauschen an, das über die gesamte Schleife verschmiert ist – es gibt keine separate Hot-Anweisung oder Stelle im Code. Nur Lärm. Und wenn Sie sich die GC-Zyklen ansehen, sind sie alle der jungen Generation und superschnell – maximal Mikrosekunden oder Millisekunden. Schließlich stirbt all diese Erinnerung sofort. Sie weisen Milliarden von Gigabyte zu, und er schneidet sie ab, schneidet sie ab und schneidet sie erneut ab. Das alles geht sehr schnell. Es stellt sich heraus, dass es günstige GC-Zyklen gibt, warmes Rauschen während des gesamten Zyklus, aber wir wollen eine 5-fache Beschleunigung erreichen. In diesem Moment sollte sich etwas in Ihrem Kopf schließen und klingen: „Warum ist das so?!“ Der Speicherstreifenüberlauf wird im klassischen Debugger nicht angezeigt; Sie müssen den Hardware-Leistungsindikator-Debugger ausführen und ihn selbst und direkt sehen. Dies lässt sich aber anhand dieser drei Symptome nicht direkt vermuten. Das dritte Symptom ist, wenn Sie sich ansehen, was Sie hervorheben, den Profiler fragen und er antwortet: „Sie haben eine Milliarde Zeilen erstellt, aber der GC hat kostenlos funktioniert.“ Sobald dies geschieht, wird Ihnen klar, dass Sie zu viele Objekte erstellt und die gesamte Erinnerungsspur verbrannt haben. Es gibt eine Möglichkeit, das herauszufinden, aber es ist nicht offensichtlich. 

Das Problem liegt in der Datenstruktur: Die nackte Struktur, die allem, was passiert, zugrunde liegt, ist zu groß, sie ist 2.7 GB auf der Festplatte, daher ist es höchst unerwünscht, eine Kopie dieser Sache zu erstellen – Sie möchten sie sofort aus dem Netzwerk-Byte-Puffer laden in die Register, um nicht fünfmal auf der Leitung hin- und herzulesen. Leider stellt Ihnen Java standardmäßig keine solche Bibliothek als Teil des JDK zur Verfügung. Aber das ist trivial, oder? Im Wesentlichen handelt es sich hierbei um 5–10 Codezeilen, die zur Implementierung Ihres eigenen gepufferten String-Loaders verwendet werden, der das Verhalten der String-Klasse wiederholt und gleichzeitig als Wrapper um den zugrunde liegenden Byte-Puffer fungiert. Als Ergebnis stellt sich heraus, dass Sie fast wie mit Strings arbeiten, aber tatsächlich werden Zeiger auf den Puffer dorthin verschoben, und die Rohbytes werden nirgendwohin kopiert, und somit werden dieselben Puffer immer wieder verwendet, und Das Betriebssystem übernimmt gerne die Dinge, für die es entwickelt wurde, wie etwa die versteckte Doppelpufferung dieser Bytepuffer, und Sie müssen sich nicht mehr durch einen endlosen Strom unnötiger Daten wühlen. Verstehen Sie übrigens, dass bei der Arbeit mit GC garantiert ist, dass jede Speicherzuweisung nach dem letzten GC-Zyklus für den Prozessor nicht sichtbar ist? Deshalb kann sich das alles unmöglich im Cache befinden, und dann kommt es zu einem 100 % garantierten Fehlschlag. Bei der Arbeit mit einem Zeiger dauert das Subtrahieren eines Registers vom Speicher auf x86 1-2 Taktzyklen, und sobald dies geschieht, zahlen Sie, zahlen Sie, zahlen Sie, weil der Speicher vollständig aktiviert ist NEUN Caches – und das sind die Kosten für die Speicherzuweisung. Echter Wert.

Mit anderen Worten: Datenstrukturen sind am schwierigsten zu ändern. Und wenn Sie erst einmal feststellen, dass Sie die falsche Datenstruktur gewählt haben, die später die Leistung beeinträchtigt, gibt es normalerweise noch viel zu tun, aber wenn Sie dies nicht tun, wird es noch schlimmer. Zunächst müssen Sie über Datenstrukturen nachdenken, das ist wichtig. Die Hauptkosten entstehen hier durch dicke Datenstrukturen, die zunehmend im Sinne von „Ich habe die Datenstruktur X in die Datenstruktur Y kopiert, weil mir die Form von Y besser gefällt“ verwendet werden. Aber der Kopiervorgang (der billig erscheint) verschwendet tatsächlich Speicherbandbreite, und darin steckt die gesamte verschwendete Ausführungszeit. Wenn ich einen riesigen JSON-String habe und ihn in einen strukturierten DOM-Baum von POJOs oder so etwas umwandeln möchte, führt das Parsen dieses Strings, das Erstellen des POJO und der spätere erneute Zugriff auf das POJO zu unnötigen Kosten – das ist es nicht billig. Es sei denn, Sie laufen viel häufiger um POJOs herum als um eine Schnur. Sie können stattdessen ohne weiteres versuchen, die Zeichenfolge zu entschlüsseln und nur das zu extrahieren, was Sie benötigen, ohne sie in ein POJO umzuwandeln. Wenn das alles auf einem Pfad passiert, von dem maximale Leistung verlangt wird, keine POJOs für Sie, müssen Sie irgendwie direkt in die Leitung einsteigen.

Warum eine eigene Programmiersprache erstellen?

Andrew: Sie sagten, dass Sie zum Verständnis des Kostenmodells Ihre eigene kleine Sprache schreiben müssen ...

Cliff: Keine Sprache, sondern ein Compiler. Eine Sprache und ein Compiler sind zwei verschiedene Dinge. Der wichtigste Unterschied liegt im Kopf. 

Andrew: Soweit ich weiß, experimentieren Sie übrigens damit, Ihre eigenen Sprachen zu erstellen. Wofür?

Cliff: Weil ich kann! Ich bin halb im Ruhestand, das ist also mein Hobby. Ich habe mein ganzes Leben lang die Sprachen anderer Leute implementiert. Ich habe auch viel an meinem Codierungsstil gearbeitet. Und auch, weil ich Probleme in anderen Sprachen sehe. Ich sehe, dass es bessere Möglichkeiten gibt, vertraute Dinge zu tun. Und ich würde sie nutzen. Ich habe es einfach satt, Probleme in mir selbst, in Java, in Python oder in jeder anderen Sprache zu sehen. Ich schreibe jetzt in React Native, JavaScript und Elm als Hobby, bei dem es nicht um den Ruhestand geht, sondern um aktive Arbeit. Ich schreibe auch in Python und werde höchstwahrscheinlich weiterhin am maschinellen Lernen für Java-Backends arbeiten. Es gibt viele beliebte Sprachen und alle haben interessante Funktionen. Jeder ist auf seine Weise gut und Sie können versuchen, all diese Funktionen zusammenzubringen. Also untersuche ich Dinge, die mich interessieren, das Verhalten der Sprache und versuche, eine vernünftige Semantik zu entwickeln. Und bis jetzt gelingt es mir! Im Moment habe ich Probleme mit der Speichersemantik, weil ich es wie in C und Java haben möchte und ein starkes Speichermodell und eine starke Speichersemantik für Lade- und Speichervorgänge erhalten möchte. Gleichzeitig verfügen Sie über eine automatische Typinferenz wie in Haskell. Hier versuche ich, Haskell-ähnliche Typinferenz mit Speicherarbeit in C und Java zu kombinieren. Das mache ich zum Beispiel seit 2-3 Monaten.

Andrew: Wenn Sie eine Sprache entwickeln, die bessere Aspekte aus anderen Sprachen übernimmt, glauben Sie, dass jemand das Gegenteil tun wird: Ihre Ideen übernehmen und sie nutzen?

Cliff: Genau so entstehen neue Sprachen! Warum ähnelt Java C? Weil C eine gute Syntax hatte, die jeder verstand, und Java von dieser Syntax inspiriert wurde, indem es Typsicherheit, Array-Grenzenprüfung und GC hinzufügte und auch einige Dinge gegenüber C verbesserte. Sie fügten ihre eigenen hinzu. Aber sie waren ziemlich inspiriert, oder? Jeder steht auf den Schultern der Giganten, die vor Ihnen kamen – so entsteht Fortschritt.

Andrew: Soweit ich weiß, wird Ihre Sprache speichersicher sein. Haben Sie darüber nachgedacht, so etwas wie einen Borrow-Checker von Rust zu implementieren? Hast du ihn angeschaut, was denkst du über ihn?

Cliff: Nun, ich schreibe schon seit Ewigkeiten C, mit all diesem Malloc und Free und der manuellen Verwaltung der Lebensdauer. Wissen Sie, 90-95 % der manuell gesteuerten Lebensdauer haben die gleiche Struktur. Und es ist sehr, sehr schmerzhaft, es manuell zu machen. Ich möchte, dass der Compiler Ihnen einfach sagt, was dort vor sich geht und was Sie mit Ihren Aktionen erreicht haben. Für einige Dinge erledigt der Borrow Checker dies sofort. Und es sollte automatisch Informationen anzeigen, alles verstehen und mich nicht einmal mit der Präsentation dieses Verständnisses belasten. Es muss zumindest eine lokale Escape-Analyse durchführen, und nur wenn dies fehlschlägt, müssen Typanmerkungen hinzugefügt werden, die die Lebensdauer beschreiben – und ein solches Schema ist viel komplexer als ein Borrow-Checker oder tatsächlich jeder vorhandene Speicher-Checker. Die Wahl zwischen „alles ist gut“ und „ich verstehe nichts“ – nein, es muss etwas Besseres geben. 
Als jemand, der viel Code in C geschrieben hat, denke ich, dass die Unterstützung der automatischen Lebenszeitkontrolle das Wichtigste ist. Ich habe es auch satt, wie viel Java Speicher verbraucht, und der Hauptkritikpunkt ist der GC. Wenn Sie in Java Speicher zuweisen, erhalten Sie nicht den Speicher zurück, der beim letzten GC-Zyklus lokal war. Dies ist bei Sprachen mit genauerer Speicherverwaltung nicht der Fall. Wenn Sie malloc aufrufen, erhalten Sie sofort den Speicher, der normalerweise gerade verwendet wurde. Normalerweise macht man einige vorübergehende Dinge mit dem Gedächtnis und gibt es sofort zurück. Und es kehrt sofort zum Malloc-Pool zurück und wird beim nächsten Malloc-Zyklus wieder herausgezogen. Daher wird die tatsächliche Speichernutzung auf die Menge lebender Objekte zu einem bestimmten Zeitpunkt reduziert, zuzüglich Lecks. Und wenn nicht alles auf völlig unanständige Weise durchsickert, landet der größte Teil des Speichers in Caches und im Prozessor, und es funktioniert schnell. Erfordert jedoch viel manuelle Speicherverwaltung mit Malloc und Free, die in der richtigen Reihenfolge und am richtigen Ort aufgerufen werden. Rust kann dies selbstständig bewältigen und bietet in vielen Fällen eine sogar bessere Leistung, da der Speicherverbrauch auf die aktuelle Berechnung beschränkt wird – anstatt auf den nächsten GC-Zyklus zu warten, um Speicher freizugeben. Dadurch haben wir eine sehr interessante Möglichkeit zur Leistungsverbesserung erhalten. Und ziemlich leistungsstark – ich meine, ich habe solche Dinge getan, als ich Daten für Fintech verarbeitete, und dadurch konnte ich eine etwa fünffache Geschwindigkeitssteigerung erzielen. Das ist ein ziemlich großer Fortschritt, insbesondere in einer Welt, in der Prozessoren nicht schneller werden und wir immer noch auf Verbesserungen warten.

Karriere als Leistungsingenieur

Andrew: Ich würde mich auch gerne allgemein zum Thema Karriere umhören. Sie erlangten durch Ihre JIT-Arbeit bei HotSpot Bekanntheit und wechselten dann zu Azul, einem ebenfalls JVM-Unternehmen. Aber wir haben bereits mehr an Hardware als an Software gearbeitet. Und dann wechselten sie plötzlich zu Big Data und maschinellem Lernen und dann zur Betrugserkennung. Wie ist das passiert? Das sind sehr unterschiedliche Entwicklungsbereiche.

Cliff: Ich programmiere schon ziemlich lange und habe es geschafft, viele verschiedene Kurse zu belegen. Und wenn Leute sagen: „Oh, du bist derjenige, der JIT für Java gemacht hat!“, ist das immer lustig. Zuvor arbeitete ich jedoch an einem Klon von PostScript – der Sprache, die Apple einst für seine Laserdrucker verwendete. Und davor habe ich eine Implementierung der Forth-Sprache durchgeführt. Ich denke, das gemeinsame Thema für mich ist die Werkzeugentwicklung. Mein ganzes Leben lang habe ich Tools entwickelt, mit denen andere Leute ihre coolen Programme schreiben. Ich war aber auch an der Entwicklung von Betriebssystemen, Treibern, Debuggern auf Kernel-Ebene und Sprachen für die Betriebssystementwicklung beteiligt, die zunächst trivial waren, mit der Zeit jedoch immer komplexer wurden. Das Hauptthema ist jedoch immer noch die Entwicklung von Werkzeugen. Ein großer Teil meines Lebens verbrachte ich zwischen Azul und Sun und drehte mich um Java. Aber als ich mich mit Big Data und maschinellem Lernen beschäftigte, setzte ich meinen schicken Hut wieder auf und sagte: „Oh, jetzt haben wir ein nicht triviales Problem, und es passieren viele interessante Dinge und die Leute tun Dinge.“ Dies ist ein großartiger Entwicklungspfad.

Ja, ich liebe verteiltes Rechnen wirklich. Mein erster Job war als C-Student bei einem Werbeprojekt. Hierbei handelte es sich um verteiltes Rechnen auf Zilog Z80-Chips, die Daten für die analoge OCR sammelten, die von einem echten analogen Analysator erzeugt wurden. Es war ein cooles und völlig verrücktes Thema. Aber es gab Probleme, ein Teil wurde nicht richtig erkannt, also musste man ein Bild herausnehmen und es einer Person zeigen, die bereits mit den Augen lesen und berichten konnte, was darauf stand, und deshalb gab es Jobs mit Daten und diese Jobs hatten ihre eigene Sprache. Es gab ein Backend, das all dies verarbeitete – Z80s liefen parallel mit VT100-Terminals – eines pro Person, und es gab ein paralleles Programmiermodell auf dem Z80. Ein gemeinsamer Speicher, den alle Z80 in einer Sternkonfiguration gemeinsam nutzen; Auch die Rückwandplatine wurde gemeinsam genutzt, und die Hälfte des RAM wurde innerhalb des Netzwerks gemeinsam genutzt, die andere Hälfte war privat oder ging an etwas anderes. Ein sinnvoll komplexes parallel verteiltes System mit gemeinsam genutztem... halbgeteiltem Speicher. Wann war das... Ich kann mich nicht einmal erinnern, irgendwo Mitte der 80er Jahre. Vor ziemlich langer Zeit. 
Ja, gehen wir davon aus, dass 30 Jahre schon ziemlich lange her sind. Probleme im Zusammenhang mit verteiltem Rechnen gibt es schon seit geraumer Zeit; die Menschen haben sich schon lange damit auseinandergesetzt Beowulf-Cluster. Solche Cluster sehen aus wie ... Zum Beispiel: Es gibt Ethernet und Ihr schneller x86 ist mit diesem Ethernet verbunden, und jetzt möchten Sie einen gefälschten gemeinsam genutzten Speicher erhalten, weil damals niemand verteilte Computercodierung durchführen konnte, das war zu schwierig und daher vorhanden war gefälschter Shared Memory mit Schutzspeicherseiten auf x86, und wenn Sie auf diese Seite geschrieben haben, dann haben wir anderen Prozessoren mitgeteilt, dass, wenn sie auf denselben Shared Memory zugreifen, dieser von Ihnen geladen werden müsste, und somit so etwas wie ein Protokoll zur Unterstützung Cache-Kohärenz erschien und Software dafür. Interessantes Konzept. Das eigentliche Problem war natürlich etwas anderes. Das alles funktionierte, aber es kam schnell zu Leistungsproblemen, weil niemand die Leistungsmodelle ausreichend verstand – welche Speicherzugriffsmuster es gab, wie man sicherstellte, dass die Knoten sich nicht endlos gegenseitig anpingten und so weiter.

Was ich bei H2O herausgefunden habe, ist, dass es die Entwickler selbst sind, die dafür verantwortlich sind, zu bestimmen, wo Parallelität versteckt ist und wo nicht. Ich habe ein Codierungsmodell entwickelt, das das Schreiben von Hochleistungscode einfach und unkompliziert macht. Aber langsam laufenden Code zu schreiben ist schwierig und sieht schlecht aus. Sie müssen ernsthaft versuchen, langsamen Code zu schreiben, Sie müssen nicht standardmäßige Methoden verwenden. Der Bremscode ist auf den ersten Blick erkennbar. Daher schreiben Sie normalerweise Code, der schnell läuft, müssen aber herausfinden, was im Fall von Shared Memory zu tun ist. All dies ist an große Arrays gebunden und das Verhalten dort ähnelt nichtflüchtigen großen Arrays in parallelem Java. Ich meine, stellen Sie sich vor, dass zwei Threads in ein paralleles Array schreiben, einer von ihnen gewinnt und der andere dementsprechend verliert, und Sie wissen nicht, welcher Thread welcher ist. Wenn sie nicht volatil sind, kann die Reihenfolge beliebig sein – und das funktioniert wirklich gut. Den Leuten ist die Reihenfolge der Vorgänge sehr wichtig, sie platzieren „flüchtig“ an den richtigen Stellen und erwarten an den richtigen Stellen speicherbezogene Leistungsprobleme. Andernfalls würden sie einfach Code in Form von Schleifen von 1 bis N schreiben, wobei N einige Billionen beträgt, in der Hoffnung, dass alle komplexen Fälle automatisch parallel werden – und dort funktioniert es nicht. Aber in H2O ist dies weder Java noch Scala; Sie können es als „Java minus minus“ betrachten, wenn Sie möchten. Dies ist ein sehr klarer Programmierstil und ähnelt dem Schreiben von einfachem C- oder Java-Code mit Schleifen und Arrays. Gleichzeitig kann Speicher jedoch in Terabyte verarbeitet werden. Ich verwende immer noch H2O. Ich verwende es von Zeit zu Zeit in verschiedenen Projekten – und es ist immer noch das schnellste Gerät, Dutzende Male schneller als seine Konkurrenten. Wenn Sie Big Data mit spaltenbasierten Daten erstellen, ist H2O kaum zu schlagen.

Technische Herausforderungen

Andrew: Was war Ihre größte Herausforderung in Ihrer gesamten Karriere?

Cliff: Besprechen wir den technischen oder nichttechnischen Teil des Problems? Ich würde sagen, dass die größten Herausforderungen nicht technischer Natur sind. 
Was technische Herausforderungen betrifft. Ich habe sie einfach besiegt. Ich weiß nicht einmal, was das größte war, aber es gab einige ziemlich interessante, die ziemlich viel Zeit und mentale Anstrengung erforderten. Als ich zu Sun ging, war ich mir sicher, dass ich einen schnellen Compiler erstellen würde, und ein paar Senioren antworteten, dass mir das nie gelingen würde. Aber ich bin diesem Weg gefolgt, habe einen Compiler bis zum Register-Allocator geschrieben, und es war ziemlich schnell. Es war so schnell wie das moderne C1, aber der Allokator war damals viel langsamer, und im Nachhinein betrachtet handelte es sich um ein großes Datenstrukturproblem. Ich brauchte es, um einen grafischen Registerzuordner zu schreiben, und verstand das Dilemma zwischen Code-Ausdruckskraft und Geschwindigkeit nicht, das zu dieser Zeit bestand und sehr wichtig war. Es stellte sich heraus, dass die Datenstruktur normalerweise die Cache-Größe auf x86-Geräten dieser Zeit übersteigt. Wenn ich also zunächst davon ausging, dass der Registerzuteiler 5-10 Prozent der gesamten Jitter-Zeit ausmachen würde, dann stellte sich heraus, dass dies in Wirklichkeit der Fall war 50 Prozent.

Mit der Zeit wurde der Compiler sauberer und effizienter, erzeugte in mehr Fällen keinen schrecklichen Code mehr und die Leistung ähnelte immer mehr der eines C-Compilers. Es sei denn natürlich, Sie schreiben irgendeinen Mist, den selbst C nicht beschleunigt . Wenn Sie Code wie C schreiben, erhalten Sie in mehr Fällen eine Leistung wie C. Und je weiter Sie gingen, desto häufiger bekamen Sie Code, der asymptotisch mit der Ebene C übereinstimmte, und der Registerzuteiler begann wie etwas Vollständiges auszusehen ... unabhängig davon, ob Ihr Code schnell oder langsam läuft. Ich habe weiter am Allokator gearbeitet, um eine bessere Auswahl zu ermöglichen. Er wurde immer langsamer, aber er zeigte immer bessere Leistungen in Fällen, in denen kein anderer damit zurechtkam. Ich könnte in einen Registerzuordner eintauchen, dort einen Monat lang arbeiten und plötzlich würde der gesamte Code 5 % schneller ausgeführt. Dies geschah immer wieder und der Registerzuteiler wurde so etwas wie ein Kunstwerk – jeder liebte oder hasste ihn, und Leute von der Akademie stellten Fragen zum Thema „Warum wird alles so gemacht“ und warum nicht Zeilenscan, und was ist der Unterschied. Die Antwort ist immer noch dieselbe: Ein Allokator, der auf der Färbung von Diagrammen und einer sehr sorgfältigen Arbeit mit dem Puffercode basiert, ist gleichbedeutend mit einer Waffe des Sieges, der besten Kombination, die niemand besiegen kann. Und das ist eine eher nicht offensichtliche Sache. Alles andere, was der Compiler dort macht, sind ziemlich gut untersuchte Dinge, obwohl sie auch auf das Niveau der Kunst gebracht wurden. Ich habe immer Dinge gemacht, die den Compiler in ein Kunstwerk verwandeln sollten. Aber nichts davon war etwas Außergewöhnliches – bis auf den Registerzuteiler. Der Trick besteht darin, vorsichtig zu sein verringern unter Last und wenn dies passiert (ich kann es bei Interesse genauer erklären), bedeutet dies, dass Sie aggressiver inline können, ohne Gefahr zu laufen, über einen Knick im Leistungsplan zu geraten. Damals gab es eine ganze Reihe vollwertiger Compiler, die mit Schnickschnack behangen waren und über Registerzuordner verfügten, aber niemand sonst konnte das.

Das Problem besteht darin, dass, wenn Sie Methoden hinzufügen, die dem Inlining unterliegen, den Inlining-Bereich vergrößern und vergrößern, die Menge der verwendeten Werte sofort die Anzahl der Register übersteigt und Sie diese abschneiden müssen. Das kritische Niveau erreicht normalerweise, wenn der Zuteiler aufgibt und ein guter Kandidat für eine Verschüttung einen anderen wert ist, Sie werden einige allgemein wilde Dinge verkaufen. Der Wert des Inlinings liegt hier darin, dass Sie einen Teil des Overheads, des Overheads für Aufrufe und Speicherung, verlieren, die darin enthaltenen Werte sehen und diese weiter optimieren können. Der Preis für das Inlining besteht darin, dass eine große Anzahl von Live-Werten gebildet wird. Wenn Ihr Registerzuteiler mehr als nötig verbraucht, verlieren Sie sofort. Daher haben die meisten Allokatoren ein Problem: Wenn Inlining eine bestimmte Grenze überschreitet, wird alles auf der Welt gekürzt und die Produktivität kann in die Toilette gespült werden. Diejenigen, die den Compiler implementieren, fügen einige Heuristiken hinzu: Um beispielsweise das Inlining zu stoppen, beginnen Sie mit einer ausreichend großen Größe, da Zuweisungen alles ruinieren. So entsteht ein Knick im Leistungsdiagramm – Sie inline, inline, die Leistung wächst langsam – und dann boomt es! – es fällt wie ein Mauersegler herunter, weil man zu viel gelinet hat. So funktionierte alles vor dem Aufkommen von Java. Java erfordert viel mehr Inlining, daher musste ich meinen Allokator viel aggressiver gestalten, damit er sich einpendelt und nicht abstürzt. Wenn Sie zu viel Inline verwenden, fängt es an zu verschütten, aber dann kommt immer noch der Moment, in dem es heißt, dass es kein Verschütten mehr gibt. Das ist eine interessante Beobachtung, und sie kam mir aus dem Nichts, nicht offensichtlich, aber sie hat sich ausgezahlt. Ich habe intensiv mit Inlining begonnen und es hat mich an Orte geführt, an denen Java- und C-Leistung nebeneinander funktionieren. Sie liegen sehr nahe beieinander – ich kann Java-Code schreiben, der deutlich schneller ist als C-Code und ähnliches, aber im Großen und Ganzen sind sie im Durchschnitt ungefähr vergleichbar. Ich denke, ein Teil dieses Verdienstes ist der Register-Allokator, der es mir ermöglicht, so dumm wie möglich zu inline. Ich füge einfach alles hinzu, was ich sehe. Die Frage ist hier, ob der Allokator gut funktioniert und ob das Ergebnis intelligent funktionierender Code ist. Das war eine große Herausforderung: das alles zu verstehen und umzusetzen.

Ein wenig über Registerzuteilung und Multicores

Vladimir: Probleme wie die Registerbelegung scheinen eine Art ewiges, endloses Thema zu sein. Ich frage mich, ob es jemals eine Idee gab, die vielversprechend schien und dann in der Praxis scheiterte?

Cliff: Sicherlich! Die Registerzuordnung ist ein Bereich, in dem Sie versuchen, einige Heuristiken zur Lösung eines NP-vollständigen Problems zu finden. Und eine perfekte Lösung kann man nie erreichen, oder? Das ist einfach unmöglich. Schauen Sie, die Ahead-of-Time-Kompilierung funktioniert auch schlecht. Das Gespräch hier dreht sich um einige durchschnittliche Fälle. Über die typische Leistung, damit Sie etwas messen können, das Ihrer Meinung nach eine gute typische Leistung ist – schließlich arbeiten Sie daran, sie zu verbessern! Die Registerzuteilung ist ein Thema, bei dem es um die Leistung geht. Sobald Sie den ersten Prototyp haben, dieser funktioniert und bemalt, was benötigt wird, beginnt die Aufführungsarbeit. Sie müssen lernen, gut zu messen. Warum ist es wichtig? Wenn Sie klare Daten haben, können Sie sich verschiedene Bereiche ansehen und sehen: Ja, hier hat es geholfen, aber da ist alles kaputt gegangen! Ein paar gute Ideen kommen auf, man fügt neue Heuristiken hinzu und plötzlich funktioniert alles im Durchschnitt etwas besser. Oder es startet nicht. Ich hatte eine Reihe von Fällen, in denen wir um die Leistung von fünf Prozent kämpften, die unsere Entwicklung vom vorherigen Allokator unterschied. Und jedes Mal sieht es so aus: Irgendwo gewinnt man, irgendwo verliert man. Wenn Sie über gute Tools zur Leistungsanalyse verfügen, können Sie die verlorenen Ideen finden und verstehen, warum sie scheitern. Vielleicht lohnt es sich, alles so zu belassen, wie es ist, oder die Feinabstimmung ernsthafter anzugehen oder etwas anderes zu reparieren. Es ist eine ganze Menge Dinge! Ich habe diesen coolen Hack gemacht, aber ich brauche auch diesen und diesen und diesen – und ihre Gesamtkombination bringt einige Verbesserungen. Und Einzelgänger können scheitern. Dies ist die Natur der Leistungsarbeit an NP-vollständigen Problemen.

Vladimir: Man hat das Gefühl, dass Dinge wie das Einstreichen von Allokatoren ein Problem sind, das bereits gelöst wurde. Nun, es ist für Sie entschieden, nach dem, was Sie sagen, also lohnt es sich dann überhaupt ...

Cliff: Es ist als solches nicht gelöst. Sie müssen das Problem in „gelöst“ umwandeln. Es gibt schwierige Probleme und sie müssen gelöst werden. Sobald dies erledigt ist, ist es Zeit, an der Produktivität zu arbeiten. Sie müssen diese Arbeit entsprechend angehen – Benchmarks durchführen, Metriken sammeln, Situationen erklären, in denen Ihr alter Hack nach einem Rollback auf eine frühere Version wieder funktionierte (oder umgekehrt aufhörte). Und geben Sie nicht auf, bis Sie etwas erreicht haben. Wie ich bereits sagte, wenn es coole Ideen gibt, die nicht funktioniert haben, ist es im Bereich der Zuordnung von Ideenregistern nahezu endlos. Sie können beispielsweise wissenschaftliche Publikationen lesen. Obwohl sich dieser Bereich jetzt viel langsamer bewegt und klarer geworden ist als in seiner Jugend. Es gibt jedoch unzählige Menschen, die in diesem Bereich arbeiten und alle ihre Ideen sind einen Versuch wert, sie alle warten in den Startlöchern. Und wie gut sie sind, merkt man erst, wenn man sie probiert. Wie gut lassen sie sich mit allem anderen in Ihrem Allokator integrieren, denn ein Allokator macht viele Dinge, und einige Ideen funktionieren in Ihrem spezifischen Allokator nicht, in einem anderen Allokator jedoch problemlos. Der Hauptweg, um für den Allokator zu gewinnen, besteht darin, die langsamen Dinge aus dem Hauptpfad herauszuziehen und sie zu zwingen, sich entlang der Grenzen der langsamen Pfade aufzuteilen. Wenn Sie also einen GC ausführen, den langsamen Weg wählen, deoptimieren, eine Ausnahme auslösen möchten, all das – Sie wissen, dass diese Dinge relativ selten sind. Und sie sind wirklich selten, das habe ich überprüft. Man macht zusätzliche Arbeit und es beseitigt viele Einschränkungen auf diesen langsamen Wegen, aber das spielt keine Rolle, weil sie langsam sind und selten befahren werden. Zum Beispiel ein Nullzeiger – das passiert nie, oder? Sie benötigen mehrere Pfade für verschiedene Dinge, diese sollten jedoch den Hauptpfad nicht beeinträchtigen. 

Vladimir: Was halten Sie von Multicores, wenn es Tausende von Kernen gleichzeitig gibt? Ist das eine nützliche Sache?

Cliff: Der Erfolg der GPU zeigt, dass sie durchaus nützlich ist!

Vladimir: Sie sind ziemlich spezialisiert. Was ist mit Allzweckprozessoren?

Cliff: Nun, das war Azuls Geschäftsmodell. Die Antwort kam in einer Zeit, in der die Menschen vorhersehbare Leistung wirklich liebten. Damals war es schwierig, parallelen Code zu schreiben. Das H2O-Codierungsmodell ist hoch skalierbar, aber kein Allzweckmodell. Vielleicht etwas allgemeiner als bei der Verwendung einer GPU. Sprechen wir über die Komplexität der Entwicklung eines solchen Produkts oder über die Komplexität seiner Verwendung? Azul hat mir zum Beispiel eine interessante Lektion beigebracht, die eher nicht offensichtlich ist: Kleine Caches sind normal. 

Die größte Herausforderung im Leben

Vladimir: Was ist mit nichttechnischen Herausforderungen?

Cliff: Die größte Herausforderung bestand nicht darin, freundlich und nett zu den Menschen zu sein. Dadurch befand ich mich ständig in extremen Konfliktsituationen. Diejenigen, bei denen ich wusste, dass etwas schief lief, aber ich wusste nicht, wie ich mit diesen Problemen weitermachen sollte, und konnte nicht damit umgehen. Auf diese Weise entstanden viele langfristige Probleme, die sich über Jahrzehnte erstreckten. Die Tatsache, dass Java über C1- und C2-Compiler verfügt, ist eine direkte Folge davon. Auch die Tatsache, dass es zehn Jahre in Folge keine mehrstufige Kompilierung in Java gab, ist eine direkte Folge. Es ist offensichtlich, dass wir ein solches System brauchten, aber es ist nicht offensichtlich, warum es nicht existierte. Ich hatte Probleme mit einem Ingenieur ... oder einer Gruppe von Ingenieuren. Es war einmal, als ich anfing, bei Sun zu arbeiten, ich war... Okay, nicht nur damals, ich habe im Allgemeinen immer zu allem meine eigene Meinung. Und ich dachte, es sei wahr, dass Sie Ihre Wahrheit einfach annehmen und direkt sagen könnten. Vor allem, da ich die meiste Zeit erschreckend Recht hatte. Und wenn Ihnen dieser Ansatz nicht gefällt ... insbesondere, wenn Sie offensichtlich falsch liegen und Unsinn machen ... Im Allgemeinen könnten nur wenige Menschen diese Form der Kommunikation tolerieren. Obwohl einige es könnten, so wie ich. Ich habe mein ganzes Leben auf meritokratischen Prinzipien aufgebaut. Wenn du mir etwas Falsches zeigst, drehe ich mich sofort um und sage: Du hast Unsinn gesagt. Gleichzeitig entschuldige ich mich natürlich und werde die Vorzüge, falls vorhanden, zur Kenntnis nehmen und andere richtige Maßnahmen ergreifen. Andererseits habe ich mit einem erschreckend großen Prozentsatz der Gesamtzeit erschreckend recht. Und in Beziehungen zu Menschen funktioniert es nicht besonders gut. Ich versuche nicht nett zu sein, aber ich stelle die Frage unverblümt. „Das wird nie funktionieren, denn eins, zwei und drei.“ Und sie sagten: „Oh!“ Es gab andere Konsequenzen, die man wahrscheinlich besser ignorieren sollte: zum Beispiel diejenigen, die zur Scheidung von meiner Frau und danach zu zehn Jahren Depression führten.

Herausforderung ist ein Kampf mit Menschen, mit ihrer Wahrnehmung dessen, was man tun kann und was nicht, was wichtig ist und was nicht. Es gab viele Herausforderungen hinsichtlich des Codierungsstils. Ich schreibe immer noch viel Code, und damals musste ich sogar langsamer werden, weil ich zu viele parallele Aufgaben erledigte und diese schlecht erledigte, anstatt mich auf eine zu konzentrieren. Rückblickend habe ich die Hälfte des Codes für den Java-JIT-Befehl geschrieben, den C2-Befehl. Der nächstschnellste Programmierer schrieb halb so langsam, der nächste halb so langsam, und es war ein exponentieller Rückgang. Die siebte Person in dieser Reihe war sehr, sehr langsam – das passiert immer! Ich habe viel Code berührt. Ich schaute mir an, wer was geschrieben hat, ausnahmslos, ich starrte auf ihren Code, überprüfte jeden von ihnen und schrieb immer noch mehr selbst als jeder von ihnen. Dieser Ansatz funktioniert bei Menschen nicht sehr gut. Manche Leute mögen das nicht. Und wenn sie damit nicht klarkommen, beginnen alle möglichen Beschwerden. Mir wurde zum Beispiel einmal gesagt, ich solle mit dem Codieren aufhören, weil ich zu viel Code schreibe und das das Team gefährde, und für mich klang das alles wie ein Witz: Alter, wenn der Rest des Teams verschwindet und ich weiter Code schreibe, dann du Ich werde nur die Hälfte der Mannschaften verlieren. Wenn ich andererseits weiterhin Code schreibe und Sie die Hälfte des Teams verlieren, klingt das nach sehr schlechtem Management. Ich habe nie wirklich darüber nachgedacht, nie darüber gesprochen, aber es war immer noch irgendwo in meinem Kopf. Der Gedanke drehte sich in meinem Hinterkopf: „Willst du mich alle veräppeln?“ Das größte Problem waren also ich und meine Beziehungen zu Menschen. Jetzt verstehe ich mich selbst viel besser, ich war lange Zeit Teamleiter für Programmierer und jetzt sage ich den Leuten direkt: Weißt du, ich bin, wer ich bin, und du wirst dich mit mir auseinandersetzen müssen – ist es in Ordnung, wenn ich stehe? Hier? Und als sie anfingen, sich damit auseinanderzusetzen, funktionierte alles. Tatsächlich bin ich weder schlecht noch gut, ich habe keine schlechten Absichten oder egoistischen Bestrebungen, es ist einfach mein Wesen und ich muss irgendwie damit leben.

Andrew: Erst vor Kurzem haben alle angefangen, über Selbstbewusstsein für Introvertierte und Soft Skills im Allgemeinen zu sprechen. Was können Sie dazu sagen?

Cliff: Ja, das war die Einsicht und Lektion, die ich aus meiner Scheidung von meiner Frau gelernt habe. Was ich aus der Scheidung gelernt habe, war, mich selbst zu verstehen. So begann ich, andere Menschen zu verstehen. Verstehen Sie, wie diese Interaktion funktioniert. Dies führte zu Entdeckungen nach der anderen. Es gab ein Bewusstsein dafür, wer ich bin und was ich vertrete. Was mache ich: Entweder bin ich mit der Aufgabe beschäftigt, oder ich vermeide Konflikte oder etwas anderes – und dieses Maß an Selbstbewusstsein hilft mir wirklich, die Kontrolle zu behalten. Danach geht alles viel einfacher. Eine Sache, die ich nicht nur bei mir selbst, sondern auch bei anderen Programmierern entdeckt habe, ist die Unfähigkeit, Gedanken auszudrücken, wenn man sich in einem emotionalen Stresszustand befindet. Du sitzt zum Beispiel da und programmierst, in einem Flow-Zustand, und dann kommen sie auf dich zugerannt und fangen an, hysterisch zu schreien, dass etwas kaputt ist und dass jetzt extreme Maßnahmen gegen dich ergriffen werden. Und Sie können kein Wort sagen, weil Sie sich in einem emotionalen Stresszustand befinden. Das erworbene Wissen ermöglicht es Ihnen, sich auf diesen Moment vorzubereiten, ihn zu überstehen und mit einem Rückzugsplan fortzufahren, nach dem Sie etwas unternehmen können. Also ja, wenn man anfängt zu begreifen, wie das alles funktioniert, ist das ein großes, lebensveränderndes Ereignis. 
Ich selbst konnte nicht die richtigen Worte finden, aber ich erinnerte mich an die Abfolge der Handlungen. Der Punkt ist, dass diese Reaktion sowohl körperlich als auch verbal ist und Sie Raum brauchen. Solch ein Raum im Zen-Sinne. Genau das muss man erklären und dann sofort zur Seite treten – rein körperlich zurücktreten. Wenn ich verbal schweige, kann ich die Situation emotional verarbeiten. Wenn das Adrenalin Ihr Gehirn erreicht und Sie in den Kampf- oder Fluchtmodus versetzt, können Sie nichts mehr sagen, nein – jetzt sind Sie ein Idiot, ein peitschender Ingenieur, unfähig, angemessen zu reagieren oder den Angriff sogar zu stoppen, und der Angreifer ist frei immer wieder anzugreifen. Sie müssen zunächst wieder Sie selbst werden, die Kontrolle wiedererlangen und aus dem „Kampf-oder-Flucht“-Modus herauskommen.

Und dafür brauchen wir verbalen Raum. Nur freier Speicherplatz. Wenn du überhaupt etwas sagst, dann kannst du genau das sagen und dann wirklich „Raum“ für dich finden: im Park spazieren gehen, dich unter der Dusche einschließen – das macht nichts. Die Hauptsache ist, sich vorübergehend von dieser Situation zu lösen. Sobald man für mindestens ein paar Sekunden abschaltet, die Kontrolle zurückerlangt, beginnt man nüchtern zu denken. „Okay, ich bin kein Idiot, ich mache keine dummen Sachen, ich bin ein ziemlich nützlicher Mensch.“ Sobald Sie sich selbst überzeugt haben, ist es an der Zeit, mit dem nächsten Schritt fortzufahren: dem Verstehen, was passiert ist. Sie wurden angegriffen, der Angriff kam von einer Stelle, an der Sie ihn nicht erwartet hatten, es war ein unehrlicher, abscheulicher Hinterhalt. Das ist schlecht. Der nächste Schritt besteht darin, zu verstehen, warum der Angreifer dies benötigte. Wirklich warum? Vielleicht weil er selbst wütend ist? Warum ist er verrückt? Zum Beispiel, weil er es vermasselt hat und keine Verantwortung übernehmen kann? Dies ist der Weg, mit der gesamten Situation sorgfältig umzugehen. Dafür braucht es aber Handlungsspielraum, verbalen Freiraum. Der allererste Schritt besteht darin, den verbalen Kontakt abzubrechen. Vermeiden Sie Diskussionen mit Worten. Brechen Sie es ab und gehen Sie so schnell wie möglich weg. Wenn es sich um ein Telefongespräch handelt, legen Sie einfach auf – diese Fähigkeit habe ich durch die Kommunikation mit meiner Ex-Frau gelernt. Wenn das Gespräch nicht gut verläuft, verabschieden Sie sich einfach und legen Sie auf. Von der anderen Seite des Telefons: „bla bla bla“, antworten Sie: „Ja, tschüss!“ und auflegen. Sie beenden einfach das Gespräch. Fünf Minuten später, wenn Sie die Fähigkeit zum vernünftigen Denken wiedererlangen, haben Sie sich etwas abgekühlt und können über alles nachdenken, was passiert ist und was als nächstes passieren wird. Und fangen Sie an, eine durchdachte Antwort zu formulieren, anstatt nur aus Emotionen heraus zu reagieren. Für mich war der Durchbruch in der Selbsterkenntnis gerade die Tatsache, dass ich bei emotionalem Stress nicht sprechen kann. Aus diesem Zustand herauszukommen, darüber nachzudenken und zu planen, wie man auf Probleme reagiert und sie kompensiert – das sind die richtigen Schritte, wenn man nicht sprechen kann. Der einfachste Weg besteht darin, vor der Situation zu fliehen, in der sich emotionaler Stress manifestiert, und einfach aufzuhören, an diesem Stress teilzunehmen. Danach werden Sie fähig zu denken, wenn Sie denken können, werden Sie fähig zu sprechen und so weiter.

Übrigens, vor Gericht versucht der gegnerische Anwalt, Ihnen das anzutun – jetzt ist klar, warum. Weil er die Fähigkeit hat, Sie so weit zu unterdrücken, dass Sie beispielsweise nicht einmal Ihren Namen aussprechen können. Im wahrsten Sinne des Wortes werden Sie nicht in der Lage sein zu sprechen. Wenn Ihnen das passiert und Sie wissen, dass Sie sich an einem Ort befinden werden, an dem verbale Auseinandersetzungen toben, an einem Ort wie einem Gericht, dann können Sie mit Ihrem Anwalt kommen. Der Anwalt wird sich für Sie einsetzen und den verbalen Angriff stoppen, und zwar auf völlig legale Weise, und der verlorene Zen-Raum wird zu Ihnen zurückkehren. Ich musste zum Beispiel ein paar Mal meine Familie anrufen, der Richter war diesbezüglich recht freundlich, aber der gegnerische Anwalt schrie und schrie mich an, ich brachte nicht einmal ein Wort zu Wort. In diesen Fällen funktioniert für mich der Einsatz eines Mediators am besten. Der Mediator stoppt den ganzen Druck, der kontinuierlich auf Sie einwirkt, Sie finden den nötigen Zen-Raum und damit kehrt die Fähigkeit zum Sprechen zurück. Dies ist ein ganzes Wissensgebiet, in dem es viel zu studieren und viel in sich selbst zu entdecken gibt, und all dies führt zu strategischen Entscheidungen auf hoher Ebene, die für jeden Menschen unterschiedlich sind. Manche Menschen haben die oben beschriebenen Probleme nicht; professionelle Verkäufer haben sie normalerweise nicht. 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 solche Probleme nicht, aber ich schon.

Andrew: Es war unerwartet. Großartig, wir haben bereits viel geredet und es ist Zeit, dieses Interview zu beenden. Wir werden uns auf jeden Fall auf der Konferenz treffen und diesen Dialog fortsetzen können. Wir sehen uns bei Hydra!

Sie können Ihr Gespräch mit Cliff auf der Hydra 2019-Konferenz fortsetzen, die vom 11. bis 12. Juli 2019 in St. Petersburg stattfindet. Er wird mit einem Bericht kommen „Das Azul Hardware Transactional Memory-Erlebnis“. Tickets können erworben werden auf der offiziellen Website.

Source: habr.com

Kommentar hinzufügen