Como o WebAssembly pode executar JavaScript rapidamente
(bytecodealliance.org)Introdução
-
Quando se executa JS no navegador, o mecanismo de JS do navegador é bem ajustado, então a execução é rápida, mas hoje em dia o JS também é muito usado em outros ambientes. (serverless, consoles de videogame, iOS etc.)
-
WASM é uma tecnologia que permite executar JS rapidamente nesses runtimes.
Como funciona
-
Se houver um mecanismo de JS, o código JS é transformado em bytecode por meio de um interpretador e de um compilador JIT, entre outros.
-
Em ambientes sem mecanismo de JS, é preciso distribuir o mecanismo junto com o código; ao distribuir o mecanismo de JS como um módulo WASM, ele pode se tornar portável em vários ambientes.
-
O código JS passa a rodar dentro de um mecanismo de JS isolado dentro do mecanismo WASM.
-
O mecanismo de JS usado pelo mecanismo WASM é o SpiderMonkey, que também é usado pelo Firefox.
-
O WASM não consegue, por si só, gerar código de máquina, então precisa passar por compilação para JS.
-
Mas como não pode usar JIT, o normal seria que o WASM fosse lento. Então como exatamente o WASM consegue “acelerar” a execução de JS?
Onde o WASM é usado
Usar JS no iOS (ou em ambientes onde não se pode usar JIT)
- Consoles de videogame, apps iOS sem privilégios, smart TVs etc. não podem usar JIT por motivos de segurança.
(→ O texto fala como se fosse óbvio que a compilação JIT tem questões de segurança, mas mesmo procurando não encontrei bem o motivo.)
- Por isso, nesses casos é preciso usar interpretador, mas na prática os apps que rodam nessas plataformas costumam executar por muito tempo e ter bastante código, então faz sentido evitar a lentidão causada por um interpretador.
- Como usar JS sem sofrer a perda de desempenho do interpretador?
Usar JS em serverless
- Em ambientes serverless, o JIT existe, mas o problema é o tempo longo de cold start, que aumenta a latência. (só para carregar o mecanismo já são pelo menos 5 ms)
- Existem técnicas de otimização para esconder o tempo de cold start, mas à medida que a camada de rede melhora (e.g., QUIC), isso perde importância; além disso, mesmo executando várias funções serverless ao mesmo tempo, essas técnicas deixam de ser muito úteis.
- Também é possível evitar o tempo de cold start com reutilização de instâncias, mas isso significa que o estado é compartilhado entre requisições, o que vira um risco de segurança.
- Por causa disso, na prática também é comum colocar muito conteúdo dentro de uma única função serverless, sem seguir as best practices.
- Em outras palavras, se o problema do cold start for resolvido, várias técnicas usadas para evitá-lo deixam de ser necessárias e muitos problemas se resolvem.
- O WASM encapsula e isola o JS, e como o próprio código do WASM é curto e simples, é mais fácil monitorá-lo e reduzir riscos de segurança.
Onde o mecanismo de JS gasta mais tempo
Fase de inicialização
- (inicialização do mecanismo) Isso corresponde ao caso serverless. O mecanismo precisa se preparar e adicionar funções embutidas ao ambiente. Essa é uma das razões pelas quais o cold start em serverless é lento.
- (inicialização da aplicação) Parse das funções em bytecode, alocação de memória para variáveis, atribuição de valores às variáveis
Fase de runtime
- A partir daqui, o throughput é afetado por várias condições.
- quais recursos da linguagem são usados
- se o código se comporta de forma previsível do ponto de vista do mecanismo de JS
- que tipo de estruturas de dados são usadas
- se o código roda por tempo suficiente para se beneficiar do compilador otimizador do mecanismo de JS
Tornar o mecanismo de JS rápido significa acelerar tanto a fase de inicialização quanto a fase de runtime. Mais precisamente, reduzir o tempo gasto na inicialização e aumentar o throughput no runtime, ou seja, a velocidade de processamento do código.
Reduzindo o tempo de inicialização
-
O WASM reduz o tempo de inicialização usando um pré-inicializador chamado Wizer. (em apps pequenos, JS on WASM é cerca de 13 vezes mais rápido que um isolate JS)
-
Na etapa de build, antes de distribuir o código, o pré-inicializador executa uma vez todo o código JS até a fase de inicialização.
-
Com isso, os códigos JS ficam armazenados como bytecode na memória linear do mecanismo de JS, e a alocação de memória também já está concluída.
-
Isso é copiado tal como está e anexado à seção de dados do WASM.
-
-
Quando o mecanismo de JS é instanciado, ele pode acessar todos os dados da seção de dados. Se precisar de uma memória específica, basta copiá-la da seção de dados. Por isso não há necessidade de tempo de start, e por isso isso é chamado de pré-inicialização.
-
Atualmente a seção de dados é anexada ao mesmo módulo que o mecanismo de JS, mas no futuro há o plano de usar module linking para tornar a seção de dados um módulo separado, permitindo que várias aplicações compartilhem o mecanismo de JS.
-
E, na verdade, essa técnica de pré-inicialização não precisa ficar limitada ao mecanismo de JS; é um conceito que pode ser usado em qualquer runtime, como Python, Ruby ou Lua.
Aumentando o throughput
-
Se o código JS roda só por pouco tempo, de qualquer forma ele não passa por JIT, então o throughput do WASM será igual ao do navegador. Porém, em códigos que rodam por bastante tempo, a diferença de throughput causada pela presença ou ausência de JIT é grande.
-
Como o WASM não pode usar JIT, ele adota em vez disso a compilação AOT (ahead-of-time), aproveitando técnicas que podem ser trazidas do JIT.
-
Uma das técnicas de otimização do JIT é inline caching: manter trechos de código executados anteriormente para reutilizá-los.
-
No WASM, padrões usados com frequência em JS são transformados em stubs. Por exemplo, acessar propriedades de objetos.
-
Para fazer corretamente o acesso a propriedades de objetos, normalmente são necessárias informações de shape e offset, e isso não pode ser conhecido via AOT.
-
Mas é possível preparar antecipadamente um stub que acessa a propriedade recebendo shape e offset como parâmetros. Esse código de stub pode ser reutilizado em vários pontos.
-
-
O WASM transforma todos esses common patterns em stubs. Isso independe de como o código JS realmente é escrito. Com isso, é possível reduzir o código de máquina gerado pelo mecanismo de JS, reduzir também o tempo de inicialização e melhorar a localidade de cache.
-
Verificou-se que, preparando apenas 2kb desses stubs, é possível cobrir cerca de 95% do código JS real.
-
Como essa técnica é ahead-of-time, ou seja, otimiza sem conhecer o conteúdo do código (sem profiling), ainda deve haver espaço para otimizar mais, como num JIT, se houver mais profiling.
- Mas como o próprio profiling não é algo simples, ainda estão trabalhando nisso.
2 comentários
Sobre as questões de segurança do JIT, esse assunto já havia sido mencionado antes em um post do blog da equipe do MS Edge que foi apresentado aqui. Basicamente, como os motores JIT são complexos, além de aumentarem a superfície de ataque, métodos aplicados pelo JIT para melhorar o desempenho, como a otimização especulativa (Speculative Optimization), aparentemente tendem a causar repetidamente certos padrões de problemas de segurança. Por isso, dizem que a proporção de falhas de segurança relacionadas a JIT entre as vulnerabilidades de segurança dos navegadores web é bastante alta.
https://pt.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
Ah, obrigado! No fim das contas, eu nem tinha procurado no GeekNews.