Compilation de Kotlin: JetBrains VS ANTLR VS JavaCC


Quelle est la vitesse d'analyse de Kotlin et qu'importe? JavaCC ou ANTLR? Les codes source JetBrains conviennent-ils?

Comparez, fantasmez et émerveillez-vous.

tl; dr


Les JetBrains sont trop difficiles à faire glisser, ANTLR est hype mais lent et inattendu, et JavaCC est trop tôt pour être annulé.

Analyser un simple fichier Kotlin avec trois implémentations différentes:
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

Une belle journée ensoleillée ...


J'ai décidé de construire un traducteur en GLSL à partir d'un langage pratique. L'idée était de programmer des shaders directement dans l'idée et d'obtenir un support IDE «gratuit» - syntaxe, débogage et tests unitaires. Cela s'est avéré vraiment très pratique .

Depuis lors, l'idée d'utiliser Kotlin est restée - vous pouvez utiliser le nom vec3, c'est plus strict et plus pratique dans l'IDE. De plus, c'est du battage médiatique. Bien que, du point de vue de mon responsable interne, ce soient toutes des raisons insuffisantes, l'idée est revenue tellement de fois que j'ai décidé de m'en débarrasser simplement en la mettant en œuvre.

Pourquoi pas Java? Il n'y a pas de surcharge d'opérateur, donc la syntaxe de l'arithmétique vectorielle sera trop différente de ce que vous avez l'habitude de voir dans le développement de jeu

Jetbrains


Les gars de JetBrains ont téléchargé leur code de compilation sur le github . Comment l'utiliser, vous pouvez jeter un œil ici et ici .

Au début, j'ai utilisé leur analyseur avec l'analyseur, car pour traduire dans une autre langue, vous devez savoir de quel type est la variable sans spécifier explicitement le type val x = vec3() . Ici, le type de lecteur est évident, mais dans l'AST, cette information n'est pas si facile à obtenir, surtout lorsqu'une autre variable est à droite, ou lors d'un appel de fonction.

Ici, j'ai été déçu. Le premier lancement de l'analyseur sur un fichier primitif prend 3 secondes (TROIS SECONDES).

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

Un tel moment présente les inconvénients évidents suivants:

  1. car il faut plus de trois secondes pour lancer un jeu ou une application.
  2. pendant le développement, j'utilise une surcharge de shader à chaud et je vois le résultat immédiatement après avoir changé le code.
  3. Je redémarre souvent l'application et je suis content qu'elle démarre assez vite (une seconde ou deux).

Plus trois secondes pour réchauffer l'analyseur - c'est inacceptable. Bien sûr, il est immédiatement devenu clair que lors des appels ultérieurs, le temps d'analyse tombe à 50 ms et même à 20 ms, ce qui supprime (presque) l'inconvénient n ° 2 de l'expression. Mais les deux autres ne vont nulle part. De plus, 50 ms par fichier est plus 2500 ms par 50 fichiers (un shader correspond à 1-2 fichiers). Et si c'était Android? (Ici, nous ne parlons que de temps.)

Il convient de noter le travail fou de JIT. Le temps d'analyse d'un fichier simple passe de 70 ms à 16 ms. Ce qui signifie, d'une part, que le JIT lui-même consomme des ressources, et d'autre part, le résultat sur une JVM différente peut être très différent.

Pour essayer de savoir d'où venaient ces chiffres, il y avait une option - utiliser leur analyseur sans analyseur. Après tout, j'ai juste besoin d'organiser les types et cela peut être fait relativement facilement, tandis que l'analyseur JetBrains fait quelque chose de beaucoup plus complexe et recueille beaucoup plus d'informations. Et puis le temps de démarrage diminue de moitié (mais presque une seconde et demie est toujours décent), et le temps des appels suivants est déjà beaucoup plus intéressant - de 8 ms dans les dix premiers à 0,9 ms quelque part dans le millier.

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

J'ai dû collecter de tels chiffres. Le premier temps de lancement est important lors du chargement des premiers shaders. C'est essentiel, car ici, vous ne pouvez pas distraire l'utilisateur pendant que les shaders sont chargés en arrière-plan, il attend juste. Une baisse de l'exécution est importante pour voir la dynamique elle-même, le fonctionnement de JIT, l'efficacité avec laquelle nous pouvons charger des shaders sur une application chaude.

La principale raison de regarder principalement l'analyseur JetBrains était le désir d'utiliser leur typificateur. Mais comme le rejeter devient l'option discutée, vous pouvez essayer d'utiliser d'autres analyseurs. De plus, les non-JetBrains seront probablement beaucoup plus petits, moins exigeants sur l'environnement, plus faciles avec le support et l'inclusion de code dans le projet.

ANTLR


Il n'y avait pas d'analyseur sur JavaCC, mais sur le battage médiatique ANTLR, comme prévu, il y en a ( un , deux ).

Mais ce qui était inattendu, c'était la vitesse. Les mêmes 3 secondes pour le chargement (premier appel) et 140 ms fantastiques pour les appels suivants. Ici, non seulement le premier lancement dure désagréablement, mais la situation n'est pas corrigée. Apparemment, les gars de JetBrains ont fait de la magie en laissant JIT optimiser leur code de cette façon. Parce que ANTLR n'est pas du tout optimisé dans le temps.

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


En général, nous sommes surpris de refuser les services d'ANTLR. L'analyse n'a pas besoin d'être aussi longue! Il n'y a aucune ambiguïté cosmique dans la grammaire de Kotlin, et je l'ai vérifié sur des fichiers pratiquement vides. Il est donc temps de découvrir l'ancien JavaCC, de retrousser vos manches et de toujours "le faire vous-même et comment."

Cette fois, les chiffres se sont avérés attendus, bien qu'en comparaison avec les alternatives - étonnamment agréables.

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

Avantages soudains de votre analyseur JavaCC
Bien sûr, au lieu d'écrire votre propre analyseur, je voudrais utiliser une solution prête à l'emploi. Mais ceux existants ont d'énormes inconvénients:

- performances (les pauses lors de la lecture d'un nouveau shader sont inacceptables, ainsi que trois secondes d'échauffement au départ)
- un énorme runtime kotlin, je ne sais même pas s'il est possible d'emballer un analyseur dans le produit final en l'utilisant
- par ailleurs, dans la solution actuelle avec Groovy le même problème - le runtime s'étire

Alors que l'analyseur JavaCC résultant est

+ excellente vitesse à la fois au début et dans le processus
+ juste quelques classes de l'analyseur lui-même

Conclusions


Les JetBrains sont trop difficiles à faire glisser, ANTLR est hype mais lent et inattendu, et JavaCC est trop tôt pour être annulé.

Analyser un simple fichier Kotlin avec trois implémentations différentes:

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

À un moment donné, j'ai décidé de regarder la taille du pot avec toutes les dépendances. Les JetBrains sont excellents comme prévu, mais le runtime ANTLR étonne par sa taille .
MISE À JOUR: Initialement, j'ai écrit 15 Mo, mais comme suggéré dans les commentaires, si vous connectez antlr4-runtime au lieu de antlr4, la taille tombe à la valeur attendue. Bien que l'analyseur JavaCC lui-même reste 10 fois plus petit que ANTLR (si vous supprimez tout le code, à l'exception des analyseurs eux-mêmes).
La taille du pot en tant que tel est bien sûr importante pour les téléphones portables. Mais cela compte également pour le bureau, car, en fait, cela signifie la quantité de code supplémentaire qui peut contenir des bogues, que l'IDE doit indexer, ce qui, exactement, affecte la vitesse du premier chargement et la vitesse de préchauffage. De plus, pour un code complexe, il y a peu d'espoir de traduire dans une autre langue.
Je ne vous invite pas à compter les kilo-octets et j'apprécie le temps et la commodité du programmeur, mais cela vaut quand même la peine de penser aux économies, car c'est ainsi que les projets deviennent maladroits et difficiles à maintenir.

Quelques mots sur ANTLR et JavaCC

Une caractéristique sérieuse d'ANTLR est la séparation de la grammaire et du code. Ce serait bien s'il n'avait pas à payer si cher. Oui, et cela n'a d'importance que pour les «développeurs en série de grammaires», et pour les produits finaux, ce n'est pas si important, car même la grammaire existante devra encore être terminée pour écrire votre code. De plus, si nous économisons de l'argent et prenons une grammaire «tierce» - cela peut simplement être gênant, il faudra quand même bien le comprendre, cela transformera l'arbre pour lui-même. En général, JavaCC, bien sûr, mélange les mouches et les côtelettes, mais est-ce vraiment important et est-ce si mauvais?

Une autre caractéristique d'ANTLR est les nombreuses plates-formes cibles. Mais ici, vous pouvez regarder de l'autre côté - le code sous JavaCC est très simple. Et c'est très simple ... diffusé! À droite avec votre code personnalisé - au moins en C #, au moins en JS.

PS


Tout le code est ici github.com/kravchik/yast

Le résultat de l'analyse est un arbre construit sur YastNode (il s'agit en fait d'une classe très simple - une carte avec des méthodes pratiques et un identifiant). Mais YastNode n'est pas vraiment un "nœud sphérique dans le vide". C'est cette classe que j'utilise activement, sur la base de laquelle j'ai collecté plusieurs outils - un typificateur, plusieurs traducteurs et un optimiseur / inliner.

L'analyseur JavaCC ne contient pas encore toute la grammaire, il en reste 10%. Mais il ne semble pas que cela puisse affecter les performances - j'ai vérifié la vitesse au fur et à mesure de l'ajout de règles, et cela n'a pas changé de façon notable. De plus, j'ai déjà fait beaucoup plus que ce dont j'avais besoin et j'essaie simplement de partager le résultat inattendu trouvé dans le processus.

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


All Articles