Binaryen è un compilatore e una toolchain
di infrastruttura per WebAssembly, scritta in C++. Punta a rendere
la compilazione in WebAssembly intuitiva, veloce ed efficace. In questo post, utilizzando
esempio di un linguaggio giocattolo sintetico chiamato ExampleScript, impara a scrivere
Moduli WebAssembly in JavaScript utilizzando l'API Binaryen.js. Tratteremo
nozioni di base sulla creazione di moduli, sull'aggiunta di funzioni al modulo ed sull'esportazione
dal modulo. In questo modo, potrai avere tutte le informazioni
i meccanismi per compilare linguaggi di programmazione
veri e propri in WebAssembly. Inoltre,
imparerai a ottimizzare i moduli Wasm sia con Binaryen.js che sul
dalla riga di comando wasm-opt
.
Informazioni su Binaryen
Binaryen ha un intuitivo API C in una singola intestazione e possono anche da JavaScript. Accetta input in Modulo WebAssembly, ma accetta anche un modello grafico di controllo del flusso per i compilatori che lo preferiscono.
Una rappresentazione intermedia (IR) è la struttura di dati o il codice utilizzato internamente da un compilatore o da una macchina virtuale per rappresentare il codice sorgente. Di Binaryen IR interno utilizza strutture dati compatte ed è progettato per essere completamente parallelo generazione e ottimizzazione di codice, utilizzando tutti i core CPU disponibili. IR di Binaryen viene compilato in WebAssembly poiché è un sottoinsieme di WebAssembly.
L'ottimizzatore di Binaryen ha molti passaggi che possono migliorare le dimensioni e la velocità del codice. Questi le ottimizzazioni mirano a rendere Binaryen abbastanza potente da poter essere usato come compilatore il backend da sé. Include ottimizzazioni specifiche di WebAssembly (che i compilatori generici potrebbero non farlo), cosa che si può pensare come Wasm la minimizzazione.
AssemblyScript come utente di esempio di Binaryen
Binaryen è utilizzato da diversi progetti, ad esempio AssemblyScript, che utilizza Binaryen per da un linguaggio simile a TypeScript, direttamente in WebAssembly. Prova l'esempio nel parco giochi AssemblyScript.
Input di AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Codice WebAssembly corrispondente in formato testuale generato da 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
)
)
La toolchain di Binaryen
La toolchain Binaryen offre una serie di strumenti utili sia per JavaScript
sviluppatori e utenti della riga di comando. Un sottoinsieme di questi strumenti è elencato nella
seguire; il
elenco completo degli strumenti contenuti
è disponibile nel file README
del progetto.
binaryen.js
: una libreria JavaScript autonoma che espone metodi Binaryen della creazione e ottimizzazione di moduli Wasm. Per le build, vedi binaryen.js su npm (o scaricalo direttamente dal GitHub o unpkg).wasm-opt
: strumento a riga di comando che carica WebAssembly ed esegue Binaryen IR ci passa sopra.wasm-as
ewasm-dis
: strumenti a riga di comando che consentono di assemblare e disassemblare WebAssembly.wasm-ctor-eval
: strumento a riga di comando in grado di eseguire funzioni (o parti di funzioni) al momento della compilazione.wasm-metadce
: strumento a riga di comando per rimuovere parti di file Wasm in un formato dipende da come viene usato il modulo.wasm-merge
: strumento a riga di comando che unisce più file Wasm in un unico e collega le importazioni corrispondenti alle esportazioni durante l'operazione. Ad esempio, bundler per JavaScript, ma per Wasm.
Compilazione in WebAssembly
Compilare una lingua a un'altra di solito comporta diversi passaggi, di cui quelle più importanti sono elencate nel seguente elenco:
- Analisi lexicale: suddivide il codice sorgente in token.
- Analisi della sintassi: crea un albero della sintassi astratta.
- Analisi semantica: verifica la presenza di errori e applica le regole relative al linguaggio.
- Generazione intermedia del codice:crea una rappresentazione più astratta.
- Generazione del codice:traduci nella lingua di destinazione.
- Ottimizzazione del codice specifico per target:ottimizza in base al target.
Nel mondo Unix, gli strumenti
di uso frequente per la compilazione sono
lex
e
yacc
:
lex
(Lexical Analyzer Builder):lex
è uno strumento che genera termini sessuali noti anche come lexer o scanner. Richiede una serie di controlli espressioni e azioni corrispondenti come input e genera il codice per analizzatore lessicale che riconosce pattern nel codice sorgente di input.yacc
(Yet Other Compiler Compiler):yacc
è uno strumento che genera per l'analisi della sintassi. Richiede una descrizione grammaticale formale di linguaggio di programmazione come input e genera codice per un parser. Parser solitamente producono albero della sintassi astratta (AST) che rappresentano la struttura gerarchica del codice sorgente.
Esempio corretto
Dato l'ambito di questo post, è impossibile coprire un'intera programmazione quindi, per semplicità, considera un linguaggio molto limitato e inutile un linguaggio di programmazione sintetico chiamato ExampleScript, che funziona esprimendo operazioni generiche tramite esempi concreti.
- Per scrivere una funzione
add()
, devi codificare un esempio di qualsiasi aggiunta, ad esempio2 + 3
. - Per scrivere una funzione
multiply()
, scrivi, ad esempio,6 * 12
.
Come da avviso, del tutto inutile, ma abbastanza semplice per il suo lessico
analizzatore in modo che sia una singola espressione regolare: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Poi deve essere presente un parser. In realtà, una versione molto semplificata di
è possibile creare una struttura di sintassi astratta utilizzando un'espressione regolare
gruppi di acquisizione con nome:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
I comandi ExampleScript sono uno per riga, pertanto l'analizzatore sintattico può elaborare il codice a riga di comando suddividendolo in caratteri di nuova riga. Questo è sufficiente per verificare la prima tre passaggi dall'elenco puntato precedente, ovvero analisi grammaticale, sintassi analisi e analisi semantica. Il codice per questi passaggi è disponibile nella seguente elenco.
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),
};
});
}
}
Generazione intermedia del codice
Ora che i programmi ExampleScript possono essere rappresentati come un albero di sintassi astratta (anche se semplificata), il passaggio successivo è creare un modello rappresentazione intermedia. Il primo passaggio consiste nel crea un nuovo modulo in Binaryen:
const module = new binaryen.Module();
Ogni riga dell'albero della sintassi astratta contiene una tripla composta da
firstOperand
, operator
e secondOperand
. Per ognuno dei quattro possibili
in ExampleScript, ovvero +
, -
, *
, /
, un nuovo
funzione deve essere aggiunta al modulo
con il metodo Module#addFunction()
di Binaryen. I parametri del parametro
I metodi di Module#addFunction()
sono i seguenti:
name
: unstring
, rappresenta il nome della funzione.functionType
: unSignature
, rappresenta la firma della funzione.varTypes
:Type[]
, indica altri locali nell'ordine indicato.body
: unExpression
, i contenuti della funzione.
Ci sono altri dettagli da esaminare e analizzare
Documentazione di Binaryen
può aiutarti a spostarti nello spazio, ma alla fine, per ExampleScript +
operatore, si finisce nel metodo Module#i32.add()
come uno dei vari
disponibili
operative relative ai numeri interi.
L'aggiunta richiede due operandi, il primo e il secondo sommando. Per
funzione sia effettivamente richiamabile, deve essere
esportato
con 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');
Dopo aver elaborato l'albero della sintassi astratta, il modulo contiene quattro metodi:
tre che lavorano con numeri interi, ovvero add()
in base a Module#i32.add()
,
subtract()
in base a Module#i32.sub()
, multiply()
in base a
Module#i32.mul()
e l'outlier divide()
in base a Module#f64.div()
perché ExampleScript funziona anche con i risultati in virgola mobile.
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 hai a che fare con codebase reali, a volte può capitare un codice guasto che non viene chiamato. Introdurre artificialmente codice morto (che verrà ottimizzato eliminato in un passaggio successivo) nell'esempio in esecuzione del file ExampleScript in Wasm, l'aggiunta di una funzione non esportata risolve il problema.
// 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)),
]),
);
Il compilatore è quasi pronto. Non è strettamente necessario, ma sicuramente
è buona norma
convalida il modulo
con il metodo Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Ottenere il codice Wasm risultante
A
ottenere il codice Wasm risultante,
esistono due metodi in Binaryen per ottenere
rappresentazione testuale
come file .wat
in S-expression
in un formato leggibile e
rappresentazione binaria
come file .wasm
eseguibile direttamente nel browser. Il codice binario può essere
vengono eseguiti direttamente nel browser. Per vedere se ha funzionato, registrando le esportazioni
guida.
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);
La rappresentazione testuale completa per un programma ExampleScript con tutti e quattro i programmi
di archiviazione sono elencate di seguito. Nota come il codice obsoleto sia ancora lì,
ma non viene mostrato come mostrato nello screenshot
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)
)
)
)
Ottimizzazione di WebAssembly
Binaryen offre due modi per ottimizzare il codice Wasm. Una nello stesso file Binaryen.js una per la riga di comando. La prima applica l'insieme standard di ottimizzazioni per impostazione predefinita e consente di impostare il livello di ottimizzazione e di restringimento mentre la seconda per impostazione predefinita non utilizza regole, ma consente una personalizzazione completa, il che significa che, con una sperimentazione sufficiente, potrai personalizzare le impostazioni a ottenere risultati ottimali in base al tuo codice.
Ottimizzare con Binaryen.js
Il modo più diretto per ottimizzare un modulo Wasm con Binaryen è
chiamare direttamente il metodo Module#optimize()
di Binaryen.js e, facoltativamente,
l'impostazione del
ottimizzare e ridurre il livello.
// 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();
In questo modo viene rimosso il codice morto introdotto artificialmente in precedenza,
rappresentazione testuale della versione Wasm del giocattolo ExampleScript, ad esempio no
più lungo. Nota anche che le coppie local.set/get
vengono rimosse
Passaggi per l'ottimizzazione
SimplifyLocals
(ottimizzazioni varie correlate a locali) e
Aspirapolvere
(rimuove il codice non necessario) e il parametro return
viene rimosso
RemoveUnusedBrs
(rimuove le pause dalle sedi non necessarie).
(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)
)
)
)
Esistono molti
pass per l'ottimizzazione,
e Module#optimize()
utilizza i livelli specifici di ottimizzazione e riduzione predefinita
set di dati. Per una personalizzazione completa, devi utilizzare lo strumento a riga di comando wasm-opt
.
Ottimizzazione con lo strumento a riga di comando wasm-opt
Per una completa personalizzazione delle tessere da utilizzare, Binaryen include il parametro
strumento a riga di comando wasm-opt
. Per ottenere un
l'elenco completo delle possibili opzioni di ottimizzazione,
consulta il messaggio della guida
dello strumento. Lo strumento wasm-opt
è probabilmente il più usato
degli strumenti e viene utilizzato da diverse catene di strumenti di compilazione per ottimizzare il codice Wasm,
tra cui Emscripten,
J2CL
Kotlin/Wasm
dart2wasm,
wasm-pack e altri.
wasm-opt --help
Per darti un'idea delle tessere, ecco un estratto di alcune sono comprensibili senza competenze specialistiche:
- CodeFolding: evita la duplicazione del codice unendolo (ad esempio, se due
if
gruppo ha istruzioni condivise alla fine). - DeadArgumentElimination: pass per l'ottimizzazione del tempo di collegamento per rimuovere gli argomenti. a una funzione se viene sempre chiamata con le stesse costanti.
- MiniifyImportsAndExports: le minimizza in
"a"
e"b"
. - DeadCodeElimination: rimuovi il codice obsoleto.
C'è un
Libro di ricette per l'ottimizzazione
disponibili con diversi suggerimenti per identificare quali delle varie segnalazioni sono più
sono importanti e che vale la pena provare prima. Ad esempio, a volte è in esecuzione wasm-opt
ripetutamente riduce ulteriormente l'input. In questi casi, la pubblicazione
con
--converge
flag
continua fino a quando non vengono eseguite ulteriori ottimizzazioni e non viene stabilito un punto fisso
raggiunto.
Demo
Per vedere i concetti introdotti in questo post in azione, gioca con le demo fornendo qualsiasi input di ExampleScript. Assicurati inoltre di visualizza il codice sorgente della demo.
Conclusioni
Binaryen fornisce un potente toolkit per compilare i linguaggi in WebAssembly e per ottimizzare il codice risultante. La libreria JavaScript e gli strumenti a riga di comando offrono flessibilità e facilità d'uso. Questo post illustra i principi fondamentali Compilation di Wasm che evidenzia l'efficacia e il potenziale di Binaryen la massima ottimizzazione. Anche se molte delle opzioni per personalizzare le ottimizzazioni richiedono una conoscenza approfondita degli aspetti interni di Wasm, le impostazioni predefinite funzionano benissimo. Buon lavoro anche per la compilazione e l'ottimizzazione con Binaryen!
Ringraziamenti
Questo post è stato esaminato da Alon Zakai, Thomas Lively e Rachel Andrew.