Compilando Kotlin: JetBrains VS ANTLR VS JavaCC


¿Qué tan rápido está analizando Kotlin y qué importa? JavaCC o ANTLR? ¿Son adecuados los códigos fuente de JetBrains?

Compara, fantasea y pregúntate.

tl; dr


JetBrains es demasiado difícil de arrastrar, ANTLR es exagerado pero inesperadamente lento, y JavaCC es demasiado temprano para cancelar.

Analizar un archivo Kotlin simple con tres implementaciones diferentes:
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 buen día soleado ...


Decidí construir un traductor en GLSL a partir de un lenguaje conveniente. La idea era programar sombreadores directamente en la idea y obtener soporte IDE "gratuito": sintaxis, depuración y pruebas unitarias. Resultó realmente muy conveniente .

Desde entonces, la idea de usar Kotlin ha permanecido: puede usar el nombre vec3 en él, es más estricto y más conveniente en el IDE. Además, es bombo publicitario. Aunque, desde el punto de vista de mi gerente interno, estas son razones insuficientes, la idea surgió tantas veces que decidí deshacerme de ella simplemente implementándola.

¿Por qué no Java? No hay sobrecarga del operador, por lo que la sintaxis de la aritmética vectorial será demasiado diferente de lo que estás acostumbrado a ver en el desarrollo del juego.

Jetbrains


Los chicos de JetBrains subieron su código de compilación al github . Cómo usarlo, puedes echar un vistazo aquí y aquí .

Al principio usé su analizador junto con el analizador, porque para traducir a otro idioma, necesitas saber de qué tipo es la variable sin especificar explícitamente el tipo val x = vec3() . Aquí el tipo para el lector es obvio, pero en el AST esta información no es tan fácil de obtener, especialmente cuando hay otra variable a la derecha o una llamada de función.

Aquí me decepcionó. El primer lanzamiento del analizador en un archivo primitivo toma 3 segundos (TRES SEGUNDOS).

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

Tal momento tiene los siguientes inconvenientes obvios:

  1. porque son más de tres segundos para iniciar un juego o una aplicación.
  2. durante el desarrollo, uso una sobrecarga de sombreador en caliente y veo el resultado inmediatamente después de cambiar el código.
  3. A menudo reinicio la aplicación y me alegro de que se inicie lo suficientemente rápido (un segundo o dos).

Más tres segundos para calentar el analizador, esto es inaceptable. Por supuesto, inmediatamente quedó claro que durante las llamadas posteriores, el tiempo de análisis se reduce a 50 ms e incluso a 20 ms, lo que elimina (casi) el inconveniente n. ° 2 de la expresión. Pero los otros dos no van a ninguna parte. Además, 50ms por archivo son más de 2500ms por 50 archivos (un sombreador es 1-2 archivos). ¿Qué pasa si es Android? (Aquí solo estamos hablando del tiempo).

Cabe destacar el loco trabajo de JIT. El tiempo de análisis para un archivo simple cae de 70 ms a 16 ms. Lo que significa, en primer lugar, que el JIT mismo consume recursos y, en segundo lugar, el resultado en una JVM diferente puede ser muy diferente.

En un intento por averiguar de dónde provenían estos números, había una opción: usar su analizador sin un analizador. Después de todo, solo necesito organizar los tipos y esto se puede hacer con relativa facilidad, mientras que el analizador JetBrains hace algo mucho más complejo y recopila mucha más información. Y luego, el tiempo de inicio se reduce a la mitad (pero casi un segundo y medio sigue siendo decente), y el tiempo de las llamadas posteriores ya es mucho más interesante: de 8 ms en los primeros diez a 0.9ms en algún lugar del millar.

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

Tuve que recoger solo esos números. El primer tiempo de lanzamiento es importante al cargar los primeros sombreadores. Es crítico, porque aquí no puedes distraer al usuario mientras los sombreadores están cargados en segundo plano, solo espera. Una caída en el tiempo de ejecución es importante para ver la dinámica en sí, cómo funciona JIT, qué tan eficientemente podemos cargar sombreadores en una aplicación cálida.

La razón principal para mirar principalmente el analizador JetBrains fue el deseo de usar su tipificador. Pero dado que rechazarlo se convierte en la opción discutida, puede intentar usar otros analizadores. Además, los que no sean JetBrains probablemente serán mucho más pequeños, menos exigentes con el medio ambiente, más fáciles con soporte e inclusión de código en el proyecto.

ANTLR


No hubo un analizador en JavaCC, pero en el ANTLR exagerado, como se esperaba, hay ( uno , dos ).

Pero lo inesperado fue la velocidad. Los mismos 3s para cargar (primera llamada) y fantásticos 140ms para llamadas posteriores. Aquí, no solo el primer lanzamiento dura desagradablemente largo, sino que la situación no se corrige. Aparentemente, los muchachos de JetBrains hicieron algo de magia al permitir que JIT optimizara su código de esa manera. Porque ANTLR no está optimizado en absoluto con el tiempo.

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 general, nos sorprende rechazar los servicios de ANTLR. ¡El análisis no tiene que ser tan largo! No hay ambigüedades cósmicas en la gramática de Kotlin, y lo comprobé en archivos prácticamente vacíos. Entonces, es hora de descubrir el antiguo JavaCC, arremangarse y aún "hacerlo usted mismo y cómo hacerlo".

Esta vez los números resultaron ser esperados, aunque en comparación con las alternativas, inesperadamente agradables.

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

Ventajas repentinas de su analizador JavaCC
Por supuesto, en lugar de escribir su propio analizador, me gustaría usar una solución preparada. Pero los existentes tienen enormes desventajas:

- rendimiento (las pausas al leer un nuevo sombreador son inaceptables, así como tres segundos de calentamiento al inicio)
- un gran tiempo de ejecución de Kotlin, ni siquiera estoy seguro de si es posible empacar el analizador con su uso en el producto final
- por cierto, en la solución actual con Groovy el mismo problema - el tiempo de ejecución se extiende

Mientras que el analizador JavaCC resultante es

+ excelente velocidad tanto al inicio como en el proceso
+ solo unas pocas clases del analizador en sí

Conclusiones


JetBrains es demasiado difícil de arrastrar, ANTLR es exagerado pero inesperadamente lento, y JavaCC es demasiado temprano para cancelar.

Analizar un archivo Kotlin simple con tres implementaciones diferentes:

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

En algún momento, decidí mirar el tamaño del frasco con todas las dependencias. Los JetBrains son geniales como se esperaba, pero el tiempo de ejecución ANTLR sorprende con su tamaño .
ACTUALIZACIÓN: Inicialmente, escribí 15 MB, pero como se sugiere en los comentarios, si conecta antlr4-runtime en lugar de antlr4, el tamaño cae al valor esperado. Aunque el analizador JavaCC en sí mismo sigue siendo 10 veces más pequeño que ANTLR (si elimina todo el código, excepto los analizadores mismos).
El tamaño de la jarra como tal es importante, por supuesto, para los teléfonos celulares. Pero también es importante para el escritorio, porque, de hecho, significa la cantidad de código adicional que puede contener errores, que el IDE debe indexar, lo que, exactamente, afecta la velocidad de la primera carga y la velocidad de calentamiento. Además, para el código complejo, hay pocas esperanzas de traducir a otro idioma.
No le insto a contar kilobytes y aprecio el tiempo y la conveniencia del programador, pero aún así vale la pena pensar en los ahorros, porque así es como los proyectos se vuelven torpes y difíciles de mantener.

Algunas palabras sobre ANTLR y JavaCC

Una característica seria de ANTLR es la separación de la gramática y el código. Sería bueno si no tuviera que pagar tan caro. Sí, y esto solo es importante para los "desarrolladores en serie de gramáticas", y para los productos finales esto no es tan importante, porque incluso la gramática existente aún tendrá que estar terminada para escribir su código. Además, si ahorramos dinero y tomamos una gramática de “terceros”, puede ser un inconveniente, aún tendrá que entenderse a fondo, transformará el árbol por sí mismo. En general, JavaCC, por supuesto, mezcla moscas y chuletas, pero ¿realmente importa y es tan malo?

Otra característica de ANTLR son las muchas plataformas de destino. Pero aquí puede mirar desde el otro lado: el código de JavaCC es muy simple. Y es muy simple ... transmisión! Justo con su código personalizado, al menos en C #, al menos en JS.

PS


Todo el código está aquí github.com/kravchik/yast

El resultado del análisis es un árbol construido en YastNode (de hecho, esta es una clase muy simple: un mapa con métodos convenientes y un identificador). Pero YastNode no es realmente un "nodo esférico en el vacío". Es esta clase la que uso activamente, en base a ella he recopilado varias herramientas: un tipificador, varios traductores y un optimizador / alineador.

El analizador JavaCC aún no contiene toda la gramática, queda un 10 por ciento, pero no parece que puedan afectar el rendimiento: verifiqué la velocidad a medida que se agregaban las reglas y no cambió notablemente. Además, ya he hecho mucho más de lo que necesitaba y solo trato de compartir el resultado inesperado encontrado en el proceso.

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


All Articles