Kotlin kompilieren: JetBrains VS ANTLR VS JavaCC


Wie schnell analysiert Kotlin und was macht es aus? JavaCC oder ANTLR? Sind JetBrains-Quellcodes geeignet?

Vergleichen, phantasieren und wundern.

tl; dr


JetBrains sind zu schwer zu ziehen, ANTLR ist ein Hype, aber unerwartet langsam, und JavaCC ist zu früh, um es abzuschreiben.

Analysieren einer einfachen Kotlin-Datei mit drei verschiedenen Implementierungen:
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8

Ein schöner sonniger Tag ...


Ich beschloss, einen Übersetzer in GLSL aus einer geeigneten Sprache zu erstellen. Die Idee war, Shader direkt in die Idee zu programmieren und "kostenlose" IDE-Unterstützung zu erhalten - Syntax, Debug und Unit-Tests. Es stellte sich wirklich sehr praktisch heraus .

Seitdem ist die Idee, Kotlin zu verwenden, geblieben - Sie können den Namen vec3 darin verwenden, er ist strenger und in der IDE bequemer. Darüber hinaus ist es Hype. Obwohl dies aus Sicht meines internen Managers alles unzureichende Gründe sind, kam die Idee so oft zurück, dass ich mich entschied, sie einfach durch Implementierung loszuwerden.

Warum nicht Java? Es gibt keine Operatorüberladung, daher unterscheidet sich die Syntax der Vektorarithmetik zu stark von der, die Sie in Game Dev gewohnt sind

Jetbrains


Die Jungs von JetBrains haben ihren Compiler-Code auf den Github hochgeladen. Wie man es benutzt, können Sie hier und hier sehen .

Zuerst habe ich ihren Parser zusammen mit dem Analysator verwendet, da Sie zum Übersetzen in eine andere Sprache wissen müssen, welcher Typ die Variable ist, ohne den Typ val x = vec3() explizit anzugeben. Hier ist der Typ für den Leser offensichtlich, aber im AST sind diese Informationen nicht so einfach zu erhalten, insbesondere wenn sich rechts eine andere Variable oder ein Funktionsaufruf befindet.

Hier war ich enttäuscht. Der erste Start des Parsers für eine primitive Datei dauert 3 Sekunden (DREI SEKUNDEN).

Kotlin JetBrains parser
first call elapsed : 3254.482ms
min time in next 10 calls: 70.071ms
min time in next 100 calls: 29.973ms
min time in next 1000 calls: 16.655ms
Whole time for 1111 calls: 40.888756 seconds

Eine solche Zeit hat die folgenden offensichtlichen Unannehmlichkeiten:

  1. weil es plus drei Sekunden dauert, um ein Spiel oder eine Anwendung zu starten.
  2. Während der Entwicklung verwende ich eine Hot-Shader-Überladung und sehe das Ergebnis sofort nach dem Ändern des Codes.
  3. Ich starte die Anwendung oft neu und bin froh, dass sie schnell genug startet (ein oder zwei Sekunden).

Plus drei Sekunden zum Aufwärmen des Parsers - das ist nicht akzeptabel. Natürlich wurde sofort klar, dass bei nachfolgenden Aufrufen die Analysezeit auf 50 ms und sogar auf 20 ms abfällt, was die Unannehmlichkeit Nr. 2 aus dem Ausdruck (fast) beseitigt. Aber die anderen beiden gehen nirgendwo hin. Zusätzlich sind 50 ms pro Datei plus 2500 ms pro 50 Dateien (ein Shader besteht aus 1-2 Dateien). Was ist, wenn es Android ist? (Hier geht es nur um Zeit.)

Bemerkenswert ist die verrückte Arbeit von JIT. Die Analysezeit für eine einfache Datei sinkt von 70 ms auf 16 ms. Dies bedeutet zum einen, dass die JIT selbst Ressourcen verbraucht, und zum anderen kann das Ergebnis einer anderen JVM sehr unterschiedlich sein.

Um herauszufinden, woher diese Zahlen stammen, gab es eine Option: Verwenden Sie den Parser ohne Analysator. Schließlich muss ich nur die Typen anordnen, und dies kann relativ einfach durchgeführt werden, während der JetBrains-Analysator etwas viel Komplexeres ausführt und viel mehr Informationen sammelt. Und dann sinkt die Startzeit um die Hälfte (aber fast eineinhalb Sekunden sind immer noch anständig), und die Zeit für nachfolgende Anrufe ist bereits viel interessanter - von 8 ms in den ersten zehn auf 0,9 ms irgendwo in den Tausenden.

Kotlin JetBrains parser (without analyzer) ()
first call elapsed : 1423.731ms
min time in next 10 calls: 8.275ms
min time in next 100 calls: 2.323ms
min time in next 1000 calls: 0.974ms
Whole time for 1111 calls: 3.6884801 seconds
()
first call elapsed : 1423.731ms
min time in next 10 calls: 8.275ms
min time in next 100 calls: 2.323ms
min time in next 1000 calls: 0.974ms
Whole time for 1111 calls: 3.6884801 seconds

Ich musste genau solche Zahlen sammeln. Die erste Startzeit ist wichtig, wenn die ersten Shader geladen werden. Dies ist wichtig, da Sie den Benutzer hier nicht ablenken können, während die Shader im Hintergrund geladen sind. Er wartet nur. Ein Rückgang der Laufzeit ist wichtig, um die Dynamik selbst zu sehen, wie JIT funktioniert und wie effizient wir Shader in eine warme Anwendung laden können.

Der Hauptgrund, sich hauptsächlich mit dem JetBrains-Parser zu befassen, war der Wunsch, den Typ zu verwenden. Da das Ablehnen jedoch zur besprochenen Option wird, können Sie versuchen, andere Parser zu verwenden. Darüber hinaus sind Nicht-JetBrains höchstwahrscheinlich viel kleiner, weniger umweltintensiv und einfacher durch die Unterstützung und Einbeziehung von Code in das Projekt.

ANTLR


Es gab keinen Parser auf JavaCC, aber auf dem Hype ANTLR gibt es erwartungsgemäß ( eins , zwei ).

Was jedoch unerwartet war, war Geschwindigkeit. Die gleichen 3s zum Laden (erster Anruf) und fantastische 140 ms für nachfolgende Anrufe. Hier dauert nicht nur der erste Start unangenehm lange, sondern die Situation wird auch nicht korrigiert. Anscheinend haben die Jungs von JetBrains etwas Magie gemacht, indem sie JIT ihren Code auf diese Weise optimieren ließen. Weil ANTLR im Laufe der Zeit überhaupt nicht optimiert wird.

Kotlin ANTLR parser ()
first call elapsed : 3705.101ms
min time in next 10 calls: 139.596ms
min time in next 100 calls: 138.279ms
min time in next 1000 calls: 137.20099ms
Whole time for 1111 calls: 161.90619 seconds
()
first call elapsed : 3705.101ms
min time in next 10 calls: 139.596ms
min time in next 100 calls: 138.279ms
min time in next 1000 calls: 137.20099ms
Whole time for 1111 calls: 161.90619 seconds

Javacc


Im Allgemeinen sind wir überrascht, die Dienste von ANTLR abzulehnen. Das Parsen muss nicht so lange dauern! Es gibt keine kosmischen Mehrdeutigkeiten in Kotlins Grammatik und ich habe sie auf praktisch leere Dateien überprüft. Es ist also Zeit, das alte JavaCC aufzudecken, die Ärmel hochzukrempeln und trotzdem "es selbst zu tun und wie es geht".

Diesmal waren die Zahlen zu erwarten, wenn auch im Vergleich zu den Alternativen - unerwartet angenehm.

Kotlin JavaCC parser ()
first call elapsed : 19.024ms
min time in next 10 calls: 1.952ms
min time in next 100 calls: 0.379ms
min time in next 1000 calls: 0.114ms
Whole time for 1111 calls: 0.38707677 seconds
()
first call elapsed : 19.024ms
min time in next 10 calls: 1.952ms
min time in next 100 calls: 0.379ms
min time in next 1000 calls: 0.114ms
Whole time for 1111 calls: 0.38707677 seconds

Plötzliche Profis Ihres JavaCC-Parsers
Anstatt einen eigenen Parser zu schreiben, möchte ich natürlich eine vorgefertigte Lösung verwenden. Bestehende haben jedoch große Nachteile:

- Leistung (Pausen beim Lesen eines neuen Shaders sind nicht akzeptabel, sowie drei Sekunden Aufwärmen zu Beginn)
- eine riesige Kotlin-Laufzeit, ich bin mir nicht einmal sicher, ob es möglich ist, den Parser mit seiner Verwendung in das Endprodukt zu packen
- Übrigens, in der aktuellen Lösung mit Groovy das gleiche Problem - die Laufzeit erstreckt sich

Während der resultierende JavaCC-Parser ist

+ ausgezeichnete Geschwindigkeit sowohl am Start als auch im Prozess
+ nur ein paar Klassen des Parsers selbst

Schlussfolgerungen


JetBrains sind zu schwer zu ziehen, ANTLR ist ein Hype, aber unerwartet langsam, und JavaCC ist zu früh, um es abzuschreiben.

Analysieren einer einfachen Kotlin-Datei mit drei verschiedenen Implementierungen:

1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8
1000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.81000 () JetBrains 3254 16,6 35.3 JetBrains (w/o analyzer) 1423 0,9 35.3 ANTLR 3705 137,2 1.4 JavaCC 19 0,1 0.8

Irgendwann entschied ich mich, die Größe des Glases mit allen Abhängigkeiten zu betrachten. JetBrains sind wie erwartet großartig, aber die ANTLR-Laufzeit beeindruckt durch ihre Größe .
UPDATE: Anfangs habe ich 15 MB geschrieben, aber wie in den Kommentaren vorgeschlagen, sinkt die Größe auf den erwarteten Wert, wenn Sie antlr4-runtime anstelle von antlr4 verbinden. Obwohl der JavaCC-Parser selbst zehnmal kleiner als ANTLR bleibt (wenn Sie den gesamten Code mit Ausnahme der Parser selbst entfernen).
Die Größe des Glases als solches ist natürlich für Mobiltelefone wichtig. Dies ist jedoch auch für den Desktop von Bedeutung, da es sich tatsächlich um die Menge an zusätzlichem Code handelt, die Fehler enthalten kann, die von der IDE indiziert werden sollten. Dies wirkt sich genau auf die Geschwindigkeit des ersten Ladens und die Geschwindigkeit des Aufwärmens aus. Darüber hinaus gibt es für komplexen Code wenig Hoffnung, in eine andere Sprache zu übersetzen.
Ich fordere Sie nicht auf, Kilobyte zu zählen, und ich schätze die Zeit und die Bequemlichkeit des Programmierers, aber es lohnt sich trotzdem, über Einsparungen nachzudenken, da Projekte so ungeschickt und schwierig zu warten sind.

Ein paar Worte zu ANTLR und JavaCC

Ein ernstes Merkmal von ANTLR ist die Trennung von Grammatik und Code. Es wäre gut, wenn es nicht so teuer bezahlen müsste. Ja, und dies ist nur für "Serienentwickler von Grammatiken" von Bedeutung, und für Endprodukte ist dies nicht so wichtig, da selbst die vorhandene Grammatik noch fertig sein muss, um Ihren Code zu schreiben. Wenn wir Geld sparen und eine Grammatik von Drittanbietern verwenden, ist dies möglicherweise nur unpraktisch, muss jedoch noch gründlich verstanden werden und transformiert den Baum für sich. Im Allgemeinen mischt JavaCC natürlich Fliegen und Schnitzel, aber ist das wirklich wichtig und ist es so schlimm?

Ein weiteres Merkmal von ANTLR sind die vielen Zielplattformen. Aber hier können Sie von der anderen Seite schauen - der Code unter JavaCC ist sehr einfach. Und es ist sehr einfach ... ausgestrahlt! Richtig mit Ihrem benutzerdefinierten Code - zumindest in C #, zumindest in JS.

PS


Der gesamte Code ist hier github.com/kravchik/yast

Das Ergebnis der Analyse ist ein auf YastNode erstellter Baum (dies ist in der Tat eine sehr einfache Klasse - eine Karte mit praktischen Methoden und einem Bezeichner). Aber YastNode ist nicht wirklich ein "sphärischer Knoten im luftleeren Raum". Es ist diese Klasse, die ich aktiv benutze, basierend darauf habe ich mehrere Werkzeuge gesammelt - einen Typierer, mehrere Übersetzer und einen Optimierer / Inliner.

Der JavaCC-Parser enthält noch nicht die gesamte Grammatik, es sind noch 10 Prozent übrig. Es scheint jedoch nicht, dass sie die Leistung beeinträchtigen könnten. Ich habe die Geschwindigkeit beim Hinzufügen von Regeln überprüft und sie hat sich nicht merklich geändert. Außerdem habe ich bereits viel mehr getan, als ich brauchte, und versuche nur, das unerwartete Ergebnis des Prozesses zu teilen.

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


All Articles