WebAssembly, a Jornada
Olá a todos! Estou postando aqui uma série de textos para mostrar a minha jornada na tentativa de aprender WebAssembly. Espero evoluir com mais artigos interessantes nessa área num futuro próximo.
Em 2015, na BrazilJS conf, eu assisti a keynote de encerramento do BrendanEich, criador do Javascript, falando sobre o nascimento, crescimento e evolução do JS. Quem o conhece, sabe que uma de suas citações famosa é “Always bet on JS”, mas no final dessa palestra ele falou algo que ficou preso na minha mente:
Always bet on Js & WASM
Essa foi a primeira vez que eu ouvi essa palavra. Mas o que é WebAssembly? Qual é o problema que essa tecnologia tenta solucionar? Naquele momento eu comecei a minha jornada para responder essas questões.
Nosso PoC
Para entender melhor essa tecnologia, me juntei ao meu amigo e também desenvolvedor Elia Maino para desenvolver um algoritmo capaz de nos permitir comparar a performance do WASM com Vanilla JS (significa JS puro, sem frameworks ou polyfill).
Para provar nosso conceito, escolhemos implementar o John Conway’s game of life como nosso problema para esse PoC. Esse é um jogo sem jogadores com algumas regras bem simples.
- O mundo é uma Matriz no qual cada célula pode ter dois estados: vivo ou morto.
- A única entrada para o jogo é o estado inicial.
- O estado de uma célula específica é determinado pela interação dessa célula com as outras células adjacentes na horizontal, vertical e diagonal.
- Uma célula viva com menos de dois vizinhos vivos morre.
- Uma célula viva com dois ou três vizinhos vivos continua viva para a próxima geração.
- Uma célula viva com mais de três vizinhos vivos morre.
- Uma célula morta com exatamente três vizinhos vivos revive para a próxima geração.
O plano consiste em criar uma matriz grande, preencher com valores randômicos (0 ou 1), enviar esse estado inicial e renderizar o resultado, então computar o próximo estado e renderizar de novo, repetindo esse último passo várias vezes.
Nós planejamos implementar essa solução em três abordagens: Vanilla JS, WebAssembly e Web workers. A complexidade da nossa solução em todas as abordagens foi de O(n*m) onde n é a largura da nossa matriz e m a altura dela. Já que a função de render é exatamente a mesma para todas as implementações, nós não vamos considerá-las nas nossas métricas.
Vanilla JS
A arquitetura básica para essa implementação consiste em criar o novo jogo, gerar e enviar o primeiro estado (matriz de zeros e uns) para ele. O componente game
guarda esse estado e retorna uma função next
que retorna o próximo estado quando necessário. Nesse caso é chamada a função getNextState()
do arquivo enviroment.js
que é a implementação Vanilla JS.
...const next = game(
document.getElementById('game'),
COLUMNS,
LINES,
createGameMatrix(LINES, COLUMNS), // gera o estado inicial
strategy(
STRATEGY,
COLUMNS,
LINES,
initialConfig
) // define qual estratégia usar para calcular a próxima geração
);...function loop() {
next().then(() => {
requestAnimationFrame(loop);
});
};loop();
No componente principal nós dividimos o problema em pequenas funções especializadas. Isso vai ajudar a ativação das optimizações do JIT compiler do browser. Iremos abordar o JIT compiler no próximo artigo. Essas funções calculam os vizinhos acima, abaixo e dos lados, cobrindo todos os casos de exceção relacionados às bordas do mundo.
Você pode estar se perguntando o porquê de tanta variação no cálculo do próximo estado, ou o porquê de usarmos tantas funções. Para responder essas questões, precisamos mostrar como JIT compilers trabalham e como eles fazem o JavaScript ser tão rápido atualmente. Mas isso fica para o próximo artigo.
Links
- Versão em inglês desse artigo: https://medium.com/@wmsbill/webassembly-the-journey-a069d6ea18a
- Brendan Eich keynote na BrazilJS 2015 (em inglês): https://www.youtube.com/watch?v=bM79WQ9iMZQ
- John Cownay Game of Life: https://pt.wikipedia.org/wiki/Jogo_da_vida
- Repositório do nosso PoC: https://github.com/eliamaino-fp/webassembly-js