Binaryen é um compilador e um conjunto de ferramentas;
biblioteca de infraestrutura para WebAssembly, escrita em C++. Ele visa tornar
e fazer a compilação para o WebAssembly intuitiva, rápida e eficaz. Nesta postagem, usar o
exemplo de uma linguagem de brinquedos sintética chamada ExampleScript, aprenda a escrever
Módulos WebAssembly em JavaScript usando a API Binaryen.js. Você vai conhecer
conceitos básicos sobre criação de módulos, adição de funções ao módulo e exportação
do módulo. Isso lhe dará conhecimento sobre o
mecânica de compilação de linguagens de programação reais para o WebAssembly. Além disso,
você vai aprender a otimizar módulos Wasm com Binaryen.js e na
linha de comando com wasm-opt
.
Informações sobre Binaryen
O Binaryen tem uma visão intuitiva API C em um único cabeçalho e também podem ser usados com JavaScript. Ele aceita entrada Formulário WebAssembly, mas também aceita uma abordagem gráfico de fluxo de controle para compiladores que preferem isso.
Uma representação intermediária (IR) é a estrutura de dados ou código usado internamente por um compilador ou máquina virtual para representar o código-fonte. de Binaryen as IRs internas usam estruturas de dados compactas e foram projetadas para operações em paralelo e otimização de código usando todos os núcleos de CPU disponíveis. RI de Binaryen é compilado para WebAssembly por ser um subconjunto do WebAssembly.
O otimizador do Binaryen tem muitas passagens que podem melhorar o tamanho e a velocidade do código. Esses têm como objetivo tornar o binárioen poderoso o suficiente para ser usado como um compilador. back-end. Ela inclui otimizações específicas do WebAssembly (que compiladores de uso geral podem não fazer), o que pode considerar o Wasm minificação.
AssemblyScript como um exemplo de usuário do Binaryen
O Binaryen é usado em vários projetos, por exemplo, AssemblyScript, que usa Binaryen para compilar de uma linguagem semelhante a TypeScript diretamente para o WebAssembly. Teste o exemplo no playground do AssemblyScript.
Entrada do AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Código WebAssembly correspondente em formato de texto gerado por Binaryen:
(module
(type $0 (func (param i32 i32) (result i32)))
(memory $0 0)
(export "add" (func $module/add))
(export "memory" (memory $0))
(func $module/add (param $0 i32) (param $1 i32) (result i32)
local.get $0
local.get $1
i32.add
)
)
O conjunto de ferramentas binário
O conjunto de ferramentas Binário oferece várias ferramentas úteis para JavaScript
desenvolvedores e usuários de linha de comando. Um subconjunto dessas ferramentas está listado
seguindo as
lista completa de ferramentas contidas
está disponível no arquivo README
do projeto.
binaryen.js
: uma biblioteca JavaScript autônoma que expõe métodos bináriosen. para criação e otimização de módulos Wasm. Para builds, consulte binaryen.js no npm (ou faça o download diretamente GitHub ou unpkg).wasm-opt
: ferramenta de linha de comando que carrega o WebAssembly e executa IR binária. passa por ele.wasm-as
ewasm-dis
: ferramentas de linha de comando que montam e desmontam o WebAssembly.wasm-ctor-eval
: ferramenta de linha de comando que pode executar funções (ou partes de ) em tempo de compilação.wasm-metadce
: ferramenta de linha de comando para remover partes de arquivos Wasm em um formato flexível maneira que depende de como o módulo é usado.wasm-merge
: ferramenta de linha de comando que mescla vários arquivos Wasm em um único , conectando as importações correspondentes às exportações. Como um bundler para JavaScript, mas para Wasm.
Como compilar no WebAssembly
Compilar um idioma para outro geralmente envolve várias etapas, mais importantes estão listados na lista a seguir:
- Análise léxica:divida o código-fonte em tokens.
- Análise de sintaxe: crie uma árvore de sintaxe abstrata.
- Análise semântica:verifique se há erros e aplique regras de linguagem.
- Geração de código intermediária:crie uma representação mais abstrata.
- Geração de código:traduza para o idioma de destino.
- Otimização de código específico do destino:otimize para a meta.
No mundo Unix, as ferramentas frequentemente usadas para compilação são
lex
e
yacc
:
lex
(Gerador de analisador léxico):lex
é uma ferramenta que gera analisadores, também conhecidos como leitores léxicos ou scanners. É necessário um conjunto expressões e ações correspondentes como entrada e gera código para uma analisador léxico que reconhece padrões no código-fonte de entrada.yacc
(Yet Another Compiler Compiler): oyacc
é uma ferramenta que gera para análise sintática. É preciso uma descrição gramatical formal de um de programação como entrada e gera código para um analisador. Analisadores normalmente produzem árvores de sintaxe abstratas (ASTs) que representam a estrutura hierárquica do código-fonte.
Um exemplo prático
Devido ao escopo desta postagem, é impossível cobrir um resumo completo linguagem natural. Por isso, por uma questão de simplicidade, considere uma abordagem muito limitada uma linguagem de programação sintética chamada ExampleScript, que funciona expressando operações genéricas por meio de exemplos concretos.
- Para escrever uma função
add()
, codifique um exemplo de qualquer adição, por exemplo,2 + 3
. - Para criar uma função
multiply()
, escreva6 * 12
, por exemplo.
De acordo com o pré-aviso, completamente inútil, mas simples o suficiente para os termos léxicos
analisador como uma única expressão regular: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Em seguida, é necessário ter um analisador. Na verdade, uma versão muito simplificada da
uma árvore de sintaxe abstrata pode ser criada usando uma expressão regular com
grupos de captura nomeados:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Os comandos de ExampleScript são um por linha, de modo que o analisador possa processar o código linha, dividindo em caracteres de nova linha. Isso é suficiente para verificar as três etapas da lista com marcadores anteriores, que são análise léxica, sintaxe análise e análise semântica. O código dessas etapas está listagem a seguir.
export default class Parser {
parse(input) {
input = input.split(/\n/);
if (!input.every((line) => /\d+\s*[\+\-\*\/]\s*\d+\s*/gm.test(line))) {
throw new Error('Parse error');
}
return input.map((line) => {
const { groups } =
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/gm.exec(
line,
);
return {
firstOperand: Number(groups.first_operand),
operator: groups.operator,
secondOperand: Number(groups.second_operand),
};
});
}
}
Geração de código intermediária
Agora que os programas ExampleScript podem ser representados como uma árvore de sintaxe abstrata (embora bem simplificado), a próxima etapa é criar um representação intermediária. A primeira etapa é Crie um novo módulo em Binaryen:
const module = new binaryen.Module();
Cada linha da árvore de sintaxe abstrata contém um triplo que consiste em
firstOperand
, operator
e secondOperand
. Para cada uma das quatro opções
em ExampleScript, ou seja, +
, -
, *
, /
, uma nova
a função precisa ser adicionada ao módulo
com o método Module#addFunction()
de Binaryen. Os parâmetros do
Estes são os métodos Module#addFunction()
:
name
: umstring
representa o nome da função.functionType
: umSignature
representa a assinatura da função.varTypes
: umType[]
indica outros locais, na ordem indicada.body
: umExpression
, o conteúdo da função.
Há mais alguns detalhes para relaxar e analisar e a
Documentação do Binaryen
pode ajudar a navegar pelo espaço, mas, eventualmente, para +
do ExampleScript
operador, o método Module#i32.add()
é um dos vários
disponível
operações de números inteiros.
A adição requer dois operandos, o primeiro e o segundo somatório. Para o
seja realmente chamável, ela precisa ser
exportado
com Module#addFunctionExport()
.
module.addFunction(
'add', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.add(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32),
),
),
module.return(module.local.get(2, binaryen.i32)),
]),
);
module.addFunctionExport('add', 'add');
Depois de processar a árvore de sintaxe abstrata, o módulo contém quatro métodos:
três trabalhando com números inteiros, ou seja, add()
com base em Module#i32.add()
;
subtract()
com base em Module#i32.sub()
, multiply()
com base em
Module#i32.mul()
e o outlier divide()
com base em Module#f64.div()
porque ExampleScript também funciona com resultados de ponto flutuante.
for (const line of parsed) {
const { firstOperand, operator, secondOperand } = line;
if (operator === '+') {
module.addFunction(
'add', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.add(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32)
)
),
module.return(module.local.get(2, binaryen.i32)),
])
);
module.addFunctionExport('add', 'add');
} else if (operator === '-') {
module.subtractFunction(
// Skipped for brevity.
)
} else if (operator === '*') {
// Skipped for brevity.
}
// And so on for all other operators, namely `-`, `*`, and `/`.
Se você lidar com bases de código reais, às vezes haverá códigos mortos que nunca for chamado. Para introduzir artificialmente código morto (que será otimizado e eliminado em uma etapa posterior) no exemplo em execução do compilação no Wasm, basta adicionar uma função não exportada.
// This function is added, but not exported,
// so it's effectively dead code.
module.addFunction(
'deadcode', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.div_u(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32),
),
),
module.return(module.local.get(2, binaryen.i32)),
]),
);
O compilador está quase pronto agora. Não é estritamente necessário, mas definitivamente
prática recomendada para
validar o módulo
com o método Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Como encontrar o código Wasm resultante
Para
obter o código Wasm resultante,
existem dois métodos em Binaryen para conseguir
representação textual
como um arquivo .wat
em S-expression
como um formato legível por humanos, e a
representação binária
como um arquivo .wasm
que pode ser executado diretamente no navegador. O código binário pode ser
são executados diretamente no navegador. Para conferir se funcionou, registre as exportações
ajudar.
const textData = module.emitText();
console.log(textData);
const wasmData = module.emitBinary();
const compiled = new WebAssembly.Module(wasmData);
const instance = new WebAssembly.Instance(compiled, {});
console.log('Wasm exports:\n', instance.exports);
A representação textual completa de um programa ExampleScript com todos os quatro
operações está listada abaixo. Observe como o código inativo ainda está lá,
mas não é exposto de acordo com a captura de tela
WebAssembly.Module.exports()
(module
(type $0 (func (param i32 i32) (result i32)))
(type $1 (func (param f64 f64) (result f64)))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "multiply" (func $multiply))
(export "divide" (func $divide))
(func $add (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.add
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $subtract (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.sub
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $multiply (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.mul
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $divide (param $0 f64) (param $1 f64) (result f64)
(local $2 f64)
(local.set $2
(f64.div
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $deadcode (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.div_u
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
)
Como otimizar o WebAssembly
O Binaryen oferece duas maneiras de otimizar o código Wasm. Um no próprio Binaryen.js e um para a linha de comando. O primeiro aplica o conjunto padrão de otimização regras por padrão e permite definir os níveis de otimização e redução, e a por padrão, não usa regras, mas permite personalização completa, o que significa que, com testes suficientes, é possível personalizar as configurações para resultados ideais com base no seu código.
Como otimizar com Binaryen.js
A maneira mais direta de otimizar um módulo Wasm com Binaryen é
chamar diretamente o método Module#optimize()
de Binaryen.js e, opcionalmente,
definindo
otimizar e reduzir.
// Assume the `wast` variable contains a Wasm program.
const module = binaryen.parseText(wast);
binaryen.setOptimizeLevel(2);
binaryen.setShrinkLevel(1);
// This corresponds to the `-Os` setting.
module.optimize();
Isso remove o código inativo que foi introduzido artificialmente antes, de modo que
representação textual da versão Wasm do brinquedo Exemplo de não
já o contém. Observe também como os pares local.set/get
são removidos pelo
etapas de otimização
SimplifyLocals
(otimizações relacionadas a locais diversos) e o
Aspirador de pó
(remove o código obviamente desnecessário), e return
é removido
RemoveUnusedBrs
(remove intervalos de locais que não são necessários).
(module
(type $0 (func (param i32 i32) (result i32)))
(type $1 (func (param f64 f64) (result f64)))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "multiply" (func $multiply))
(export "divide" (func $divide))
(func $add (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.add
(local.get $0)
(local.get $1)
)
)
(func $subtract (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.sub
(local.get $0)
(local.get $1)
)
)
(func $multiply (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.mul
(local.get $0)
(local.get $1)
)
)
(func $divide (; has Stack IR ;) (param $0 f64) (param $1 f64) (result f64)
(f64.div
(local.get $0)
(local.get $1)
)
)
)
Existem várias
transmissões de otimização,
e Module#optimize()
usa os níveis específicos de otimização e redução padrão
conjuntos Para personalização completa, é necessário usar a ferramenta de linha de comando wasm-opt
.
Como otimizar com a ferramenta de linha de comando wasm-opt
Para a personalização completa dos passes a serem usados, o Binaryen inclui os
Ferramenta de linha de comando wasm-opt
. Para receber
lista completa das possíveis opções de otimização,
confira a mensagem de ajuda da ferramenta. A ferramenta wasm-opt
é provavelmente a mais conhecida
das ferramentas e é usado por vários conjuntos de ferramentas de compilador para otimizar o código Wasm,
incluindo Emscripten,
J2CL,
Kotlin/Wasm,
dart2wasm,
wasm-pack e outros.
wasm-opt --help
Para você ter uma ideia dos passes, aqui está um trecho de alguns dos que sejam compreensíveis sem conhecimento especializado:
- CodeFolding:evita código duplicado ao mescla-lo (por exemplo, se duas
if
grupos têm algumas instruções compartilhadas no final). - DeadElimination:a otimização de tempo de vinculação é transmitida para remover argumentos. a uma função se ela é sempre chamada com as mesmas constantes.
- MinifyImportsAndExports:reduz para
"a"
,"b"
. - DeadCodeElimination:remove o código inativo.
Há um
manual de otimização (link em inglês)
disponível com várias dicas para identificar quais das diversas sinalizações são mais
importante e que vale a pena tentar primeiro. Por exemplo, às vezes executar wasm-opt
repetidamente reduz a entrada ainda mais. Nesses casos, executar
com o
Flag --converge
continua iterando até que não aconteça mais nenhuma otimização e um ponto fixo seja
alcançado.
Demonstração
Para ver os conceitos apresentados nessa postagem em ação, teste a imagem fornecendo a ele qualquer entrada ExampleScript que você imaginar. Não se esqueça de confira o código-fonte da demonstração.
Conclusões
O Binaryen oferece um kit de ferramentas poderoso para a compilação de linguagens para WebAssembly e otimizar o código resultante. A biblioteca JavaScript e as ferramentas de linha de comando oferecem flexibilidade e facilidade de uso. Essa postagem demonstrou os princípios fundamentais de Compilação Wasm, destacando a eficácia e o potencial do Binaryen para otimização máxima. Embora muitas das opções de personalização dos modelos Binaryen otimizações exigem um profundo conhecimento sobre os componentes internos do Wasm, as configurações padrão já funcionam muito bem. Desejamos a você uma boa compilação e otimização. com a Binaryen!
Agradecimentos
Esta postagem foi revisada por Alon Zakai, Thomas Lively e Rachel André.