Großes Interview mit Cliff Click, dem Vater der JIT-Kompilierung in Java

Cliff Click ist Cratus 'CTO (IoT-Sensoren zur Prozessverbesserung), der Gründer und Mitbegründer mehrerer Startups (einschließlich Rocket Realtime School, Neurensic und H2O.ai) mit mehreren erfolgreichen Exits. Cliff schrieb seinen ersten Compiler im Alter von 15 Jahren (Pascal für TRS Z-80)! Am bekanntesten für die Arbeit an C2 in Java (Sea of ​​Nodes IR). Dieser Compiler hat der Welt gezeigt, dass JIT qualitativ hochwertigen Code produzieren kann, was zu einem der Faktoren geworden ist, die Java zu einer der wichtigsten modernen Softwareplattformen gemacht haben. Cliff half Azul Systems dann beim Aufbau eines 864-Core-Mainframes mit reiner Java-Software, die GC-Pausen auf einem 500-Gigabyte-Heap für 10 Millisekunden unterstützte. Im Allgemeinen gelang es Cliff, an allen Aspekten der JVM zu arbeiten.

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


  • Übergang zu Optimierungen auf niedriger Ebene
  • Wie man viel Refactoring macht
  • Kostenmodell
  • Low-Level-Optimierungstraining
  • Fallstudien zur Produktivitätsverbesserung
  • Warum erstellen Sie Ihre eigene Programmiersprache?
  • Karriere als Performance Engineer
  • Technische Herausforderungen
  • Ein bisschen über Registerzuordnung und Multicore
  • Die größte Herausforderung im Leben

Interviews durchgeführt von:


  • Andrey Satarin von Amazon Web Services. In seiner Karriere gelang es ihm, in völlig anderen Projekten zu arbeiten: Er testete die verteilte NewSQL-Datenbank in Yandex, das Cloud-Erkennungssystem in Kaspersky Lab, das Mehrbenutzerspiel in Mail.ru und den Geldwechselberechnungsdienst in der Deutschen Bank. Er ist daran interessiert, große Backend- und verteilte Systeme zu testen.
  • Vladimir Sitnikov von Netcracker. Seit zehn Jahren arbeitet er an der Leistung und Skalierbarkeit von NetCracker OS, einer Software, die von Telekommunikationsbetreibern zur Automatisierung von Netzwerk- und Netzwerkgeräteverwaltungsprozessen verwendet wird. Er interessiert sich für Leistungsprobleme bei Java- und Oracle-Datenbanken. Der Autor von mehr als einem Dutzend Leistungsverbesserungen im offiziellen PostgreSQL JDBC-Treiber.

Übergang zu Optimierungen auf niedriger Ebene


Andrei : Sie sind eine berühmte Person in der Welt der JIT-Kompilierung in Java und arbeiten an der Leistung im Allgemeinen, oder?


Cliff : Das war's!


Andrew : Beginnen wir mit allgemeinen Fragen zur Arbeit an der Leistung. Was halten Sie von der Wahl zwischen Optimierungen auf hoher und niedriger Ebene wie Arbeiten auf CPU-Ebene?


Cliff : Es ist einfach. Der schnellste Code wird niemals ausgeführt. Daher müssen Sie immer von einer hohen Ebene ausgehen und an Algorithmen arbeiten. Eine bessere O-Notation schlägt eine schlechtere O-Notation, es sei denn, einige ziemlich große Konstanten greifen ein. Low-Level-Dinge sind die neuesten. Wenn Sie den Rest des Stapels gut genug optimiert haben und noch etwas Interessantes übrig ist, ist dies normalerweise das niedrige Niveau. Aber wie fange ich von einem hohen Niveau an? Wie kann man herausfinden, dass auf hohem Niveau genug Arbeit geleistet wurde? Nun ... auf keinen Fall. Es gibt keine vorgefertigten Rezepte. Sie müssen das Problem verstehen, entscheiden, was Sie tun möchten (um in Zukunft keine unnötigen Schritte zu unternehmen), und dann können Sie einen Profiler aufdecken, der etwas Nützliches sagen kann. Irgendwann verstehen Sie selbst, dass Sie unnötige Dinge losgeworden sind und es Zeit ist, den niedrigen Pegel zu optimieren. Dies ist definitiv eine besondere Art von Kunst. Viele Menschen tun unnötige Dinge, bewegen sich aber so schnell, dass sie keine Zeit haben, sich um die Leistung zu kümmern. Dies gilt jedoch, solange die Frage nicht aufrecht steht. Normalerweise kümmert sich in 99% der Fälle niemand darum, was ich tue, bis eine wichtige Sache, die jemandem wichtig ist, nicht auf den kritischen Pfad gelangt. Und hier fängt jeder an, Sie über das Thema "Warum es von Anfang an nicht perfekt funktioniert hat" zu nerven. Im Allgemeinen gibt es immer etwas zu verbessern. Aber 99% der Zeit haben Sie keine Leads! Sie versuchen nur, etwas zum Laufen zu bringen, und dabei verstehen Sie, worauf es ankommt. Man kann nie im Voraus wissen, dass dieses Stück perfekt gemacht werden muss, daher muss man im Wesentlichen in allem perfekt sein. Und das ist unmöglich, und das tust du nicht. Es gibt immer eine Menge Dinge zu reparieren - und das ist völlig normal.


Wie man viel Refactoring macht


Andrew : Wie arbeitest du an der Leistung? Dies ist ein Querschnittsthema. Mussten Sie beispielsweise an Problemen arbeiten, die sich aus der Überschneidung einer großen Menge vorhandener Funktionen ergeben?


Cliff : Ich versuche das zu vermeiden. Wenn ich weiß, dass die Leistung zu einem Problem wird, denke ich darüber nach, bevor ich mit dem Codieren beginne, insbesondere bei Datenstrukturen. Aber oft entdeckt man das alles viel später. Und dann müssen Sie extreme Maßnahmen ergreifen und das tun, was ich als „umschreiben und erobern“ bezeichne: Sie müssen sich an einem ziemlich großen Stück festhalten. Ein Teil des Codes muss aufgrund von Leistungsproblemen oder etwas anderem noch neu geschrieben werden. Was auch immer der Grund für das Umschreiben des Codes ist, es ist fast immer besser, einen größeren Block als einen kleineren Block neu zu schreiben. In diesem Moment beginnen alle vor Angst zu zittern: "Oh mein Gott, du kannst nicht so viel Code anfassen!" Tatsächlich funktioniert dieser Ansatz jedoch fast immer viel besser. Sie müssen das große Problem sofort aufgreifen, einen großen Kreis darum zeichnen und sagen: Ich werde alles innerhalb des Kreises neu schreiben. Der Rand ist viel kleiner als der Inhalt, der ersetzt werden muss. Und wenn eine solche Abgrenzung der Grenzen es Ihnen ermöglicht, die Arbeit im Inneren perfekt zu erledigen - Sie haben Ihre Hände gelöst, tun, was Sie wollen. Sobald Sie das Problem verstanden haben, ist der Umschreibvorgang viel einfacher. Beißen Sie also einen großen Teil ab!
Wenn Sie in großen Blöcken umschreiben und verstehen, dass die Leistung zu einem Problem wird, können Sie sich sofort Sorgen machen. Normalerweise werden daraus einfache Dinge wie „Daten nicht kopieren, Daten so einfach wie möglich verwalten, verkleinern“. Bei großen Umschreibungen gibt es Standardmethoden zur Verbesserung der Leistung. 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 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 ist nicht ohne Ironie. Ich begann in den Tagen von Acht-Bit-Maschinen zu leben, mein erster Computer arbeitete mit 256 Bytes. Es sind Bytes. Alles war sehr klein. Wir mussten die Anweisungen lesen und sobald wir anfingen, den Stapel der Programmiersprachen zu erweitern, nahmen die Sprachen immer mehr an. Es gab Assembler, dann Basic, dann C, und C übernahm den Job mit vielen Details, wie Registerzuweisung und Befehlsauswahl. Aber dort war alles ziemlich klar, und wenn ich einen Zeiger auf eine Instanz einer Variablen gemacht habe, werde ich geladen, und die Kosten sind für diese Anweisung bekannt. Eisen erzeugt eine bekannte Anzahl von Maschinenzyklen, sodass die Ausführungsgeschwindigkeit verschiedener Teile einfach durch Hinzufügen aller Anweisungen berechnet werden kann, die Sie ausführen wollten. Jeder Vergleich / Test / Zweig / Anruf / Laden / Speichern könnte gefaltet und gesagt werden: Hier haben Sie die Vorlaufzeit. Wenn Sie die Leistung verbessern, achten Sie auf jeden Fall darauf, welche Zahlen kleinen heißen Zyklen entsprechen.
Sobald Sie jedoch zu Java, Python und ähnlichen Dingen wechseln, entfernen Sie sich sehr schnell von Low-Level-Eisen. Was kostet ein Getter Call in Java? Wenn die JIT in HotSpot korrekt inline ist , wird sie geladen, andernfalls handelt es sich um einen Funktionsaufruf. Da die Herausforderung in der Hot-Loop liegt, werden alle anderen Optimierungen in dieser Loop rückgängig gemacht. Daher wird der reale Wert viel größer sein. Und Sie verlieren sofort die Fähigkeit, einen Code zu betrachten und zu verstehen, dass wir ihn in Bezug auf die Prozessortaktrate, den verwendeten Speicher und den Cache ausführen sollten. All dies wird nur dann interessant, wenn Sie sich wirklich in der Leistung betrunken haben.
Jetzt befinden wir uns in einer Situation, in der die Geschwindigkeit von Prozessoren seit einem Jahrzehnt fast nicht mehr gestiegen ist. Alte Zeiten sind zurück! Sie können sich nicht mehr auf eine gute Single-Thread-Leistung verlassen. Aber wenn Sie plötzlich parallel arbeiten - es ist wahnsinnig schwierig, sehen Sie alle als James Bond. Die zehnfache Beschleunigung tritt hier normalerweise an den Stellen auf, an denen jemand etwas schlägt. Parallelität erfordert viel Arbeit. Um die gleiche zehnfache Beschleunigung zu erhalten, müssen Sie das Kostenmodell verstehen. Was und wie viel es kostet. Und dafür müssen Sie verstehen, wie die Zunge auf dem darunter liegenden Eisen liegt.
Martin Thompson hat ein großartiges Wort für seinen Blog über mechanische Sympathie ! Sie müssen verstehen, was Eisen tun wird, wie genau es es tun wird und warum es im Allgemeinen das tut, was es tut. Auf diese Weise können Sie ganz einfach Anweisungen lesen und herausfinden, wo die Ausführungszeit fließt. Wenn Sie nicht über die entsprechende Ausbildung verfügen, suchen Sie nur nach einer schwarzen Katze in einem dunklen Raum. Ich sehe ständig Leute, die die Leistung optimieren und keine Ahnung haben, was zum Teufel sie tun. Sie sind sehr gequält und gehen nicht wirklich irgendwohin. Und wenn ich den gleichen Code nehme, dort ein paar kleine Hacks abschalte und fünf- oder zehnmal beschleunige, sind sie so: Nun, es ist so unehrlich, wir wussten bereits, dass Sie besser sind. Es ist erstaunlich. Worüber spreche ich? Das Kostenmodell handelt davon, welchen Code Sie schreiben und wie schnell er im Gesamtbild durchschnittlich funktioniert.


Andrew : Und wie hält man so ein Volumen in deinem Kopf? Wird dies durch mehr Erfahrung erreicht oder? Wo werden solche Erfahrungen gesammelt?


Cliff : Nun, meine Erfahrung war nicht der einfachste Weg. Ich habe in Assembler zu einer Zeit programmiert, als es möglich war, jede einzelne Anweisung zu verstehen. Es klingt albern, aber seitdem ist in meinem Kopf, in meiner Erinnerung, der Z80-Befehlssatz für immer geblieben. Ich erinnere mich eine Minute nach dem Gespräch nicht an die Namen von Personen, aber ich erinnere mich an den Code, der vor 40 Jahren geschrieben wurde. Komisch, es sieht aus wie ein " Learned Idiot " -Syndrom.


Low-Level-Optimierungstraining


Andrew : Gibt es einen einfacheren Weg, um ins Geschäft zu kommen?


Cliff : Ja und nein. Das Eisen, das wir alle verwenden, hat sich in dieser Zeit nicht so sehr verändert. Mit Ausnahme von Arm-Smartphones verwendet jeder x86. Wenn Sie keine Hardcore-Einbettung vornehmen, haben Sie das Gleiche. Ok, als nächstes. Auch die Anweisungen haben sich seit Jahrhunderten nicht geändert. Sie müssen etwas in Assembler schreiben. Ein bisschen, aber genug, um zu verstehen. Du lächelst, aber ich meine es absolut ernst. Sie müssen die Entsprechung von Sprache und Eisen verstehen. Danach müssen Sie gehen, ein wenig pinkeln und einen kleinen Spielzeug-Compiler für eine kleine Spielzeugsprache erstellen. "Spielzeug" bedeutet, dass Sie es in angemessener Zeit schaffen müssen. Es kann sehr einfach sein, aber es muss Anweisungen generieren. Durch das Generieren von Anweisungen können wir das Kostenmodell für die Brücke zwischen dem Code auf hoher Ebene, auf den jeder schreibt, und dem Maschinencode, der auf Hardware ausgeführt wird, verstehen. Diese Korrespondenz wird zum Zeitpunkt des Schreibens des Compilers im Gehirn verbrannt. Selbst der einfachste Compiler. Danach können Sie sich mit Java und der Tatsache befassen, dass es eine tiefere semantische Lücke aufweist, und es ist viel schwieriger, Brücken darüber zu bauen. In Java ist es viel schwieriger zu verstehen, ob sich unsere Brücke als gut oder schlecht herausgestellt hat, wodurch sie auseinander fällt und nicht. Aber Sie brauchen einen Ausgangspunkt, wenn Sie sich den Code ansehen und verstehen: „Ja, dieser Getter muss jedes Mal inline sein“. Und dann stellt sich heraus, dass dies manchmal passiert, mit Ausnahme der Situation, in der die Methode zu groß wird und die JIT beginnt, alles zu inline. Die Leistung solcher Orte kann sofort vorhergesagt werden. Normalerweise funktionieren Getter gut, aber dann sehen Sie sich die großen Hot-Loops an und stellen fest, dass einige Funktionsaufrufe darin schweben, die nicht wissen, was sie tun. Dies ist das Problem bei der weit verbreiteten Verwendung von Gettern, der Grund, warum sie nicht inline sind - es ist nicht klar, ob dies ein Getter ist. Wenn Sie eine superkleine Codebasis haben, können Sie sich einfach daran erinnern und dann sagen: Dies ist ein Getter, aber dies 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 24% unserer Zeit in einem Zyklus verloren haben. Um zu verstehen, was dieser Zyklus bewirkt, müssen wir uns jede Funktion im Inneren ansehen. Es ist unmöglich, dies zu verstehen, ohne die Funktion zu studieren, und dies verlangsamt den Prozess des Verstehens ernsthaft. Deshalb benutze ich keine Getter und Setter, ich bin auf ein neues Level gegangen!
Woher bekommen Sie das Kostenmodell? Natürlich können Sie etwas lesen ... Aber ich denke, der beste Weg ist zu handeln. Erstellen Sie einen kleinen Compiler, und dies ist der beste Weg, um das Kostenmodell zu realisieren und in Ihren eigenen Kopf zu integrieren. Ein kleiner Compiler, der für die Mikrowellenprogrammierung geeignet ist, ist eine Aufgabe für Anfänger. Nun, ich meine, wenn Sie bereits Programmierkenntnisse haben, sollten diese ausreichen. All diese Dinge sind wie das Parsen eines Strings, für den Sie eine Art algebraischen Ausdruck haben, die Anweisungen für mathematische Operationen in der richtigen Reihenfolge herausziehen und die richtigen Werte aus den Registern entnehmen - all dies wird gleichzeitig durchgeführt. Und während Sie es tun, wird es im Gehirn eingeprägt. Ich denke, jeder weiß, was der Compiler macht. Und dies gibt ein Verständnis für das Kostenmodell.


Fallstudien zur Produktivitätsverbesserung


Andrew : Worauf sollte man bei der Arbeit an der Leistung noch achten?


Cliff : Datenstrukturen. Übrigens, ja, ich habe diese Klassen schon lange nicht mehr unterrichtet ... Rocket School . Es war lustig, aber es hat so viel Mühe gekostet, zu investieren, und ich habe auch Leben! Okay. In einer der großen und interessanten Klassen, „Wohin geht Ihre Leistung?“, Gab ich den Schülern ein Beispiel: Zweieinhalb Gigabyte Fintech-Daten wurden aus einer CSV-Datei gelesen, und dann mussten wir die Anzahl der verkauften Produkte berechnen. Regelmäßige Zeckenmarktdaten. UDP-Pakete wurden seit den 70er Jahren in das Textformat konvertiert. Die Chicago Mercantile Exchange ist alles Mögliche wie Butter, Mais, Sojabohnen und dergleichen. Es war notwendig, diese Produkte, die Anzahl der Transaktionen, das durchschnittliche Volumen der Bewegung von Geldern und Waren usw. zu zählen. Dies ist eine ziemlich einfache Handelsmathematik: Suchen Sie den Produktcode (dies sind 1-2 Zeichen in der Hash-Tabelle), ermitteln Sie den Betrag, fügen Sie ihn zu einem der Deal-Sets hinzu, fügen Sie Volumen, Mehrwert und einige andere Dinge hinzu. Sehr einfache Mathematik. Die Implementierung des Spielzeugs war sehr einfach: Alles liegt in der Datei, ich lese die Datei und bewege mich darin, trenne die einzelnen Einträge in Java-Strings, suche nach den notwendigen Dingen in ihnen und falte sie gemäß der oben beschriebenen Mathematik. Und es funktioniert mit niedriger Geschwindigkeit.


Bei diesem Ansatz ist alles offensichtlich, was passiert, und paralleles Rechnen hilft hier nicht weiter, oder? Es stellt sich heraus, dass eine Verfünffachung der Produktivität nur durch Auswahl der richtigen Datenstrukturen erreicht werden kann. Und das überrascht sogar erfahrene Programmierer! In meinem speziellen Fall bestand der Trick darin, dass Sie keine Speicherzuweisungen in einer Hot-Loop durchführen sollten. Nun, das ist nicht die ganze Wahrheit, aber im Allgemeinen - Sie sollten "einmal in X" nicht hervorheben, wenn X groß genug ist. Wenn X zweieinhalb Gigabyte beträgt, sollten Sie nichts "einmal pro Buchstabe" oder "einmal pro Zeile" oder "einmal pro Feld" zuweisen, nichts dergleichen. Genau das braucht Zeit. Wie funktioniert es überhaupt? Stellen Sie sich vor, Sie rufen String.split() oder BufferedReader.readLine() . Readline eine Zeile aus einer Reihe von Bytes, die über das Netzwerk Readline , einmal für jede Zeile und für jede von Hunderten von Millionen Zeilen. Ich nehme diese Zeile, analysiere und werfe sie weg. Warum wegwerfen - nun, 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, dh bereits 5.4G, und ich brauche sie nicht mehr, daher werden sie verworfen. Wenn Sie sich die Speicherbandbreite ansehen, laden wir 2,7 G, die den Speicher und den Speicherbus im Prozessor durchlaufen, und dann wird doppelt so viel an die im Speicher liegende Zeile gesendet, und all dies wird beim Erstellen jeder neuen Zeile gerieben. Aber ich muss es lesen, das Eisen liest es, auch wenn dann alles gerieben wird. Und ich muss es aufschreiben, weil ich die Zeile erstellt habe und die Caches voll waren - der Cache passt nicht für 2.7G. Insgesamt lese ich für jedes gelesene Byte zwei weitere Bytes und schreibe zwei zusätzliche Bytes. Infolgedessen haben sie ein Verhältnis von 4: 1 - in diesem Verhältnis verschwenden wir Speicherbandbreite. Und dann stellt sich heraus, dass, wenn ich String.split() mache, dies nicht das letzte Mal mache, möglicherweise weitere 6-7 Felder darin sind. Daher führt der klassische CSV-Lesecode, gefolgt von Zeilenanalyse, zu einem Verlust der Speicherbandbreite im Bereich von 14: 1 im Vergleich zu dem, was Sie wirklich gerne hätten. Wenn Sie diese Sekrete wegwerfen, können Sie eine fünffache Beschleunigung erzielen.


Und das ist nicht sehr schwierig. Wenn Sie den Code aus dem richtigen Winkel betrachten, wird alles ganz einfach, sobald Sie die Essenz des Problems erkennen. Hören Sie nicht einmal auf, Speicher zuzuweisen: Das einzige Problem besteht darin, dass Sie etwas zuweisen, das sofort stirbt und eine wichtige Ressource auf dem Weg verbrennt, in diesem Fall die Speicherbandbreite. All dies führt zu einem Rückgang der Produktivität. Unter x86 müssen Sie normalerweise die Prozessortakte aktiv brennen, und hier haben Sie den gesamten Speicher viel früher gebrannt. Lösung - Sie müssen die Entladungsmenge reduzieren.
Ein weiterer Teil des Problems besteht darin, dass Sie, wenn Sie den Profiler nach Beendigung des Speicherstreifens starten, in dem Moment, in dem dies geschieht, normalerweise auf die Rückkehr des Caches warten, da er voller Müll ist, den Sie gerade mit all diesen Zeilen erzeugt haben. Daher wird jede Lade- oder Speicheroperation langsam, da sie zu Fehlern im Cache führt - der gesamte Cache wurde langsam und wartet darauf, dass der Müll ihn verlässt. Daher zeigt der Profiler nur warmes, zufälliges Rauschen an, das während des gesamten Zyklus verschmiert ist - es gibt keine separate Hot-Anweisung oder Stelle im Code. Nur der Lärm. Und wenn Sie sich die GC-Zyklen ansehen, sind sie alle Young Generation und superschnell - maximal Mikrosekunden oder Millisekunden. Immerhin stirbt all diese Erinnerung sofort. Sie weisen Milliarden von Gigabyte zu, und es schneidet sie und schneidet und schneidet sie erneut. All dies geschieht sehr schnell. Es stellt sich heraus, dass es billige GC-Zyklen gibt, warmes Rauschen während des gesamten Zyklus, aber wir wollen eine 5-fache Beschleunigung. In diesem Moment sollte sich etwas in meinem Kopf schließen und klingen: "Warum so ?!" Der Bandbreitenüberlauf wird im klassischen Debugger nicht angezeigt. Sie müssen den Debugger für den Hardware-Leistungsindikator ausführen und ihn selbst und direkt anzeigen. Und nicht direkt, es kann von diesen drei Symptomen vermutet werden. 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 passiert ist, stellen Sie fest, dass Sie zu viele Objekte erzeugt und den gesamten Speicherstreifen verbrannt haben. Es gibt einen Weg, dies herauszufinden, aber es ist nicht offensichtlich.


Das Problem liegt in der Datenstruktur: Die bloße Struktur hinter allem, was passiert, ist zu groß, es ist 2,7 G auf der Festplatte, daher ist das Erstellen einer Kopie dieses Dings sehr unerwünscht - ich möchte es sofort aus dem Netzwerkbytepuffer in die Register laden, um nicht in die Zeichenfolge zu lesen und zu schreiben fünfmal hin und her. Leider bietet Ihnen Java standardmäßig keine solche Bibliothek als Teil des JDK. Aber das ist trivial, oder? Tatsächlich sind dies 5-10 Codezeilen, die zum Implementieren Ihres eigenen gepufferten Zeilenladers verwendet werden, der das Verhalten der Zeilenklasse wiederholt und gleichzeitig einen Wrapper um den zugrunde liegenden Bytepuffer darstellt. Infolgedessen stellt sich heraus, dass Sie fast wie mit Zeichenfolgen arbeiten, aber tatsächlich gibt es bewegliche Zeiger auf den Puffer, und Rohbytes werden nirgendwo kopiert, und daher werden dieselben Puffer immer wieder verwendet, und das Betriebssystem übernimmt dies gerne Dinge, für die es bestimmt ist, wie das versteckte doppelte Puffern dieser Bytepuffer, und Sie selbst mahlen keinen endlosen Strom unnötiger Daten mehr. Übrigens, Sie verstehen, wenn Sie mit dem GC arbeiten, ist garantiert, dass nicht jede Speicherzuordnung für den Prozessor nach dem letzten GC-Zyklus sichtbar ist? Daher kann sich all dies in keiner Weise im Cache befinden, und dann tritt ein zu 100% garantierter Fehler auf. Wenn Sie mit einem Zeiger auf x86 arbeiten, dauert das Subtrahieren eines Registers vom Speicher 1-2 Zyklen. Sobald dies geschieht, zahlen Sie, zahlen, zahlen, da sich der Speicher ausschließlich in NEUN Caches befindet - und dies sind die Kosten für die Zuweisung von Speicher. Barwert.


Mit anderen Worten, Datenstrukturen sind am schwierigsten zu ändern. Und sobald Sie feststellen, dass Sie die falsche Datenstruktur gewählt haben, die die Produktivität in Zukunft beeinträchtigen wird, müssen Sie normalerweise die wesentlichen Arbeiten ankurbeln. Wenn Sie dies jedoch nicht tun, wird es schlimmer. Zunächst müssen Sie über Datenstrukturen nachdenken, dies ist wichtig. Die Hauptkosten liegen hier bei den fett gedruckten Datenstrukturen, die sie im Stil von "Ich habe die Datenstruktur X in die Datenstruktur Y kopiert, weil mir die Form besser gefällt" verwenden. Aber der Kopiervorgang (der billig erscheint) verbraucht tatsächlich einen Speicherstreifen und hier ist die gesamte verlorene Laufzeit begraben. Wenn ich einen riesigen String mit JSON habe und ihn in einen strukturierten DOM-Baum von POJO oder ähnlichem verwandeln möchte, wird sich das Parsen dieses Strings und das Erstellen eines POJO und ein neuer Aufruf von POJO in Zukunft als wertlos herausstellen - das ist keine billige Sache. Außer wenn Sie viel häufiger mit POJO als mit einer Leitung arbeiten. Stattdessen können Sie sofort versuchen, die Zeichenfolge zu entschlüsseln und nur das herauszuholen, was Sie benötigen, ohne sie in POJOs umzuwandeln. Wenn all dies auf dem Pfad geschieht, von dem aus maximale Leistung erforderlich ist, keine POJOs für Sie - Sie müssen sich irgendwie direkt in die Leitung graben.


Warum erstellen Sie Ihre eigene Programmiersprache?


Andrei : Sie sagten, um das Kostenmodell zu verstehen, müssen Sie Ihre eigene kleine Sprache schreiben ...


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


Andrei : Übrigens, soweit ich weiß, experimentieren Sie damit, Ihre eigenen Sprachen zu erstellen. Warum?


Cliff : Weil ich kann! Ich bin halb im Ruhestand, das ist also mein Hobby. Ich habe mein ganzes Leben lang die Sprachen anderer implementiert. Ich habe auch hart am Codierungsstil gearbeitet. Und auch, weil ich Probleme in anderen Sprachen sehe. Ich sehe, dass es bessere Möglichkeiten gibt, die üblichen Dinge zu tun. Und ich würde sie benutzen. Ich habe es einfach satt, Probleme in mir selbst, in Java, in Python und in jeder anderen Sprache zu sehen. Ich schreibe über React Native, JavaScript und Elm als Hobby, bei dem es nicht um Ruhestand geht, sondern um aktive Arbeit. Außerdem schreibe ich 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 kann etwas für sich und Sie können versuchen, all diese Chips zusammenzubringen. Also studiere ich die Dinge, die für mich interessant sind, das Verhalten der Sprache und versuche, eine vernünftige Semantik zu finden. Und bis jetzt mache ich es! Im Moment habe ich Probleme mit der Semantik des Speichers, weil ich sie sowohl in C als auch in Java haben möchte und ein starkes Speichermodell und eine starke Speichersemantik für Ladevorgänge und Speicher erhalten möchte. Haben Sie gleichzeitig eine automatische Typinferenz wie in Haskell. Hier versuche ich, Haskell-ähnliche Typinferenz mit Speicher zu mischen, der sowohl in C als auch in Java funktioniert. Ich mache das zum Beispiel seit 2-3 Monaten.


Andrei : Wenn Sie eine Sprache bauen, die bessere Aspekte aus anderen Sprachen übernimmt, haben Sie gedacht, dass jemand das Gegenteil tun würde: Nehmen Sie Ihre Ideen und verwenden Sie sie?


Cliff : So erscheinen neue Sprachen! Warum ähnelt Java C? Weil C eine gute Syntax hatte, die jeder verstand, und Java von dieser Syntax inspiriert war, die Typensicherheit hinzufügte, die Grenzen von Arrays und GC überprüfte und einige Dinge gegenüber C verbesserte. Sie fügten ihre eigene hinzu. Aber sie waren ziemlich inspiriert, oder? Jeder steht auf den Schultern der Riesen, die vor Ihnen kamen - so werden Fortschritte erzielt.


Andrew : Soweit ich weiß, ist Ihre Sprache in Bezug auf die Speichernutzung sicher. Haben Sie jemals daran gedacht, so etwas wie einen Leihprüfer von Rust zu implementieren? Du hast ihn angesehen, wie hat er dich gemocht?


Cliff : Nun, ich schreibe seit Ewigkeiten C, mit all diesen Malloc und Free, und ich verwalte die Lebensdauer manuell. Sie wissen, dass 90-95% einer manuell verwalteten Lebenszeit dieselbe Struktur haben. Und es ist sehr, sehr schmerzhaft, dies manuell zu tun. Ich möchte, dass der Compiler einfach sagt, was dort passiert und was Sie mit Ihren Aktionen erreicht haben. Für einige Dinge erledigt ein Kreditprüfer dies sofort. Und er sollte automatisch Informationen anzeigen, alles verstehen und mich nicht einmal belasten, um dieses Verständnis auszudrücken. Er sollte mindestens eine lokale Escape-Analyse durchführen. Nur wenn dies nicht gelingt, müssen Sie Typanmerkungen hinzufügen, die die Lebensdauer beschreiben. Ein solches Schema ist viel komplizierter als ein Leihprüfer oder ein vorhandener Speicherprüfer. Die Wahl zwischen "alles ist in Ordnung" und "ich habe nichts verstanden" - nein, es muss etwas Besseres geben.
Als Person, die viel C-Code geschrieben hat, denke ich, dass die Unterstützung der automatischen Lebensdauerkontrolle das Wichtigste ist. Und ich habe es satt, wie viel Java Speicher verwendet, und die Hauptbeschwerde liegt in GC. Wenn Sie Speicher in Java zuweisen, geben Sie nicht den Speicher zurück, der in der letzten GC-Schleife lokal war. In Sprachen mit genauerer Speicherverwaltung ist dies nicht der Fall. Wenn Sie malloc aufrufen, erhalten Sie sofort den Speicher, der normalerweise nur verwendet wurde. Normalerweise machen Sie einige vorübergehende Dinge mit Ihrem Gedächtnis und bringen es sofort zurück. Und sie kehrt sofort zum Malloc-Pool zurück und der nächste Malloc-Zyklus zieht sie wieder heraus. Daher wird die tatsächliche Speichernutzung zu einem bestimmten Zeitpunkt auf eine Reihe lebender Objekte sowie auf Lecks reduziert. Und wenn nicht alles auf Ihre unanständige Weise fließt, wird der größte Teil des Speichers in Caches und im Prozessor gespeichert und funktioniert schnell. Aber es erfordert viel manuelle Speicherverwaltung mit malloc und free, aufgerufen in der richtigen Reihenfolge, am richtigen Ort. Rust selbst kann dies korrekt handhaben und bietet in vielen Fällen eine noch höhere Leistung, da der Speicherverbrauch nur auf die aktuellen Berechnungen beschränkt wird - anstatt auf den nächsten GC-Zyklus zu warten, um Speicher freizugeben. Als Ergebnis haben wir einen sehr interessanten Weg gefunden, um die Leistung zu verbessern. Und ziemlich mächtig - in dem Sinne, dass ich solche Dinge bei der Verarbeitung von Daten für die Fintech getan habe, und dies ermöglichte mir, fünfmal zu beschleunigen. Dies ist eine ziemlich große Beschleunigung, insbesondere in einer Welt, in der Prozessoren nicht schneller werden und wir alle weiterhin auf Verbesserungen warten.


Karriere als Performance Engineer


Andrew : Ich möchte auch nach der Karriere als Ganzes fragen. Sie wurden berühmt dafür, dass Sie bei JIT bei HotSpot gearbeitet haben und dann zu Azul gewechselt sind - und dies ist auch eine JVM-Firma. Aber sie beschäftigten sich bereits mehr mit Eisen als mit Software. Und dann plötzlich auf Big Data und maschinelles Lernen umgestellt und dann auf Betrugserkennung. Wie ist es passiert? Dies sind sehr unterschiedliche Entwicklungsbereiche.


Cliff : Ich programmiere schon seit einiger Zeit und habe es geschafft, in sehr unterschiedlichen Klassen einzuchecken. Und wenn Leute sagen: "Oh, du bist derjenige, der JIT für Java gemacht hat!", Ist das immer lustig. Zuvor beschäftigte ich mich jedoch mit dem PostScript-Klon - der Sprache, die Apple einst für seine Laserdrucker verwendete. Und davor hat er die Forth-Sprache implementiert. Ich denke, das gemeinsame Thema für mich ist die Entwicklung von Werkzeugen. Mein ganzes Leben lang habe ich Werkzeuge 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 Entwicklung des Betriebssystems beteiligt, die trivial begannen, aber im Laufe der Zeit wurde alles kompliziert und kompliziert. Das Hauptthema ist jedoch die Entwicklung von Werkzeugen. Ein großer Teil des Lebens ging zwischen Azul und Sun, und es ging um Java. Aber als ich mit Big Data und maschinellem Lernen anfing, setzte ich meinen Hut wieder auf und sagte: „Oh, und jetzt haben wir ein nicht triviales Problem, und hier passieren viele interessante Dinge und Leute, die etwas tun.“ Dies ist ein großartiger Entwicklungspfad, der sich lohnt.


Ja, ich mag verteiltes Rechnen wirklich. Mein erster Job war als Student in C bei einem Werbeprojekt. Diese wurden auf Zilog Z80-Chips verteilt, die Daten für die analoge optische Texterkennung sammelten, die von einem echten analogen Analysator erzeugt wurden. Es war ein cooles und völlig anormales Thema. Aber es gab Probleme, ein Teil wurde nicht richtig erkannt, so dass es notwendig war, ein Bild zu machen und es einer Person zu zeigen, die bereits mit den Augen las und informierte, was dort gesagt wurde, und daher gab es Datenjugger, und dieser Job hatte seine eigene Sprache . Es gab ein Backend, das all dies erledigte - parallel zum Z80 mit vt100-Terminals - eines pro Person, und es gab ein paralleles Programmiermodell auf dem Z80. Ein bestimmtes gemeinsames Stück Speicher, das von allen Z80 innerhalb einer Sternkonfiguration gemeinsam genutzt wird. Die Rückwandplatine wurde gemeinsam genutzt, und die Hälfte des Arbeitsspeichers wurde im Netzwerk gemeinsam genutzt, und die andere Hälfte war privat oder wurde für etwas anderes ausgegeben. Ein sinnvoll komplexes parallel verteiltes System mit gemeinsam genutztem ... semi-gemeinsam genutztem Speicher. Als es war ... Schon nicht zu erinnern, irgendwo Mitte der 80er Jahre. Vor ziemlich langer Zeit.
Ja, wir gehen davon aus, dass 30 Jahre eine ziemlich lange Zeit sind. Aufgaben im Zusammenhang mit verteiltem Computing gibt es schon seit langer Zeit, Menschen haben lange mit Beowulf- Clustern gekämpft. Solche Cluster sehen aus wie ... Zum Beispiel: Es gibt Ethernet und Ihr schnelles x86 ist mit diesem Ethernet verbunden, und jetzt möchten Sie gefälschten gemeinsamen Speicher erhalten, da dann niemand das Codieren von verteiltem Computing durchführen konnte. Es war zu kompliziert und daher gefälschter gemeinsamer Speicher mit Schutz x86-Speicherseiten, und wenn Sie auf diese Seite geschrieben haben, haben wir den anderen Prozessoren mitgeteilt, dass sie von Ihnen heruntergeladen werden müssen, wenn sie Zugriff auf denselben gemeinsam genutzten Speicher haben. Daher wird so etwas wie ein Protokoll zur Unterstützung der Cache-Kohärenz angezeigt und Software dafür. Interessantes Konzept. Das eigentliche Problem war natürlich anders. All dies hat funktioniert, aber Sie haben schnell Leistungsprobleme bekommen, weil niemand die Leistungsmodelle gut genug verstanden hat - welche Speicherzugriffsmuster gibt es, wie Sie sicherstellen können, dass die Knoten sich nicht endlos anpingen und so weiter.


In H2O habe ich mir Folgendes ausgedacht: Die Entwickler selbst sind dafür verantwortlich zu bestimmen, wo die Parallelität verborgen ist und wo nicht. Ich habe mir ein solches Codierungsmodell ausgedacht, dass das Schreiben von Hochleistungscode einfach und unkompliziert war. Das Schreiben von langsam laufendem Code ist jedoch schwierig, es wird schlecht aussehen. Sie müssen ernsthaft versuchen, langsamen Code zu schreiben, Sie müssen nicht standardmäßige Methoden verwenden. Der Bremscode ist auf einen Blick sichtbar. Infolgedessen wird normalerweise Code geschrieben, der schnell funktioniert. Sie müssen jedoch herausfinden, was bei gemeinsamem Speicher zu tun ist. All dies ist an große Arrays gebunden und das Verhalten dort ist ähnlich wie bei nichtflüchtigen großen Arrays in parallelem Java. Ich meine, stellen Sie sich vor, zwei Threads schreiben in ein paralleles Array, einer gewinnt und der andere verliert, und Sie wissen nicht, welcher von ihnen wer ist. Wenn sie nicht volatil sind, kann die Reihenfolge alles sein - und es funktioniert wirklich gut. Die Leute kümmern sich wirklich um die Reihenfolge der Operationen, sie stellen die Volatilität richtig ein und sie erwarten Speicherprobleme an den richtigen Stellen. Andernfalls würden sie einfach Code in Form von Zyklen 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. In H2O ist dies jedoch weder Java noch Scala. Sie können es als "Java minus minus" betrachten, wenn Sie möchten. Dies ist ein sehr verständlicher Programmierstil, der dem Schreiben von einfachem C- oder Java-Code mit Schleifen und Arrays ähnelt. Gleichzeitig kann der Speicher mit Terabyte verarbeitet werden. Ich benutze immer noch H2O. – , . Big Data , H2O.



: ?


: ? , – .
. . , , , , . Sun, , , , . , , . , C1, , – . , . , x86- , , 5-10 , 50 .


, , , , C. , , - , C . C, C . , , C, - … , . , . , , . , , 5% . - – , « », , . : , , . . , – , . , . - – . , , ( , ), , , . , , , .


, , , , , , . , , , - . , , , . , , , , . , : , . , , - : , , - , . – , , – ! – , . Java. Java , , , , – , « ». , , . , Java C . – Java, C , , , . , – , . , . , , . : .



: - . , , - , ?


: ! – , NP- - . , ? . , Ahead of Time – . - . , , – , ! – , . , , . . ? , : , , - ! - , . . , , . : - , - . , , . , , , , - . ! , , , – . . NP- .


: , – . , , , , …


: . «». . , . – , , , ( , ). , - . , , , . , , . , . , , . , , . , , - , – . – . , GC, , , , – , . , . , , . , – , ? , .


: , ? ?


: GPU , !


: . ?


: , - Azul. , . . H2O , . , GPU. ? , Azul, : – .



: ?


: , … . , . , , , , . , , . , Java C1 C2 – . , Java – . , , – . … . - , Sun, … , , . , . , . … … , . , , . . - , : . , , , , , , . , . . , . « , , ». : «!». , , , : , .


– , , , . . , , , , . , Java JIT, C2. , – . , – ! . , , , , , , . . . , . , , , , : , , . , – . , , - . : « ?». , . , , : , , – ? , . , , , , , , - .


: , -. ?


: , , . – . . , . . . : , , - – . . , , – , . , , , , - , . , . , , - . , , – , .
, . , – , , . , . , – . , . , , « », , – , , , , . , , « ».


. . - , , «»: , – . – . , , . «, -, , ». , : , . , , . Das ist schlecht. – , . , ? , ? ? , ? . , . – . . , . – – , . , « » . : «--», : «, !» . . , , , , . , . , . , – , . – , . , , , .


, – , . , , . , . , , , , . , , . , , , , . . , , , . , , , , . , , , . , – , , , . , .


: … . , . . Hydra!


Hydra 2019, 11-12 2019 -. «The Azul Hardware Transactional Memory experience» . .

Source: https://habr.com/ru/post/de458718/


All Articles