Para melhorar o desempenho de aplicativos da web, use WebAssembly em conjunto com o AssemblyScript para reescrever componentes JavaScript de desempenho crítico de um aplicativo da web. “E isso realmente ajudará?” Você pergunta.
Infelizmente, não há uma resposta clara para essa pergunta. Tudo depende de como você os usa. As opções são muitas: em alguns casos a resposta será negativa, em outros será positiva. Em uma situação, é melhor escolher JavaScript em vez de AssemblyScript e, em outra, é o contrário. Isso é influenciado por muitas condições diferentes.
Neste artigo, iremos analisar essas condições, propor uma série de soluções e testá-las em vários exemplos de código de teste.
Quem sou eu e por que estou escrevendo este tópico?
(Você pode pular esta seção, não é essencial para a compreensão de mais material).
Eu realmente gosto da linguagem AssemblyScript . Até comecei a ajudar os desenvolvedores financeiramente em algum momento. Eles têm uma pequena equipe na qual todos estão seriamente apaixonados por este projeto. AssemblyScript é uma linguagem semelhante ao TypeScript muito jovem, capaz de compilar para WebAssembly (Wasm). Essa é precisamente uma de suas vantagens. Anteriormente, para usar o Wasm, um desenvolvedor da web precisava aprender linguagens estrangeiras como C, C ++, C #, Go ou Rust, uma vez que essas linguagens de alto nível com tipagem estática podiam ser compiladas em WebAssembly a partir do bem no início.
Embora o AssemblyScript (ASC) seja semelhante ao TypeScript (TS), ele não está associado a essa linguagem e não é compilado para JS. Similaridade na sintaxe e semântica é necessária para facilitar o processo de "portar" de TS para ASC. Essa transferência basicamente se resume a adicionar anotações de tipo.
Sempre tive interesse em pegar o código JS, portá-lo para ASC, compilá-lo para Wasm e comparar o desempenho. Quando meu colega Ingvar me enviou um snippet de JavaScript para desfocar as imagens , decidi usá-lo. Fiz uma pequena experiência para ver se valia a pena explorar este tópico mais profundamente. Acabou valendo a pena. Como resultado, este artigo apareceu.
Para conhecer melhor o AssemblyScript, você pode conferir o site oficial , entrar no canal Discord ou assistir ao vídeo introdutório no meu canal no Youtube. E seguimos em frente.
Benefícios do WebAssembly
Como escrevi acima, por muito tempo a principal tarefa do Wasm era a capacidade de compilar código escrito em linguagens de uso geral de alto nível. Por exemplo, na Squoosh (uma ferramenta de processamento de imagem online), usamos bibliotecas do ecossistema C / C ++ e Rust. Essas bibliotecas não foram originalmente projetadas para uso em aplicativos da web, mas o WebAssembly torna isso possível.
Além disso, de acordo com a crença popular, compilar o código-fonte no Wasm também é necessário porque o uso de binários do Wasm permite que você agilize o trabalho de um aplicativo da web. Eu concordo, no mínimo, que sob condições ideais (de laboratório), os binários WebAssembly e JavaScript podemfornecem valores aproximadamente iguais de desempenho máximo. Isso dificilmente é possível em projetos de combate na web.
Na minha opinião, faz mais sentido pensar no WebAssembly como uma ferramenta de otimização para valores médios de desempenho de trabalho. Embora recentemente, Wasm tenha a capacidade de usar instruções SIMD e fluxos de memória compartilhada. Isso deve aumentar sua competitividade. Mas, em qualquer caso, como escrevi acima, tudo depende da situação específica e das condições iniciais.
A seguir, consideraremos várias dessas condições:
Falta de aquecimento
O mecanismo V8 JS processa o código-fonte e o apresenta como uma árvore de sintaxe abstrata (AST). Com base no AST construído, o interpretador Ignition otimizado gera bytecode. O bytecode resultante é obtido pelo compilador Sparkplug e na saída ele produz o código de máquina ainda não otimizado, com uma grande pegada. Durante a execução do código, V8 coleta informações sobre as formas (tipos) dos objetos usados e, em seguida, executa o compilador de otimização TurboFan. Ele gera instruções de máquina de baixo nível otimizadas para a arquitetura de destino com base nas informações coletadas sobre os objetos.
Você pode entender como os mecanismos JS funcionam estudando a tradução deste artigo .
Pipeline do mecanismo JS. Esquema geral
Por outro lado, WebAssembly usa tipagem estática, então você pode gerar código de máquina imediatamente a partir dele. O motor V8 tem um compilador de streaming Wasm chamado Liftoff. Como o Ignition, ele ajuda você a preparar e executar código não otimizado rapidamente. E depois disso, o mesmo TurboFan acorda e otimiza o código de máquina. Ele será executado mais rápido do que após a compilação do Liftoff, mas demorará mais para ser gerado.
A diferença fundamental entre o pipeline JavaScript e o pipeline WebAssembly: o mecanismo V8 não precisa coletar informações sobre objetos e tipos, uma vez que Wasm tem tipagem estática e tudo é conhecido com antecedência. Isso economiza tempo.
Falta de desotimização
O código de máquina que o TurboFan gera para JavaScript só pode ser usado enquanto as suposições de tipo forem mantidas. Digamos que o TurboFan gerou código de máquina, por exemplo, para uma função f com um parâmetro numérico. Então, ao encontrar uma chamada para esta função com um objeto em vez de um número, o mecanismo usa novamente a Ignição ou Sparkplug. Isso é chamado de desotimização.
Para WebAssembly, os tipos não podem ser alterados durante a execução do programa. Portanto, não há necessidade de tal desotimização. E os próprios tipos que o Wasm suporta são traduzidos organicamente em código de máquina.
Minimizando binários para grandes projetos
Wasm foi originalmente projetado com o formato de arquivo binário compacto em mente. Portanto, esses binários são carregados rapidamente. Mas em muitos casos, eles ainda saem mais do que gostaríamos (pelo menos em termos de volumes aceitos na rede). No entanto, com gzip ou brotli, esses arquivos serão bem compactados.
Ao longo dos anos, o JavaScript aprendeu muitas coisas fora da caixa: arrays, objetos, dicionários, iteradores, processamento de strings, herança de protótipos e assim por diante. Tudo isso está embutido em seu motor. E a linguagem C ++, por exemplo, pode se orgulhar de um escopo muito maior. E cada vez que você usa qualquer uma dessas abstrações de linguagem ao compilar para WebAssembly, o código correspondente subjacente deve ser incluído em seu binário. Esta é uma das razões para a proliferação de binários do WebAssembly.
Wasm realmente não sabe nada sobre C ++ (ou qualquer outra linguagem). Portanto, o tempo de execução do Wasm não fornece uma biblioteca C ++ padrão e o compilador deve adicioná-la a cada arquivo binário. Mas esse código precisa ser conectado apenas uma vez. Portanto, para projetos maiores, isso não afeta muito o tamanho resultante do binário Wasm, que no final é geralmente menor do que outros binários.
É claro que nem em todos os casos é possível tomar uma decisão informada comparando apenas os tamanhos dos binários. Se, por exemplo, o código-fonte do AssemblyScript é compilado em Wasm, então o binário realmente se tornará muito compacto. Mas quão rápido será? Eu me propus a comparar diferentes versões de binários JS e ASC com base em dois critérios ao mesmo tempo - velocidade e tamanho.
Portando para AssemblyScript
Como já escrevi, TypeScript e ASC são muito semelhantes em sintaxe e semântica. É fácil presumir que há semelhanças com JS, então a portabilidade é principalmente sobre a adição de anotações de tipo (ou substituição de tipos). Para começar a portar Glur , biblioteca JS para desfoque de imagem.
Mapeamento de tipo de dados
Os tipos de AssemblyScript integrados são implementados de forma semelhante aos tipos de máquina virtual Wasm (WebAssembly VM). Se no TypeScript, por exemplo, o tipo Number é implementado como um número de ponto flutuante de 64 bits (de acordo com o padrão IEEE754), então no ASC há vários tipos numéricos: u8, u16, u32, i8, i16, i32 , f32 e f64. Além disso, você pode encontrar tipos de dados compostos comuns (string, Array, ArrayBuffer, Uint8Array e assim por diante) na biblioteca padrão do AssemblyScript, que, com certas reservas, estão presentes no TypeScript e JavaScript. Não vou considerar aqui as tabelas de mapeamento de tipo AssemblyScript, TypeScript e Wasm VM, este é um assunto para outro artigo. A única coisa que quero observar é que o ASC implementa o tipo StaticArray, para o qual não encontrei análogos em JS e WebAssembly VM.
Finalmente, voltamos ao nosso código de exemplo da biblioteca glur.
JavaScript:
function gaussCoef(sigma) {
if (sigma < 0.5)
sigma = 0.5;
var a = Math.exp(0.726 * 0.726) / sigma;
/* ... more math ... */
return new Float32Array([
a0, a1, a2, a3,
b1, b2,
left_corner, right_corner
]);
}
AssemblyScript:
function gaussCoef(sigma: f32): Float32Array {
if (sigma < 0.5)
sigma = 0.5;
let a: f32 = Mathf.exp(0.726 * 0.726) / sigma;
/* ... more math ... */
const r = new Float32Array(8);
const v = [
a0, a1, a2, a3,
b1, b2,
left_corner, right_corner
];
for (let i = 0; i < v.length; i++) {
r[i] = v[i];
}
return r;
}
O fragmento de código AssemblyScript contém um loop adicional no final, já que não há como inicializar a matriz por meio do construtor. O ASC não implementa sobrecarga de função, portanto, neste caso, temos apenas um construtor Float32Array (lengthOfArray: i32). O AssemblyScript tem retornos de chamada, mas não fecha, então não há como usar .forEach () para preencher uma matriz com valores. Então eu tive que usar um loop for regular para copiar um elemento por vez.
Você deve ter notado que no snippet de código em AssemblyScript Math, Mathf. , 64- , — 32-. Math . - , , f32. . .
:
Levei muito tempo para entender: a escolha dos tipos é muito importante. Desfocar a imagem envolve operações de convolução, e isso é um monte de loops for passando por todos os pixels. Era ingênuo pensar que se todos os índices de pixel forem positivos, os contadores de loop também serão positivos. Eu não deveria ter escolhido o tipo u32 (inteiro sem sinal de 32 bits) para eles. Se qualquer um desses loops funcionar na direção oposta, ele se tornará infinito (o programa fará um loop):
let j: u32;
// ... many many lines of code ...
for (j = width — 1; j >= 0; j--) {
// ...
}
Não encontrei nenhuma outra dificuldade em portar.
Benchmarks de shell D8
Ok, os trechos de código bilíngüe estão prontos. Agora você pode compilar ASC para Wasm e executar os primeiros benchmarks.
Algumas palavras sobre o d8: trata-se de um shell de comando para o motor V8 (ele próprio não possui interface própria), que permite realizar todas as ações necessárias com Wasm e JS. Em princípio, d8 pode ser comparado ao Node, que repentinamente cortou a biblioteca padrão e apenas ECMAScript puro permaneceu. Se você não tiver uma versão compilada do V8 no local (como compilá-la é descrito aqui ), você não pode usar o d8. Para instalar o d8, use a ferramenta jsvu .
No entanto, como o título desta seção contém a palavra "Benchmarks", acho importante fornecer algum tipo de isenção de responsabilidade aqui: os números e os resultados que recebi referem-se ao código que escrevi nas linguagens de minha escolha em execução no meu computador (2020 MacBook Air M1) usando os scripts de teste que criei. Os resultados são, na melhor das hipóteses, diretrizes aproximadas. Portanto, seria precipitado fornecer estimativas quantitativas generalizadas do desempenho de AssemblyScript com WebAssembly ou JavaScript com V8 com base nelas.
Você pode ter outra pergunta: por que escolhi o d8 e não executei scripts no navegador ou no Node? Acredito que tanto o navegador quanto o Node, digamos, não são estéreis o suficiente para meus experimentos. Além da esterilidade necessária, o d8 possibilita o controle da tubulação do motor V8. Posso capturar qualquer cenário de otimização e usar, por exemplo, apenas Ignition, apenas Sparkplug ou Liftoff para que as características de desempenho não mudem no meio do teste.
Técnica experimental
Como escrevi acima, temos a oportunidade de "aquecer" o mecanismo JavaScript antes de executar o teste de desempenho. Durante este processo de aquecimento, o V8 faz a otimização necessária. Então, executei o programa de desfoque 5 vezes antes de iniciar as medições, depois executei 50 execuções e ignorei as 5 execuções mais rápidas e mais lentas para remover possíveis outliers e muitos outliers.
Veja o que aconteceu:
Por um lado, fiquei feliz que o Liftoff produziu um código mais rápido em comparação com o Ignition e o Sparkplug. Mas o fato de que o AssemblyScript, compilado em Wasm usando otimização, acabou sendo várias vezes mais lento do que o pacote JavaScript - TurboFan, fiquei confuso.
Embora mais tarde eu tenha admitido que as forças inicialmente não eram iguais: uma grande equipe de engenheiros tem trabalhado em JS e seu motor V8 por muitos anos, implementando otimização e outras coisas inteligentes. AssemblyScript é um projeto relativamente novo com uma equipe pequena. O próprio compilador ASC é de passagem única e coloca todos os esforços de otimização na biblioteca Binaryen... Isso significa que a otimização é feita no nível do bytecode do Wasm VM depois que a maior parte da semântica de alto nível já foi compilada. O V8 tem uma vantagem clara aqui. No entanto, o código de desfoque é muito simples - são as operações aritméticas usuais com valores da memória. Parecia que ASC e Wasm deveriam ter feito melhor com esta tarefa. Qual é o problema aqui?
Vamos cavar mais fundo
Eu rapidamente consultei os caras espertos da equipe V8 e os caras igualmente espertos da equipe do AssemblyScript (graças a Daniel e Max!). Descobriu-se que, ao compilar o ASC, a "verificação de limites" (valores de limite) não é iniciada.
O V8 pode examinar o código JS de origem a qualquer momento e compreender sua semântica. Ele usa essas informações para otimização repetida ou adicional. Por exemplo, você tem um ArrayBuffer contendo um conjunto de dados binários. Nesse caso, o V8 espera que seja mais razoável não apenas executar caoticamente as células de memória, mas usar um iterador por meio de um loop for ... of.
for (<i>variable</i> of <i>iterableObject</i>) {
<i>statement</i>
}
A semântica desse operador garante que nunca ultrapassemos os limites do array. Conseqüentemente, o compilador TurboFan não controla a verificação de limites. Mas antes de compilar ASC para Wasm, a semântica do AssemblyScript não é usada para essa otimização: toda a otimização é realizada no nível da máquina virtual WebAssembly.
Felizmente, o ASC ainda tem um trunfo na manga - a anotação não marcada (). Indica quais valores devem ser verificados para a possibilidade de sair dos limites.
- prev_prev_out_r = prev_src_r * coeff [6];
- linha [line_index] = prev_out_r;
As 2 linhas anteriores precisam ser reescritas da seguinte maneira:
+ prev_prev_out_r = prev_src_r * unchecked (coeff [6]);
+ desmarcado (linha [line_index] = prev_out_r);
Sim, há mais. Em AssemblyScript, matrizes digitadas (Uint8Array, Float32Array e assim por diante) são implementadas na imagem e semelhança de ArrayBuffer. No entanto, devido à falta de otimização de alto nível para acessar o elemento da matriz com índice i, cada vez que você tem que acessar a memória duas vezes: a primeira vez para carregar o ponteiro para o primeiro elemento da matriz e a segunda vez para carregar o elemento em offset i * sizeOfType. Ou seja, você deve se referir ao array como um buffer (por meio de um ponteiro). No caso do JS, na maioria das vezes isso não acontece, já que o V8 consegue fazer uma otimização de alto nível do acesso ao array usando um único acesso à memória.
Para melhorar o desempenho, o AssemblyScript implementa matrizes estáticas (StaticArray). Eles são semelhantes ao Array, exceto pelo fato de terem um comprimento fixo. E, conseqüentemente, não há necessidade de armazenar um ponteiro para o primeiro elemento da matriz. Se possível, use essas matrizes para acelerar seus programas.
Então, peguei o grupo AssemblyScript - TurboFan (funcionava mais rápido) e o chamei de ingênuo. Em seguida, adicionei as duas otimizações de que falei nesta seção e obtive uma variante chamada otimizada.
Muito melhor! Fizemos um progresso significativo. No entanto, o AssemblyScript ainda é mais lento do que o JavaScript. Isso é tudo que podemos fazer? [spoiler: não]
Ai esses silêncios
Os caras da equipe do AssemblyScript também me disseram que o sinalizador --optimize é equivalente a -O3s. Otimiza bem a velocidade de trabalho, mas não a leva ao máximo, pois ao mesmo tempo impede o crescimento do arquivo binário. O sinalizador -O3 otimiza apenas a velocidade e o faz até o fim. Usar -O3s por padrão parece correto, já que é comum em desenvolvimento web reduzir o tamanho dos binários, mas vale a pena? Pelo menos neste exemplo específico, a resposta é não: -O3s economiza insignificantes ~ 30 bytes, mas ignora uma queda significativa no desempenho:
Um único sinalizador do otimizador simplesmente vira o jogo: finalmente, o AssemblyScript superou o JavaScript (neste caso de teste específico!).
Não vou mais indicar o flag O3 na tabela, mas fique tranquilo: de agora em diante até o final do artigo, ele ficará invisível para nós.
Tipo de bolha
Para ter certeza de que o exemplo borrado não é apenas um acidente, decidi pegar outro. Peguei a implementação de classificação no StackOverflow e passei pelo mesmo processo:
- portou o código adicionando tipos;
- lançou o teste;
- otimizado;
- executei o teste novamente.
(Eu não testei a criação e preenchimento de uma matriz para classificação por bolha).
Fizemos de novo! Desta vez, com um ganho de velocidade ainda maior: o AssemblyScript otimizado é quase duas vezes mais rápido que o JavaScript. Mas isso não é tudo. Mais altos e baixos me aguardam novamente. Por favor, não mude!
Gerenciamento de memória
Alguns de vocês devem ter notado que ambos os exemplos não mostram realmente como trabalhar com a memória. Em JavaScript, o V8 cuida de todo o gerenciamento de memória (e coleta de lixo) para você. No WebAssembly, por outro lado, você acaba com um pedaço de memória linear e tem que decidir como usá-lo (ou melhor, Wasm tem que decidir). Quanto nossa tabela mudará se usarmos heap intensivamente?
Decidi dar um novo exemplo com uma implementação de heap binário... Durante o teste, eu preencho uma pilha com 1 milhão de números aleatórios (cortesia de Math.random ()) e pop () os retorna de volta, verificando se os números estão em ordem crescente. O esquema geral de trabalho permanece o mesmo: portar o código JS para ASC, compilar com a configuração ingênua, executar os testes, otimizar e executar os testes novamente:
80x mais lento do que JavaScript com TurboFan?! E 6x mais lento que o Ignition! O que deu errado?
Configurando o ambiente de execução
Todos os dados que geramos em AssemblyScript devem ser armazenados na memória. Mas precisamos ter certeza de não sobrescrever nada que já esteja lá. Como o AssemblyScript tende a imitar o comportamento do JavaScript, ele também tem um coletor de lixo e, quando compilado, adiciona esse coletor ao módulo WebAssembly. O ASC não quer que você se preocupe com quando alocar e quando liberar memória.
Neste modo (denominado incremental), funciona por padrão. Ao mesmo tempo, apenas cerca de 2 KB no arquivo gzip é adicionado ao módulo Wasm. O AssemblyScript também oferece modos alternativos, mínimo e stub. Os modos podem ser selecionados usando o sinalizador --runtime. Minimal usa o mesmo alocador de memória, mas um coletor de lixo mais leve que não inicia automaticamente, mas deve ser chamado manualmente. Isso é útil ao desenvolver aplicativos de alto desempenho (como jogos) onde você deseja controlar quando o coletor de lixo pausa seu programa. No modo stub, muito pouco código é adicionado ao módulo Wasm (~ 400B no formato gzip). Funciona rapidamente, uma vez que um alocador de backup é usado (mais detalhes sobre alocadores estão escritos aqui ).
Os alocadores redundantes são muito rápidos, mas não podem liberar memória. Pode parecer bobo, mas pode ser útil para instâncias "únicas" de módulos, onde depois de completar a tarefa, em vez de liberar memória, você exclui toda a instância WebAssembly e cria uma nova.
Vamos ver como nosso módulo funcionará em diferentes modos:
Tanto o mínimo quanto o stub nos aproximaram nitidamente do nível de desempenho do JavaScript. Eu quero saber porque? Conforme mencionado acima, mínimo e incremental usam o mesmo alocador. Ambos também têm um coletor de lixo, mas o minimal não o executa a menos que seja chamado explicitamente (o que eu não faço). Isso significa que a questão é que a coleta de lixo incremental inicia automaticamente, e geralmente o faz desnecessariamente. Bem, por que isso é necessário se ele só precisa rastrear um array?
Problema de alocação de memória
Depois de executar o módulo Wasm no modo de depuração (--debug) várias vezes, descobri que a velocidade do trabalho diminui devido à biblioteca libsystem_platform.dylib. Ele contém primitivas de nível de sistema operacional para gerenciamento de thread e memória. As chamadas para esta biblioteca são feitas de __new () e __renew (), que por sua vez são chamadas de Array # push: Entendo : há um problema de gerenciamento de memória aqui. Mas o JavaScript de alguma forma consegue processar rapidamente um array sempre crescente. Então, por que o AssemblyScript não pode fazer isso? Felizmente, o código-fonte da biblioteca padrão do AssemblyScript está disponível publicamente , então vamos dar uma olhada nesta sinistra função push () da classe Array:
[Bottom up (heavy) profile]:
ticks parent name
18670 96.1% /usr/lib/system/libsystem_platform.dylib
13530 72.5% Function: *~lib/rt/itcms/__renew
13530 100.0% Function: *~lib/array/ensureSize
13530 100.0% Function: *~lib/array/Array#push
13530 100.0% Function: *binaryheap_optimized/BinaryHeap#push
13530 100.0% Function: *binaryheap_optimized/push
5119 27.4% Function: *~lib/rt/itcms/__new
5119 100.0% Function: *~lib/rt/itcms/__renew
5119 100.0% Function: *~lib/array/ensureSize
5119 100.0% Function: *~lib/array/Array#push
5119 100.0% Function: *binaryheap_optimized/BinaryHeap#push
export class Array<T> {
// ...
push(value: T): i32 {
var length = this.length_;
var newLength = length + 1;
ensureSize(changetype<usize>(this), newLength, alignof<T>());
// ...
return newLength;
}
// ...
}
Até agora, tudo está correto: o novo comprimento da matriz é igual ao comprimento atual, aumentado por 1. Em seguida, uma chamada para a função garantirSize () segue para certificar-se de que há capacidade suficiente no buffer (capacidade) para o novo elemento.
function ensureSize(array: usize, minSize: usize, alignLog2: u32): void {
// ...
if (minSize > <usize>oldCapacity >>> alignLog2) {
// ...
let newCapacity = minSize << alignLog2;
let newData = __renew(oldData, newCapacity);
// ...
}
}
A função garantirSize (), por sua vez, verifica: A capacidade é menor que o novo minSize? Nesse caso, aloque um novo buffer minSize usando a função _renew (). Isso envolve a cópia de todos os dados do buffer antigo para o novo. Por esse motivo, nosso teste de preencher o array com 1 milhão de valores (um elemento após o outro) leva à realocação de uma grande quantidade de memória e cria muito lixo.
Em outras bibliotecas (como std :: vec em Rust ou fatiasem Go), o novo buffer tem o dobro da capacidade do antigo, o que ajuda a tornar o processo de trabalho com memória mais barato e lento. Estou trabalhando neste problema no ASC, e até agora a única solução é criar meu próprio CustomArray, com sua própria otimização de memória.
Agora incremental é tão rápido quanto mínimo e stub. Mas o JavaScript continua sendo o líder neste caso de teste. Eu provavelmente poderia fazer mais otimizações no nível da linguagem, mas este não é um artigo sobre como otimizar o próprio AssemblyScript. Eu já afundei fundo o suficiente.
Existem muitas otimizações simples que o compilador AssemblyScript pode fazer por mim. Para isso, a equipe ASC está trabalhando em um otimizador de IR (Intermediate Representation) de alto nível chamado AIR. Isso pode tornar o trabalho mais rápido e evitar que eu tenha que otimizar manualmente o acesso ao array todas as vezes? Provavelmente. Será mais rápido que o JavaScript? Difícil de dizer. Mas em qualquer caso, foi interessante para mim competir pelo ASC, avaliar as capacidades do JS e ver o que uma linguagem mais "madura" com ferramentas de compilação "muito inteligentes" pode alcançar.
Rust e C ++
Reescrevi o código em Rust, da forma mais idiomática que pude, e compilei em WebAssembly. Ele acabou sendo mais rápido do que o AssemblyScript (ingênuo), mas mais lento do que o nosso AssemblyScript otimizado com CustomArray. Em seguida, otimizei o módulo compilado do Rust da mesma maneira que o AssemblyScript. Com essa otimização, o módulo Wasm baseado em Rust é mais rápido que nosso AssemblyScript otimizado, mas ainda mais lento que JavaScript. Usei
a mesma abordagem para C ++, usando Emscripten para compilar para WebAssembly . Para minha surpresa, mesmo a primeira opção sem otimização acabou não sendo pior do que JavaScript.
Não há nenhum URL de imagem aqui. Eu mesmo fiz uma captura de tela.
As versões marcadas como idiomáticas foram influenciadas pelo código-fonte JS de qualquer maneira. Tentei usar meu conhecimento das expressões idiomáticas do Rust, C ++, mas a instalação estava firme na minha cabeça que eu estava fazendo a portabilidade. Tenho certeza de que alguém com mais experiência nessas linguagens poderia implementar a tarefa do zero e o código ficaria diferente.
Tenho certeza de que os módulos Rust e C ++ poderiam ser executados ainda mais rápido. Mas eu não tinha conhecimento profundo o suficiente dessas línguas para extrair mais delas.
Mais uma vez sobre o tamanho dos binários
Vamos dar uma olhada nos tamanhos dos binários após a compactação gzip. Comparados ao Rust e C ++, os binários do AssemblyScript são de fato muito mais leves.
E ainda ... recomendações
Escrevi sobre isso no início do artigo, e vou repeti-lo agora: os resultados são, na melhor das hipóteses, diretrizes aproximadas. Portanto, seria precipitado fornecer estimativas quantitativas generalizadas de produtividade com base nisso. Por exemplo, você não pode dizer que Rust é 1,2 vezes mais lento que JavaScript em todos os casos. Esses números dependem muito do código que escrevi, das otimizações que apliquei e da máquina que usei.
Ainda assim, acho que existem algumas diretrizes gerais que podemos aprender para ajudá-lo a entender melhor o tópico e tomar melhores decisões:
- Liftoff AssemblyScript Wasm-, , , Ignition SparkPlug JavaScript. JS-, WebAssembly — .
- V8 JavaScript-. WebAssembly , JavaScript, , .
- , , .
- Os módulos AssemblyScript são geralmente muito mais leves do que os módulos Wasm compilados de outras linguagens. Neste artigo, o binário do AssemblyScript não era menor do que o binário do JavaScript, mas o oposto é verdadeiro para módulos maiores, conforme declarado pela equipe de desenvolvimento ASC.
Se você não acredita em mim (e não precisa) e quer descobrir o código dos casos de teste por conta própria, aqui estão eles .
Nossos servidores podem ser usados para desenvolvimento de WebAssembly.
Cadastre-se pelo link acima ou clicando no banner e ganhe 10% de desconto no primeiro mês de aluguel de um servidor de qualquer configuração!