Binaryen est un compilateur et une chaîne d'outils.
d'infrastructure pour WebAssembly, écrite en C++. Elle vise à rendre
une compilation à WebAssembly intuitive, rapide et efficace. Dans cet article, nous allons utiliser
exemple de langage de jouet synthétique appelé ExampleScript, apprenez à écrire
modules WebAssembly en JavaScript à l'aide de l'API Binaryen.js. Vous aborderez
principes de base de la création de module, de l'ajout de fonctions au module et de l'exportation
du module. Cela vous permettra d'en savoir plus sur l'ensemble
de compilation de langages de programmation
réels dans WebAssembly. En outre,
vous apprendrez à optimiser les modules Wasm à l'aide de Binaryen.js et dans le
via la ligne de commande wasm-opt
.
Contexte sur Binaryen
Binaryen dispose d'une interface intuitive API C dans un seul en-tête et peuvent également être utilisées à partir de JavaScript. Il accepte les entrées Formulaire WebAssembly mais accepte également une règle générale graphique de flux de contrôle pour les compilateurs qui préfèrent cela.
Une représentation intermédiaire (IR) est la structure de données ou le code utilisé en interne par un compilateur ou une machine virtuelle pour représenter le code source. de Binaryen la protection infrarouge interne utilise des structures de données compactes et est conçue pour fonctionner en parallèle la génération et l'optimisation de code, en utilisant tous les cœurs de processeur disponibles. IR de Binaryen se compile vers WebAssembly, car il s'agit d'un sous-ensemble de WebAssembly.
L'optimiseur de Binaryen comporte de nombreuses passes qui peuvent améliorer la taille et la vitesse du code. Ces les optimisations visent à rendre Binaryen suffisamment puissant pour être utilisé comme compilateur seul votre backend. Elle inclut des optimisations spécifiques à WebAssembly (qui que les compilateurs à usage général ne peuvent pas faire), que vous pouvez considérer comme Wasm minimisation.
AssemblyScript comme exemple d'utilisateur de Binaryen
Binaryen est utilisé par un certain nombre de projets, par exemple, AssemblyScript, qui utilise Binaryen pour : compiler à partir d'un langage de type TypeScript directement vers WebAssembly. Essayer l'exemple dans AssemblyScript Playground.
Entrée AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Code WebAssembly correspondant, généré par Binaryen, sous forme textuelle:
(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
)
)
Chaîne d'outils Binaryen
La chaîne d'outils Binaryen offre un certain nombre d'outils utiles pour JavaScript
développeurs et utilisateurs de ligne de commande. Un sous-ensemble de ces outils est présenté dans la section
suivis ; la
liste complète des outils
est disponible dans le fichier README
du projet.
binaryen.js
: bibliothèque JavaScript autonome qui expose les méthodes Binaryen pour créer et optimiser des modules Wasm. Pour les builds, consultez binaryen.js sur npm (ou téléchargez-la directement sur GitHub ou unpkg).wasm-opt
: outil de ligne de commande qui charge WebAssembly et exécute Binaryen IR la transmet.wasm-as
etwasm-dis
: outils de ligne de commande permettant d'assembler et de désassembler WebAssembly.wasm-ctor-eval
: outil de ligne de commande capable d'exécuter des fonctions (ou des parties de fonctions) au moment de la compilation.wasm-metadce
: outil de ligne de commande permettant de supprimer des parties des fichiers Wasm dans un environnement tout dépend de la façon dont le module est utilisé.wasm-merge
: outil de ligne de commande qui fusionne plusieurs fichiers Wasm en un seul , en connectant les importations correspondantes aux exportations. Comme bundler pour JavaScript, mais pour Wasm.
Compiler sur WebAssembly
La compilation d'un langage dans un autre implique généralement plusieurs étapes, les plus les plus importants sont répertoriés dans la liste suivante:
- Analyse lexicale:divisez le code source en jetons.
- Analyse syntaxique:créer une arborescence syntaxique abstraite.
- Analyse sémantique:détectez les erreurs et appliquez les règles linguistiques.
- Génération de code intermédiaire:créez une représentation plus abstraite.
- Génération de code:traduisez dans la langue cible.
- Optimisation du code spécifique à la cible:effectuez une optimisation en fonction de la cible.
Dans le monde Unix, les outils fréquemment utilisés pour la compilation sont
lex
et
yacc
:
lex
(Lexical Analyzer Generator) :lex
est un outil qui génère des métriques lexicales analyseurs, aussi appelés lexers ou scanners. Il faut un ensemble de données et les actions correspondantes en entrée, puis génère du code pour un analyseur lexical qui reconnaît des modèles dans le code source d'entrée.yacc
(Yet Other Compiler Compiler):yacc
est un outil qui génère pour l'analyse syntaxique. Il faut une description grammaticale formelle d'un de programmation en entrée et génère du code pour un analyseur. Analyseurs produit généralement arbres syntaxiques abstraites qui représentent la structure hiérarchique du code source.
Exemple qui a fonctionné
Étant donné le champ d'application de cet article, il nous est impossible d'aborder une programmation complète Par souci de simplicité, envisagez d'utiliser un langage ou ExampleScript, qui fonctionne en exprimant des opérations génériques à travers des exemples concrets.
- Pour écrire une fonction
add()
, vous codez un exemple de toute addition, par exemple2 + 3
- Pour écrire une fonction
multiply()
, écrivez, par exemple,6 * 12
.
Comme indiqué dans l'avertissement préalable, cela ne sert à rien, mais assez simple pour sa compréhension lexicale
"Analyzer" pour qu'il corresponde à une expression régulière unique: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Ensuite, il doit y avoir un analyseur. En fait, une version très simplifiée de
une arborescence de syntaxe abstraite peut être créée en utilisant une expression régulière avec
groupes de capture nommés:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
Les commandes ExampleScript étant une par ligne, l'analyseur peut traiter le code au niveau de la ligne en effectuant une division sur les caractères de retour à la ligne. Cela suffit à vérifier Les trois étapes de la liste à puces précédente, à savoir l'analyse lexicale, la syntaxe l'analyse et l'analyse sémantique. Le code de ces étapes se trouve dans le la liste suivante.
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),
};
});
}
}
Génération de code intermédiaire
Maintenant que les programmes ExampleScript peuvent être représentés sous la forme d'une arborescence syntaxique abstraite, (simplifiée), l'étape suivante consiste à créer un schéma représentation intermédiaire. La première étape consiste à Créez un module dans Binaryen:
const module = new binaryen.Module();
Chaque ligne de l'arbre syntaxique abstrait
contient un triple composé de
firstOperand
, operator
et secondOperand
. Pour chacune des
quatre possibilités
dans ExampleScript, c'est-à-dire +
, -
, *
, /
, une nouvelle
la fonction doit être ajoutée au module.
avec la méthode Module#addFunction()
de Binaryen. Les paramètres de la fonction
Les méthodes Module#addFunction()
sont les suivantes:
name
:string
, représente le nom de la fonction.functionType
:Signature
, qui représente la signature de la fonction.varTypes
:Type[]
indique des éléments locaux supplémentaires, dans l'ordre donné.body
:Expression
, le contenu de la fonction.
Il y a d'autres détails à vous détendre et
Documentation Binaryen
peut vous aider à naviguer dans l'espace, mais, à terme, pour le fichier +
d'ExampleScript,
vous obtenez la méthode Module#i32.add()
, qui est l'une des nombreuses
disponible
entier d'opérations.
L'addition nécessite deux opérandes : le premier et le deuxième summand. Pour le
pour qu'elle puisse être appelée, elle doit être
exporté
avec 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');
Après avoir traité l'arborescence syntaxique abstraite, le module contient quatre méthodes :
et trois avec des nombres entiers, à savoir add()
(basé sur Module#i32.add()
),
subtract()
pour Module#i32.sub()
et multiply()
pour
Module#i32.mul()
et la valeur aberrante divide()
basée sur Module#f64.div()
car ExampleScript fonctionne également
avec les résultats à virgule flottante.
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 `/`.
Si vous avez affaire à de véritables codebases, il peut arriver qu'il y ait du code mort qui n'est jamais est appelé. Pour introduire artificiellement du code mort (qui sera optimisé et éliminés ultérieurement) dans l'exemple du code d'ExampleScript à Wasm, l'ajout d'une fonction non exportée fait le travail.
// 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)),
]),
);
Le compilateur est presque prêt. Ce n'est pas obligatoire, mais certainement
bonne pratique
valider le module
à l'aide de la méthode Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Obtenir le code Wasm obtenu
À
obtenir le code Wasm obtenu ;
deux méthodes existent dans Binaryen pour obtenir
représentation textuelle
en tant que fichier .wat
dans S-expression ;
en tant que format lisible par l'humain,
représentation binaire
sous forme de fichier .wasm
pouvant s'exécuter directement dans le navigateur. Le code binaire peut être
directement dans le navigateur. Pour voir que cela a fonctionné, la journalisation des exportations peut
pour vous aider.
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 représentation textuelle complète d'un programme ExampleScript contenant les quatre
est énumérée ci-dessous. Notez que le code mort
est toujours là,
mais qui n'est pas visible sur la capture d'écran
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)
)
)
)
Optimiser WebAssembly
Binaryen propose deux façons d'optimiser le code Wasm. un dans Binaryen.js. une pour la ligne de commande. Le premier applique l'ensemble standard d'optimisations par défaut, et vous permet de définir les niveaux d'optimisation et de réduction. La seconde n'utilise par défaut aucune règle, mais permet une personnalisation complète. Cela signifie qu'avec suffisamment d'expérimentation, vous pouvez adapter les paramètres des résultats optimaux en fonction de votre code.
Optimiser avec Binaryen.js
Le moyen le plus simple d'optimiser un module Wasm avec Binaryen consiste à
appeler directement la méthode Module#optimize()
de Binaryen.js et, si vous le souhaitez,
en paramétrant le
à optimiser et à réduire.
// 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();
Cela permet de supprimer le code mort introduit de manière artificielle auparavant,
représentation textuelle de la version Wasm de l'exemple de jouet ExampleScript non
ne le contient plus. Notez également que les paires local.set/get
sont supprimées par le
étapes d'optimisation
SimplifyLocals
(diverses optimisations liées aux paramètres locaux) et
Aspirateur
(supprime le code inutilement inutile), et return
est supprimé par
RemoveUnusedBrs
(il supprime les pauses dans les endroits qui ne sont pas nécessaires).
(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)
)
)
)
Il existe de nombreux
d'optimisation,
et Module#optimize()
utilise les niveaux d'optimisation et de réduction particuliers par défaut
ensembles de données. Pour une personnalisation complète, vous devez utiliser l'outil de ligne de commande wasm-opt
.
Optimiser les performances avec l'outil de ligne de commande Wasm-opt
Pour une personnalisation complète des cartes à utiliser, Binaryen inclut le
Outil de ligne de commande wasm-opt
. Pour obtenir un
la liste complète des options d'optimisation possibles ;
consultez le message d'aide de l'outil. L'outil wasm-opt
est probablement l'outil le plus utilisé
et est utilisé par plusieurs chaînes d'outils de compilation
pour optimiser le code Wasm.
y compris Emscripten,
J2CL
Kotlin/Wasm,
dart2wasm,
wasm-pack, etc.
wasm-opt --help
Pour vous donner un aperçu des cartes, voici un extrait de celles sont compréhensibles sans l'expertise d'un expert:
- CodeFolding:permet d'éviter le code en double en le fusionnant (par exemple, si deux
if
partagent des instructions). - DeadArgumentElimination:transmission d'optimisation du délai d'association pour supprimer des arguments à une fonction si elle est toujours appelée avec les mêmes constantes.
- MinifyImportsAndExports:réduit leur taille à
"a"
et"b"
. - DeadCodeElimination:supprimez le code mort.
Il y a un
livre de recettes pour l'optimisation
ainsi que des conseils pour identifier les options
importantes et qui valent
la peine d’être essayées en premier. Par exemple, si wasm-opt
est parfois exécuté
à plusieurs reprises réduit encore davantage l'entrée. Dans de tels cas, l’exécution
avec le
Option --converge
continue d'itérer jusqu'à ce qu'aucune autre optimisation ne se produise et qu'un point fixe soit
atteint.
Démo
Pour voir en pratique les concepts présentés dans cet article, amusez-vous avec l'API en lui fournissant toutes les entrées ExampleScript auxquelles vous pouvez penser. Veillez également à afficher le code source de la démonstration.
Conclusions
Binaryen fournit un kit d'outils puissant pour compiler des langages pour WebAssembly et pour optimiser le code obtenu. Sa bibliothèque JavaScript et ses outils de ligne de commande offrent flexibilité et facilité d'utilisation. Cet article a montré les principes fondamentaux de Compilation Wasm mettant en évidence l'efficacité et le potentiel de Binaryen pour une optimisation maximale. Même si la plupart des options de personnalisation les optimisations exigent une connaissance approfondie des composants internes de Wasm, généralement les paramètres par défaut fonctionnent déjà très bien. Nous sommes heureux de vous aider à compiler et optimiser avec Binaryen.
Remerciements
Ce post a été examiné par Alon Zakai, Thomas Lively et Rachel Andrew.