Wasm ou não Wasm?

Nós da Linkurious estamos trabalhando no Linkurious Enterprise. É uma plataforma baseada na web que usa o poder dos gráficos e suas ferramentas de visualização para ajudar empresas e autoridades em todo o mundo a combater o crime financeiro.



Uma das principais características do Linkurious Enterprise é uma interface de visualização gráfica fácil de aprender e usar, projetada para leigos. Em 2015, frustrados com os recursos das bibliotecas JavaScript existentes para visualização de gráficos, começamos a desenvolver nossa própria biblioteca - Ogma.











Ogma é uma biblioteca JS de renderização e desempenho computacional destinada a renderizar estruturas de rede. Você pode ter visto como as estruturas de rede são renderizadas usando outras ferramentas JavaScript como D3.js ou Sigma.js. Não tínhamos os recursos dessas ferramentas. Era importante para nós que a solução que estávamos usando tivesse alguns recursos específicos para que atendesse a certos requisitos de desempenho. Não encontramos um ou outro em bibliotecas de terceiros. Portanto, decidimos desenvolver nossa própria biblioteca do zero.



Tarefa





Visualização da estrutura da rede



A biblioteca Ogma foi projetada para usar os algoritmos mais avançados. Todos os seus componentes, desde o mecanismo de renderização baseado em WebGL avançado até os web workers usados ​​em suas profundidades, têm como objetivo fornecer o melhor desempenho de renderização de rede disponível. Nosso objetivo era tornar a biblioteca muito interativa ao executar tarefas de longo prazo, e para que os principais algoritmos de visualização de gráficos implementados nela funcionassem de forma rápida e estável.



A tecnologia WebAssembly (Wasm), desde o momento em que foi relatada pela primeira vez, prometeu aos desenvolvedores da web um nível de desempenho que se compara favoravelmente com o que estava anteriormente disponível para eles. Ao mesmo tempo, os próprios desenvolvedores não precisaram fazer esforços excessivos para usar a nova tecnologia. Eles apenas tinham que escrever o código em alguma linguagem de alto desempenho que conheciam, que, após a compilação, poderia ser executada em um navegador.



Depois que a tecnologia Wasm se desenvolveu um pouco, decidimos testá-la e realizar alguns testes de desempenho antes de implementá-la. Em geral, antes de entrar no rápido trem Wasm sem olhar para trás, decidimos apostar no seguro.



O que nos atraiu no Wasm foi que essa tecnologia poderia muito bem resolver nossos problemas, usando recursos de memória e processador de forma mais eficiente do que JavaScript.



Nossa pesquisa



Nossa pesquisa começou com uma busca por um algoritmo orientado a gráficos que pudesse ser facilmente portado para diferentes linguagens usando estruturas de dados semelhantes.



Escolhemos o algoritmo de n-corpos. É freqüentemente usado como uma linha de base para comparar algoritmos de visualização de gráfico de força. Os cálculos realizados de acordo com este algoritmo são a parte mais intensiva de recursos dos sistemas de visualização. Se esta parte do trabalho de tais sistemas pudesse ser realizada de forma mais eficiente do que antes, isso teria um efeito muito positivo em todos os algoritmos de visualização de gráfico de força implementados no Ogma.



Benchmark



Existem mentiras, mentiras grosseiras e referências.



Max De Marzi



Desenvolver um benchmark “honesto” muitas vezes é uma tarefa impossível. O fato é que em um ambiente criado artificialmente, é difícil reproduzir cenários típicos do mundo real. Criar um ambiente adequado para sistemas complexos é sempre extremamente difícil. De fato, em um ambiente de laboratório é fácil controlar fatores externos, mas, na realidade, muitas influências inesperadas são como o "desempenho" de uma solução se parece.



No nosso caso, o benchmark teve como objetivo resolver um problema único e bem definido, para estudar o desempenho da implementação do algoritmo de n-corpos.



É um algoritmo claro e bem documentado usado por organizações conceituadas para medir o desempenho.linguagens de programação.



Como em qualquer teste justo, nós pré-definimos algumas regras para os vários idiomas que estudamos:



  • Diferentes implementações do algoritmo devem usar estruturas de código semelhantes.
  • É proibido usar vários processos ou vários threads.
  • O uso de SIMD é proibido .
  • Somente versões estáveis ​​de compiladores são testadas. O uso de lançamentos como nightly, beta, alpha, pre-alpha é proibido.
  • Apenas as versões mais recentes do compilador são usadas para cada idioma.


Depois de definir as regras, já podemos prosseguir com a implementação do algoritmo. Mas antes de fazer isso, também tivemos que escolher as línguas cujo desempenho iremos investigar.



Concorrentes JS



Wasm é um formato binário para instruções executadas em um navegador. O código escrito em várias linguagens de programação é compilado neste formato. Eles falam sobre o código Wasm como um código legível por humanos, mas escrever programas diretamente no Wasm não é uma decisão que uma pessoa sã pode tomar. Como resultado, conduzimos uma pesquisa sobre o benchmark e selecionamos os seguintes candidatos:





O algoritmo n-body foi implementado nessas três linguagens. O desempenho de várias implementações foi comparado ao desempenho da implementação base do JavaScript.



Em cada uma das implementações do algoritmo, usamos 1000 pontos e rodamos o algoritmo com um número diferente de iterações. Medimos o tempo que levou para completar cada sessão do programa.



Os testes foram realizados utilizando o seguinte software e hardware:



  • NodeJS 12.9.1
  • Chrome 79.0.3945.130 (versão oficial) (64 bits)
  • clang 10.0.0 - para a versão do algoritmo implementado na linguagem C
  • emcc 1.39.6 - frontend para chamar o compilador Emscripten da linha de comando, uma alternativa para gcc / clang, bem como um linker
  • carga 1.40.0
  • wasm-pack 0.8.1
  • AssemblyScript 0.9.0
  • MacOS 10.15.2
  • Macbook Pro 2017 Retina
  • Intel Dual Core i5 2,3 GHz, 8 GB DDR3, SSD de 256 GB


Como você pode ver, para os testes não escolhemos o computador mais rápido, mas estamos testando o Wasm, ou seja, o código que será executado no contexto do navegador. E o navegador, de qualquer forma, geralmente não tem acesso a todos os núcleos de processador disponíveis no sistema e a toda a RAM.



Para torná-lo mais interessante, criamos várias versões de cada implementação do algoritmo. Em um ponto do sistema de n corpos, eles tinham representação numérica de coordenadas de 64 bits, no outro - 32 bits.



É importante notar também que usamos uma implementação "dupla" do algoritmo em Rust. Primeiro, sem usar nenhuma ferramenta Wasm, uma implementação Rust "nativa" e "insegura" foi escrita. Mais tarde, usando o wasm-pack, foi criada uma implementação Rust "segura" adicional. Esperava-se que esta implementação do algoritmo fosse mais fácil de integrar com JS e que fosse capaz de gerenciar melhor a memória no Wasm.



Testando



Executamos nossos experimentos em dois ambientes principais. Este é o Node.js e o navegador (Chrome).



Em ambos os casos, os benchmarks foram realizados usando um script quente. Ou seja, a coleta de lixo não foi iniciada antes da execução dos testes. Quando executamos a coleta de lixo depois de executar cada teste, isso não pareceu ter muito impacto nos resultados.



Com base no código-fonte escrito em AssemblyScript, o seguinte foi gerado:



  • Implementação JS básica do algoritmo.
  • Módulo Wasm.
  • Módulo Asm.js.


É interessante notar que o módulo asm.js não era totalmente compatível com asm.js. Tentamos adicionar uma diretiva "use asm" na parte superior do módulo, mas o navegador não aceitou essa otimização. Mais tarde, descobrimos que o compilador binaryen que usamos não estava realmente tentando tornar o código totalmente compatível com asm.js. Em vez disso, estava focado em formar algum tipo de versão JS eficiente do Wasm.



Primeiro executamos testes em Node.js.





Executando o código no ambiente Node.js



Em seguida, medimos o desempenho do mesmo código no navegador.





Executando o código em um navegador



Notamos imediatamente que a versão asm.js do código funciona mais lentamente do que as outras opções. Mas esses gráficos não nos permitem comparar claramente os resultados de várias variantes de código com a implementação JS básica do algoritmo. Portanto, para entender melhor tudo, construímos mais alguns diagramas.





Diferenças entre outras implementações do algoritmo e implementação JS (versão benchmark com coordenadas de pontos de 64 bits)





Diferenças entre outras implementações do algoritmo e implementação JS (versão de benchmark com coordenadas de ponto de 32 bits) As



versões de benchmark com coordenadas de ponto de 64 bits e 32 bits diferem acentuadamente. Isso pode nos levar a pensar que, em JavaScript, os números podem ser ambos. O fato é que os números em JS, na implementação do algoritmo, tomado como base de comparação, são sempre de 64 bits, mas os compiladores que convertem código de outras linguagens para Wasm trabalham com tais números de maneiras diferentes.



Em particular, isso tem um grande impacto na versão asm.js do teste. Sua versão com coordenadas de pontos de 32 bits é muito inferior em desempenho tanto à implementação JS básica quanto à versão asm.js, que usa números de 64 bits.



Nos diagramas anteriores, é difícil entender como o desempenho das outras variantes de código se relaciona com a variante JS. Isso ocorre porque as métricas do AssemblyScript são muito diferentes das demais. Para entender isso, construímos outro diagrama, removendo os resultados de asm.js.





Diferenças entre outras implementações do algoritmo da implementação JS (versão de benchmark com coordenadas de pontos de 64 bits, sem asm.js)





Diferenças entre outras implementações do algoritmo e implementação JS (versão de benchmark com coordenadas de pontos de 32 bits, sem asm.js).



Representações diferentes de números parecem ter influenciado outras versões do teste. Mas eles influenciaram de maneiras diferentes. Por exemplo, a variante C, que usava números de 32 bits (floats), tornou-se mais lenta do que a variante C, que usava números de 64 bits (double). Ambas as versões do Rust do teste com números de 32 bits (f32) tornaram-se mais rápidas do que suas versões com números de 64 bits (f64).



Implementação deficiente do algoritmo?



A análise dos dados acima pode sugerir o seguinte pensamento. Uma vez que todos os assemblies Wasm testados são muito próximos em desempenho às implementações de JavaScript, é possível que as implementações Wasm reflitam apenas os recursos de desempenho de implementações de algoritmo nativo?





Comparando implementações nativas de um algoritmo com uma implementação de JavaScript



Versões nativas de uma implementação de algoritmo são sempre mais rápidas do que uma implementação de JavaScript.



Também notamos que os assemblies Wasm são mais lentos do que as versões nativas do código usado para criar esses assemblies. A diferença de desempenho é de 20-50%. Descobrimos isso em uma versão reduzida do benchmark com 1000 iterações.





Implementação C e montagem Wasm correspondente





Implementação do Rust e compilação Wasm correspondente





Implementação do Rust, que foi criada usando o wasm-pack, e o assembly Wasm correspondente



Ao medir o tempo de execução do código nativo, o tempo necessário para carregar o programa também foi levado em consideração, e no caso de variantes do Wasm, este tempo não foi levado em consideração.



Resultado



Em média, o ganho de desempenho das duas implementações Rust do algoritmo, em comparação com sua implementação JS básica, foi de 20%. Isso provavelmente é bom para a imagem do Rust, mas é muito pouco ganho de desempenho em comparação com o esforço necessário para obtê-lo.



Que conclusões podemos tirar desses testes? E aqui estão algumas: a escrita cuidadosa do código JS permite que você obtenha um desempenho bastante alto e não requer a mudança para outras linguagens de programação.



Aprender novas linguagens de programação é sempre bom. Mas deve haver boas razões para aprender novas línguas. O desempenho costuma ser o motivo "errado", pois a arquitetura de alto nível do projeto afeta o desempenho mais do que compiladores ou micro-otimizações.



Como um experimento, mudamos JavaScript para TypeScript para escrever uma implementação do algoritmo de visualização de gráfico de força. Como resultado, melhoramos a qualidade da base de código, mas não o desempenho. Medimos especificamente o desempenho e descobrimos que, após a transição, ele aumentou ligeiramente, em 5%. Provavelmente, o motivo é a refatoração do código.



Se você está interessado em desenvolvimento e desempenho de JavaScript, talvez se interesse em assistir a esta palestra, que teve resultados semelhantes aos que obtivemos.



Como você aborda o desenvolvimento das partes "pesadas" dos projetos web?






All Articles