Compilando o Kotlin: JetBrains x ANTLR x JavaCC


Qual a velocidade da análise do Kotlin e o que isso importa? JavaCC ou ANTLR? Os códigos-fonte JetBrains são adequados?

Compare, fantasia e admira.

tl; dr


É muito difícil arrastar o JetBrains, o ANTLR é um hype, mas inesperadamente lento, e o JavaCC é muito cedo para anular.

Analisando um arquivo Kotlin simples com três implementações 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

Um belo dia de sol ...


Decidi construir um tradutor em GLSL a partir de uma linguagem conveniente. A idéia era programar shaders diretamente na idéia e obter suporte IDE "gratuito" - sintaxe, depuração e testes de unidade. Acabou realmente muito conveniente .

Desde então, a idéia de usar o Kotlin permaneceu - você pode usar o nome vec3 nele, é mais rigoroso e mais conveniente no IDE. Além disso, é exagero. Embora, do ponto de vista do meu gerente interno, essas sejam todas razões insuficientes, a idéia voltou tantas vezes que eu decidi me livrar dela simplesmente implementando-a.

Por que não Java? Como não há sobrecarga do operador, a sintaxe da aritmética vetorial será muito diferente do que você está acostumado a ver no game dev.

Jetbrains


Os caras do JetBrains fizeram o upload do código do compilador no github . Como usá-lo, você pode espreitar aqui e aqui .

No começo, usei o analisador junto com o analisador, porque para traduzir para outro idioma, você precisa saber qual o tipo da variável sem especificar explicitamente o tipo val x = vec3() . Aqui o tipo de leitor é óbvio, mas no AST essas informações não são tão fáceis de obter, especialmente quando outra variável está à direita ou uma chamada de função.

Aqui fiquei desapontado. O primeiro lançamento do analisador em um arquivo primitivo leva 3s (TRÊS 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

Esse período apresenta os seguintes inconvenientes óbvios:

  1. porque são necessários mais três segundos para iniciar um jogo ou aplicativo.
  2. durante o desenvolvimento, uso uma sobrecarga de shader quente e vejo o resultado imediatamente após alterar o código.
  3. Costumo reiniciar o aplicativo e fico feliz por ele iniciar rápido o suficiente (um ou dois segundos).

Mais três segundos para aquecer o analisador - isso é inaceitável. Obviamente, ficou claro imediatamente que, durante as chamadas subseqüentes, o tempo de análise cai para 50 ms e até 20 ms, o que remove (quase) o inconveniente nº 2 da expressão. Mas os outros dois não vão a lugar algum. Além disso, 50ms por arquivo é mais 2500ms por 50 arquivos (um shader é um ou dois arquivos). E se for Android? (Aqui estamos falando apenas de tempo.)

Digno de nota é o trabalho louco do JIT. O tempo de análise de um arquivo simples cai de 70ms para 16ms. O que significa que, primeiro, o próprio JIT consome recursos e, segundo, o resultado em uma JVM diferente pode ser muito diferente.

Na tentativa de descobrir de onde esses números vieram, havia uma opção: usar o analisador sem um analisador. Afinal, eu só preciso organizar os tipos e isso pode ser feito com relativa facilidade, enquanto o analisador JetBrains faz algo muito mais complexo e coleta muito mais informações. E então o tempo de inicialização cai pela metade (mas quase um segundo e meio ainda é decente), e o tempo das chamadas subseqüentes já é muito mais interessante - de 8ms nos primeiros dez a 0,9ms em algum lugar dos milhares.

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

Eu tive que coletar apenas esses números. O primeiro tempo de inicialização é importante ao carregar os primeiros shaders. É fundamental, porque aqui você não pode distrair o usuário enquanto os shaders estão carregados em segundo plano, ele apenas espera. Uma queda no tempo de execução é importante para ver a própria dinâmica, como o JIT funciona, com que eficiência podemos carregar shaders em um aplicativo quente.

O principal motivo para observar principalmente o analisador JetBrains foi o desejo de usar seu tipificador. Mas como rejeitá-la se torna a opção discutida, você pode tentar usar outros analisadores. Além disso, os não-JetBrains provavelmente serão muito menores, menos exigentes para o ambiente, mais fáceis com o suporte e a inclusão de código no projeto.

ANTLR


Não havia analisador no JavaCC, mas no hype ANTLR, como esperado, existem ( um , dois ).

Mas o que foi inesperado foi a velocidade. Os mesmos 3s para carregamento (primeira chamada) e 140ms fantásticos para chamadas subseqüentes. Aqui, não apenas o primeiro lançamento dura desagradavelmente por muito tempo, mas a situação não é corrigida. Aparentemente, os caras do JetBrains fizeram alguma mágica, deixando o JIT otimizar seu código dessa maneira. Porque o ANTLR não é otimizado o tempo todo.

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


Em geral, ficamos surpresos ao recusar os serviços da ANTLR. A análise não precisa ser tão longa! Não há ambiguidades cósmicas na gramática de Kotlin, e eu a verifiquei em arquivos praticamente vazios. Portanto, é hora de descobrir o antigo JavaCC, arregaçar as mangas e ainda "faça você mesmo e como fazê-lo".

Dessa vez, os números eram esperados, embora em comparação com as alternativas - inesperadamente agradáveis.

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

Profissionais repentinos do seu analisador JavaCC
Obviamente, em vez de escrever seu próprio analisador, eu gostaria de usar uma solução pronta. Mas os existentes têm enormes desvantagens:

- desempenho (pausas ao ler um novo shader são inaceitáveis, bem como três segundos de aquecimento no início)
- um enorme tempo de execução do kotlin, nem tenho certeza se é possível embalar o analisador com seu uso no produto final
- a propósito, na solução atual com Groovy o mesmo problema - o tempo de execução se estende

Enquanto o analisador JavaCC resultante é

+ excelente velocidade no início e no processo
+ apenas algumas classes do próprio analisador

Conclusões


É muito difícil arrastar o JetBrains, o ANTLR é um hype, mas inesperadamente lento, e o JavaCC é muito cedo para anular.

Analisando um arquivo Kotlin simples com três implementações 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

Em algum momento, decidi analisar o tamanho do frasco com todas as dependências. O JetBrains é ótimo como o esperado, mas o tempo de execução do ANTLR surpreende com seu tamanho .
UPDATE: Inicialmente, escrevi 15 MB, mas, como sugerido nos comentários, se você conectar o antlr4-runtime em vez do antlr4, o tamanho cai para o valor esperado. Embora o analisador JavaCC em si permaneça 10 vezes menor que o ANTLR (se você remover todo o código, exceto os analisadores).
O tamanho do frasco, como tal, é importante, é claro, para os telefones celulares. Mas também é importante para a área de trabalho, porque, de fato, significa a quantidade de código adicional que pode conter bugs, que o IDE deve indexar, o que, de fato, afeta a velocidade da primeira carga e a velocidade do aquecimento. Além disso, para código complexo, há pouca esperança de tradução para outro idioma.
Não recomendo que você conte kilobytes e aprecio o tempo e a conveniência do programador, mas ainda vale a pena pensar em economia, porque é assim que os projetos se tornam desajeitados e difíceis de manter.

Algumas palavras sobre ANTLR e JavaCC

Uma característica séria do ANTLR é a separação de gramática e código. Seria bom se não precisasse pagar tanto. Sim, e isso é importante apenas para “desenvolvedores seriais de gramáticas” e, para produtos finais, isso não é tão importante, porque mesmo a gramática existente ainda precisará ser finalizada para escrever seu código. Além disso, se economizarmos dinheiro e adotarmos uma gramática de "terceiros" - pode ser inconveniente, ainda precisará ser bem entendido, pois transformará a árvore por si mesma. Em geral, o JavaCC, é claro, mistura moscas e costeletas, mas isso realmente importa e é tão ruim?

Outro recurso do ANTLR são as muitas plataformas de destino. Mas aqui você pode olhar do outro lado - o código no JavaCC é muito simples. E é muito simples ... transmitido! Direito com seu código personalizado - pelo menos em C #, pelo menos em JS.

PS


Todo o código está aqui github.com/kravchik/yast

O resultado da análise é uma árvore criada no YastNode (na verdade, é uma classe muito simples - um mapa com métodos convenientes e um identificador). Mas o YastNode não é realmente um "nó esférico no vácuo". É essa classe que eu uso ativamente; com base nela, coletei várias ferramentas - um tipificador, vários tradutores e um otimizador / inliner.

O analisador JavaCC ainda não contém toda a gramática, restam 10%, mas não parece que eles possam afetar o desempenho - verifiquei a velocidade à medida que as regras foram adicionadas e não mudou visivelmente. Além disso, já fiz muito mais do que precisava e apenas tentei compartilhar o resultado inesperado encontrado no processo.

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


All Articles