Binaryen ist ein Compiler und eine Toolchain.
Infrastrukturbibliothek für WebAssembly, geschrieben in C++. Ziel ist es,
intuitive, schnelle und effektive Kompilierung in WebAssembly. In diesem Beitrag wird mithilfe der
Beispiel für eine künstliche Spielzeugsprache namens ExampleScript. Hier erfahren Sie, wie Sie
WebAssembly-Module in JavaScript mit der Binaryen.js API. Sie werden die
Grundlagen der Modulerstellung, Hinzufügen von Funktionen zum Modul und Exportieren
aus dem Modul an. So erhalten Sie ein besseres Verständnis
zur Kompilierung tatsächlicher Programmiersprachen in WebAssembly. Außerdem
erfahren Sie, wie Sie Wasm-Module sowohl mit Binaryen.js
Befehlszeile mit wasm-opt
.
Hintergrundinformationen zu Binaryen
Binaryen hat eine intuitive C API in einer einzigen Kopfzeile und kann auch als aus JavaScript verwendet. Die Funktion akzeptiert Eingaben in WebAssembly-Formular akzeptiert aber auch eine allgemeine Kontrollflussdiagramm für Compiler, die das bevorzugen.
Eine Zwischendarstellung (IR) ist die verwendete Datenstruktur oder der verwendete Code intern durch einen Compiler oder eine virtuelle Maschine zur Darstellung von Quellcode. Binaryen Die interne Infrarotstrahlung verwendet kompakte Datenstrukturen und ist für vollständig parallele Angriffe ausgelegt Codegenerierung und -optimierung unter Verwendung aller verfügbaren CPU-Kerne IR von Binaryen wird zu WebAssembly kompiliert, da es sich dabei um eine Untergruppe von WebAssembly handelt.
Das Optimierungstool von Binaryen hat viele Durchläufe, mit denen die Codegröße und -geschwindigkeit verbessert werden kann. Diese Optimierungen zielen darauf ab, Binaryen so leistungsstark zu machen, dass es als Compiler verwendet werden kann. Back-End zu erstellen. Sie umfasst WebAssembly-spezifische Optimierungen (die Compiler für allgemeine Zwecke möglicherweise nicht), was Sie sich als Wasm vorstellen können, zu reduzieren.
AssemblyScript als Beispielnutzer von Binaryen
Binaryen wird von einer Reihe von Projekten verwendet, z. B. AssemblyScript, das Binaryen nutzt, um aus einer TypeScript-ähnlichen Sprache direkt in WebAssembly kompilieren. Beispiel ausprobieren im AssemblyScript-Playground wieder.
AssemblyScript-Eingabe:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Entsprechender von Binaryen generierter WebAssembly-Code in Textform:
(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
)
)
Die Binaryen-Toolchain
Die Binaryen-Toolchain bietet eine Reihe nützlicher Tools sowohl für JavaScript-
und Befehlszeilen-Nutzenden. Einige dieser Tools sind in der
Folgendes: die
vollständige Liste der enthaltenen Tools
ist in der README
-Datei des Projekts verfügbar.
binaryen.js
: Eine eigenständige JavaScript-Bibliothek, die Binaryen-Methoden verfügbar macht für Wasm-Module erstellen und optimieren. Informationen zu Builds finden Sie unter binaryen.js auf npm. (oder direkt von GitHub oder unpkg).wasm-opt
: Befehlszeilentool, das WebAssembly lädt und Binaryen IR ausführt übergibt.wasm-as
undwasm-dis
: Befehlszeilentools zum Zusammen- und Auseinanderbauen WebAssembly.wasm-ctor-eval
: Befehlszeilentool, das Funktionen (oder Teile von Funktionen) beim Kompilieren.wasm-metadce
: Befehlszeilentool zum Entfernen von Teilen von Wasm-Dateien in einer flexiblen Umgebung je nachdem, wie das Modul verwendet wird.wasm-merge
: Befehlszeilentool, das mehrere Wasm-Dateien zu einer einzigen zusammenführt -Datei und verknüpft die entsprechenden Importe dabei mit den Exporten. Gefällt mir Bundler für JavaScript, aber für Wasm.
Kompilierung in WebAssembly
Das Kompilieren einer Sprache in eine andere umfasst in der Regel mehrere Schritte. sind in der folgenden Liste aufgeführt:
- Lexikalische Analyse:Teilen Sie den Quellcode in Tokens auf.
- Syntaxanalyse:Erstellen Sie eine abstrakte Syntaxstruktur.
- Semantische Analyse:Sie können nach Fehlern suchen und Sprachregeln erzwingen.
- Codegenerierung in der Zwischenversion:Erstellen Sie eine abstraktere Darstellung.
- Codegenerierung:Übersetzen Sie sie in die Zielsprache.
- Zielspezifische Codeoptimierung: Optimieren Sie für das Ziel.
In der Unix-Welt werden häufig verwendete Kompilierungstools verwendet:
lex
und
yacc
:
lex
(Lexical Analyzer Generator):lex
ist ein Tool, das lexikalische Analysen generiert. Analysatoren, auch Lexer oder Scanner genannt. Dazu sind regelmäßige Ausdrücke und entsprechende Aktionen als Eingabe und generiert Code für ein Lexikalanalysator, der Muster im Eingabequellcode erkennt.yacc
(Yet Another Compiler Compiler):yacc
ist ein Tool, das Parser für die Syntaxanalyse. Dabei wird eine formelle Grammatikbeschreibung Programmiersprache als Eingabe und generiert Code für einen Parser. Parser produzieren in der Regel abstrakte Syntaxstrukturen (AST), die die hierarchische Struktur des Quellcodes darstellen.
Ein funktionierendes Beispiel
In Anbetracht des Umfangs dieses Beitrags ist es unmöglich, Der Einfachheit halber sollten Sie eine sehr begrenzte und nutzlose die synthetische Programmiersprache ExampleScript, die generische Operationen anhand konkreter Beispiele.
- Um eine
add()
-Funktion zu schreiben, codieren Sie ein Beispiel für eine beliebige Addition, z. B.2 + 3
. - Um eine
multiply()
-Funktion zu schreiben, schreiben Sie beispielsweise6 * 12
.
Gemäß der Vorwarnung ist das Ganze völlig nutzlos, aber einfach genug für seine lexikalische Sprache.
Analyzer zu einem einzelnen regulären Ausdruck: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Als Nächstes muss ein Parser vorhanden sein. Eine sehr vereinfachte Version von
kann ein abstrakter Syntaxbaum mithilfe eines regulären Ausdrucks mit
Benannte Erfassungsgruppen:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
BeispielScript-Befehle stehen pro Zeile, sodass der Parser den Code verarbeiten kann. zeilenweise durch Aufteilen nach Zeilenumbruchzeichen. Das reicht aus, um die erste drei Schritte aus der Aufzählungsliste zuvor, nämlich lexikalische Analyse, Syntax Analyse und semantische Analyse. Der Code für diese Schritte befindet sich in der folgenden Eintrag.
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),
};
});
}
}
Codegenerierung in der fortgeschrittenen Phase
Da ExampleScript-Programme jetzt als abstrakte Syntaxstruktur dargestellt werden können, Der nächste Schritt besteht darin, eine abstrakte eine Zwischendarstellung. Der erste Schritt besteht darin, Erstellen Sie ein neues Modul in Binaryen:
const module = new binaryen.Module();
Jede Zeile des abstrakten Syntaxbaums enthält ein Dreifach, das aus
firstOperand
, operator
und secondOperand
. Für jede der vier möglichen
in ExampleScript, also +
, -
, *
, /
, eine neue
Funktion muss dem Modul hinzugefügt werden.
mit der Module#addFunction()
-Methode von Binaryen. Die Parameter der
Module#addFunction()
-Methoden:
name
: Einstring
, der den Namen der Funktion darstellt.functionType
: EinSignature
, das die Signatur der Funktion darstellt.varTypes
: EinType[]
, das zusätzliche Ortsansässige in der angegebenen Reihenfolge angibt.body
: einExpression
, der Inhalt der Funktion.
Es gibt einige weitere Details zum Entspannen und Abschalten sowie
Binaryen-Dokumentation
kann Ihnen bei der Navigation im Gruppenbereich helfen, doch letztendlich ist es für +
von ExampleScript hilfreich,
wird die Methode Module#i32.add()
als eine von mehreren
verfügbar
Ganzzahlvorgänge.
Für die Addition sind zwei Operanden erforderlich: der erste und der zweite Summand. Für die
tatsächlich aufrufbar ist, muss sie
exportiert
mit 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');
Nach der Verarbeitung des abstrakten Syntaxbaums enthält das Modul vier Methoden:
drei mit Ganzzahlen arbeiten, nämlich add()
basierend auf Module#i32.add()
,
subtract()
basierend auf Module#i32.sub()
, multiply()
basierend auf
Module#i32.mul()
und der Ausreißer divide()
basierend auf Module#f64.div()
weil ExampleScript auch mit Gleitkommaergebnissen funktioniert.
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 `/`.
Bei einer echten Codebasis gibt es manchmal toten Code, der nie aufgerufen wird. Um toten Code künstlich einzuführen (der dann optimiert und in einem späteren Schritt gelöscht werden) Kompilierung in Wasm durch Hinzufügen einer nicht exportierten Funktion.
// 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)),
]),
);
Der Compiler ist jetzt fast fertig. Es ist nicht unbedingt notwendig,
bewährte Methode,
Modul validieren
mit der Methode Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Den resultierenden Wasm-Code abrufen
Bis
den resultierenden Wasm-Code abrufen,
In Binaryen gibt es zwei Methoden,
Textdarstellung
als .wat
-Datei in S-Ausdruck
in einem visuell lesbaren Format
Binärdarstellung
als .wasm
-Datei, die direkt im Browser ausgeführt werden kann. Der Binärcode kann
direkt im Browser ausgeführt werden. Um festzustellen, ob es funktioniert hat,
kann das Protokollieren der Exporte
Hilfe.
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);
Die vollständige Textdarstellung für ein ExampleScript-Programm mit allen vier
Operationen sind im Folgenden aufgeführt. Der tote Code ist immer noch da,
aber nicht wie im Screenshot des
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)
)
)
)
WebAssembly optimieren
Binaryen bietet zwei Möglichkeiten, Wasm-Code zu optimieren. eine in Binaryen.js selbst und eine für die Befehlszeile. Bei Ersterem wird der Standardsatz von Optimierungsmaßnahmen und ermöglicht es Ihnen, die Optimierungs- und Verkleinerungsebene sowie die verwendet standardmäßig keine Regeln, ermöglicht aber eine vollständige Anpassung. Wenn Sie also genug experimentiert haben, können Sie die Einstellungen anhand Ihres Codes optimale Ergebnisse erzielen.
Mit Binaryen.js optimieren
Der einfachste Weg zur Optimierung eines Wasm-Moduls mit Binaryen ist,
die Methode Module#optimize()
von Binaryen.js direkt aufrufen und optional
das Festlegen der
Optimierung und Verkleinerungsebene.
// 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();
Dadurch wird toter Code entfernt, der zuvor künstlich eingefügt wurde, sodass das
Textdarstellung der Wasm-Version des Beispielskripts für das Spielzeugbeispiel – Nr.
nicht mehr enthält. Beachten Sie auch, wie die local.set/get
-Paare durch die
Optimierungsschritte
SimplifyLocals
(sonstige ortsbezogene Optimierungen) und die
Staubsauger
(löscht offensichtlich nicht benötigten Code) und return
wird durch
RemoveUnusedBrs
Dadurch werden Werbeunterbrechungen an nicht benötigten Orten entfernt.
(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)
)
)
)
Es gibt viele
Optimierungspässe,
Module#optimize()
verwendet die entsprechenden Optimierungs- und Verkleinerungsebenen. Standard
festgelegt. Für eine vollständige Anpassung müssen Sie das Befehlszeilentool wasm-opt
verwenden.
Mit dem Befehlszeilentool wasm-opt optimieren
Für eine vollständige Anpassung der zu verwendenden Karten/Tickets enthält Binaryen die
wasm-opt
-Befehlszeilentool. Um eine
vollständige Liste der möglichen Optimierungsoptionen,
sehen Sie sich die Hilfemeldung des Tools an. Das wasm-opt
-Tool ist wahrscheinlich am beliebtesten
der Tools und wird von mehreren Compiler-Toolchains verwendet, um Wasm-Code zu optimieren.
einschließlich Emscripten,
J2CL
Kotlin/Wasm
dart2wasm
Wasm-Pack und andere.
wasm-opt --help
Um Ihnen ein Gefühl für die Pässe zu vermitteln, finden Sie hier einen Auszug aus den Karten, die auch ohne Fachwissen verständlich sind:
- CodeFolding: Vermeidet Codeduplikat durch Zusammenführen von Code (z. B. wenn zwei
if
Verzweigungen einige gemeinsame Anweisungen am Ende). - DeadArgumentElimination:Übergabe zur Optimierung der Linkzeit zum Entfernen von Argumenten wenn sie immer mit denselben Konstanten aufgerufen wird.
- MinifyImportsAndExports:Reduziert die Anzahl auf
"a"
,"b"
. - DeadCodeElimination:Entfernen Sie fehlerhaften Code.
Es gibt eine
Cookbook zur Optimierung
Dort finden Sie Tipps dazu, welche
Flags am häufigsten verwendet werden,
und erst einmal ausprobieren sollten. Wenn z. B. manchmal wasm-opt
ausgeführt wird,
verkleinert die Eingabe immer wieder. In solchen Fällen führt die Ausführung
mit dem
--converge
-Flag
iteriert weiter, bis keine weitere Optimierung erfolgt und ein bestimmter Punkt
erreicht haben.
Demo
Um die in diesem Post vorgestellten Konzepte in Aktion zu sehen, spielen Sie mit den eingebetteten Demo, indem Sie beliebige ExampleScript-Eingaben zur Verfügung stellen, die Ihnen einfallen. Achten Sie außerdem darauf, Quellcode der Demo ansehen
Ergebnisse
Binaryen bietet ein leistungsstarkes Toolkit zum Kompilieren von Sprachen in WebAssembly und Optimierung des resultierenden Codes. JavaScript-Bibliothek und Befehlszeilentools Flexibilität und Nutzerfreundlichkeit bieten. In diesem Beitrag wurden die Grundprinzipien Wasm-Kompilierung, die die Wirksamkeit und das Potenzial von Binaryen maximale Optimierung. Während viele der Optionen zum Anpassen der Optimierungen erfordern tiefes Wissen über die internen Strukturen von Wasm, in der Regel Standardeinstellungen funktionieren bereits gut. Viel Spaß beim Kompilieren und Optimieren mit Binaryen!
Danksagungen
Dieser Beitrag wurde von Alon Zakai geprüft. Thomas Lively und Rachel Andrew