Compilador vs. interpretador e otimização em ambiente embarcado
Uma equipe precisa executar uma linguagem de script de alto nível em um microcontrolador com 64 KB de RAM, 256 KB de flash e CPU de 80 MHz. O código do usuário é carregado em tempo de execução, muda com frequência e é executado repetidamente em um laço de controle a cada 10 ms. Duas propostas: (A) interpretar a árvore sintática abstrata (AST) diretamente; (B) compilar para bytecode e executar em uma máquina virtual com compilação JIT para código nativo. Medições preliminares mostram que a interpretação da AST consome ~2,5 ms por execução do script, e a máquina virtual com JIT consome ~0,4 ms por execução, mas o JIT precisa de 30 ms na primeira execução e aloca buffers de 48 KB.
1. Avalie as duas propostas considerando o orçamento de tempo (10 ms) e de memória (64 KB).
Read the worked answer
Tempo: a AST leva 2,5 ms por execução, dentro dos 10 ms - folga de 7,5 ms. O JIT leva 0,4 ms por execução após aquecer, mas 30 ms na primeira, o que estoura o período de 10 ms e pode perder o prazo do laço de controle na inicialização (a menos que o aquecimento ocorra fora do laço). Memória: o JIT precisa de 48 KB de buffers, sobrando 16 KB para pilha, dados, sistema e o próprio interpretador - provavelmente inviável em 64 KB, ainda mais com código do usuário e buffers de I/O. A AST, embora mais lenta, tem pegada menor e previsível. Conclusão: a proposta B é arriscada por memória e por latência de aquecimento; a proposta A atende ao orçamento atual.
2. Que estratégia intermediária você recomendaria se o script crescer e a AST passar a levar 8 ms?
Read the worked answer
Com 8 ms, a margem cai para 2 ms, insuficiente para variação de carga. Alternativas, da menor para a maior complexidade: (1) otimizar o interpretador de AST - cache de resolução de nomes, avaliação de constantes, redução de alocações; (2) compilar para bytecode sem JIT: elimina o custo de percorrer a AST a cada execução (a compilação ocorre uma vez, quando o script muda), com pegada de memória pequena e sem os 48 KB do JIT; (3) JIT seletivo apenas para os trechos mais quentes, com buffers pequenos e aquecimento fora do laço crítico. Recomendo (2) como primeiro passo: mantém previsibilidade, cabe na memória e deve reduzir bastante o tempo por execução; só considerar JIT se ainda faltar margem, e nesse caso medindo memória real e aquecendo fora do período de 10 ms.
3. Como a mudança frequente do código do usuário afeta a escolha?
Read the worked answer
Se o código muda com frequência, o custo de compilação é pago repetidamente e o benefício do JIT (amortizar compilação ao longo de muitas execuções) diminui. Com mudanças a cada poucas execuções, o JIT pode nunca amortizar os 30 ms iniciais. Por outro lado, um bytecode compilado uma vez por versão ainda se beneficia: compila na carga e executa muitas vezes até a próxima mudança. A AST é a que menos sofre com mudanças (nenhuma compilação), mas é a mais lenta por execução. Portanto, quanto maior a frequência de mudança e menor o número de execuções por versão, mais a balança pende para interpretação/bytecode simples e contra o JIT - a decisão deve ser guiada pela razão entre execuções por versão e custo de compilação, além dos limites de memória.