WebAssembly, a Jornada

Willian Martins
Training Center
Published in
3 min readJan 5, 2018

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.

A performance do cálculo do próximo estado variou de 4 a 9ms para uma matriz de 800x450.

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

--

--

Willian Martins
Training Center

JS formatter/CSS tweaker @eBay. From São Paulo Brazil, but lives in Berlin. Sim racer gamer and Soccer fan.