Binaryen to kompilator i łańcuch narzędzi,
biblioteki infrastruktury WebAssembly napisanej w języku C++. Ma to na celu
intuicyjna, szybka i skuteczna kompilacja do WebAssembly. W tym poście przy użyciu funkcji
Przykład syntetycznego języka zabawek o nazwie ExampleScript, naucz się pisać
Moduły WebAssembly w JavaScript przy użyciu interfejsu API Binaryen.js. Omówisz:
podstaw tworzenia modułów, dodawania do nich funkcji i eksportowania ich
z funkcji dostępnych w module. Dzięki temu poznasz ogólne
czyli kompilowania rzeczywistych języków programowania do WebAssembly. Dalej:
dowiesz się, jak zoptymalizować moduły Wasm zarówno za pomocą plików Binaryen.js, jak i w
wiersza poleceń z wasm-opt
.
Informacje o aplikacji Binaryen
Binaryen ma intuicyjny C API w jednym nagłówku, a także używanych za pomocą JavaScriptu. Akceptuje on też Formularz WebAssembly, lecz także akceptuje ogólne kontrolny wykres przepływu dla kompilatorów, którzy preferują taką opcję.
Reprezentacja pośrednia (IR) to używana struktura lub kod danych wewnętrznie przez kompilatora lub maszynę wirtualną do reprezentowania kodu źródłowego. Binaryen's wewnętrzna podczerwień korzysta z kompaktowych struktur danych i jest zaprojektowany z myślą generowanie i optymalizowanie kodu z wykorzystaniem wszystkich dostępnych rdzeni procesora. IR Binaryena kompiluje się do formatu WebAssembly, ponieważ jest podzbiorem WebAssembly.
Optymalizator Binaryen ma wiele kart, które mogą zwiększyć rozmiar i szybkość kodu. Te mają na celu zapewnienie wystarczającej wydajności Binaryen, aby można było używać go jako kompilatora z backendu. Obejmuje optymalizacje typowe dla WebAssembly (które kompilatory ogólnego przeznaczenia mogą nie działać), które mogą być nazywane Wasmem. minifikacji.
AssemblyScript jako przykładowy użytkownik Binaryen
Binaryen jest używany w wielu projektach, na przykład AssemblyScript, który używa Binaryen do a następnie skompilować je z języka podobnego do TypeScriptu bezpośrednio do WebAssembly. Przykład na placu zabaw na placu zabaw.
Dane wejściowe AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Odpowiadający mu kod WebAssembly w formie tekstowej wygenerowany przez 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
)
)
Łańcuch narzędzi Binaryen
Łańcuch narzędzi Binaryen zawiera wiele przydatnych narzędzi JavaScript
programistów
i użytkowników wiersza poleceń. Część tych narzędzi znajduje się na liście
obserwowany;
pełna lista dostępnych narzędzi
jest dostępny w pliku README
projektu.
binaryen.js
: samodzielna biblioteka JavaScript, która udostępnia metody Binaryen. w przypadku tworzenie i optymalizowanie modułów Wasm. Informacje o kompilacjach znajdziesz w sekcji binaryen.js na npm. (lub pobierz ją bezpośrednio z GitHub lub unpkg).wasm-opt
: narzędzie wiersza poleceń, które wczytuje WebAssembly i uruchamia Binaryen IR i ją omija.wasm-as
iwasm-dis
: narzędzia wiersza poleceń służące do montażu i demontażu WebAssembly.wasm-ctor-eval
: narzędzie wiersza poleceń, które może wykonywać funkcje (lub części ).wasm-metadce
: narzędzie wiersza poleceń do usuwania części plików Wasm z elastycznego narzędzia w zależności od sposobu użycia tego modułu.wasm-merge
: narzędzie wiersza poleceń, które scala wiele plików Wasm w jeden. , łącząc odpowiednie operacje importu z eksportami. Podobne do ale dla Wasm.
Kompiluję do WebAssembly
Skompilowanie jednego języka na inny obejmuje zwykle kilka kroków, ważne są wymienione na tej liście:
- Analiza leksykalna: podziel kod źródłowy na tokeny.
- Analiza składni: tworzenie abstrakcyjnego drzewa składni.
- Analiza semantyczna: sprawdzanie błędów i egzekwowanie reguł językowych.
- Średnio zaawansowane generowanie kodu: utwórz bardziej abstrakcyjną reprezentację.
- Generowanie kodu: przetłumacz na język docelowy.
- Optymalizacja kodu pod kątem wartości docelowych: optymalizacja pod kątem wartości docelowej.
W świecie Unix często używanymi narzędziami do kompilacji są:
lex
i
yacc
:
lex
(Generator analizatora lekarskiego):lex
to narzędzie do generowania np. lekserów lub skanerów. Potrzebny jest zestaw zwykłych wyrażenia i odpowiadające im działania jako dane wejściowe, a następnie generuje kod dla argumentu analizator leksykalny, który rozpoznaje wzorce w wejściowym kodzie źródłowym.yacc
(Yet Other Compiler Compiler):yacc
to narzędzie, które generuje do analizy składni. Występuje w nim formalny opis gramatyczny języka programowania jako danych wejściowych i generowania kodu dla parsera. Analizatory zwykle produkują abstrakcyjne drzewa składni (AST), które reprezentują hierarchiczną strukturę kodu źródłowego.
Praktyczny przykład
Biorąc pod uwagę zakres tego posta, nie możemy przedstawić pełnego programu, dlatego dla uproszczenia należy rozważyć bardzo ograniczone i bezużyteczne syntetyczny język programowania o nazwie ExampleScript, który wykorzystuje funkcje na konkretnych przykładach.
- Aby napisać funkcję
add()
, musisz napisać kod przykładowego dowolnego dodawania, np.2 + 3
- Aby napisać funkcję
multiply()
, napisz na przykład6 * 12
.
Zgodnie z wstępnym ostrzeżeniem jest ona całkowicie bezużyteczna, ale wystarczająco prosta, aby jej leksyczność
analizator, by były pojedynczym wyrażeniem regularnym: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Następnie potrzebny jest parser. Właściwie to bardzo uproszczona wersja
abstrakcyjne drzewo składni można utworzyć za pomocą wyrażenia regularnego
nazwane grupy przechwytywania:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Polecenia exampleScript są umieszczone po jednym w wierszu, więc parser może przetworzyć kod w kolejnych wierszach, dzieląc je na znaki nowego wiersza. To wystarczy, aby sprawdzić pierwsze 3 kroki z poprzedniej listy, czyli analiza leksykalna, składnia analiza i analiza semantyczna. Kod dla tych kroków znajduje się w poniżej.
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),
};
});
}
}
Generowanie kodu dla średnio zaawansowanych
Programy ExampleScript można przedstawić jako abstrakcyjne drzewo składni. (choć może to być dość uproszczone), następnym krokiem jest utworzenie abstrakcyjnego schematu pośrednia. Najpierw utwórz nowy moduł w binaryen:
const module = new binaryen.Module();
Każdy wiersz abstrakcyjnego drzewa składni zawiera potrójny ciąg:
firstOperand
, operator
i secondOperand
. Dla każdej z czterech możliwych
z operatorem ExampleScript, czyli +
, -
, *
, /
,
do modułu należy dodać funkcję
za pomocą metody Module#addFunction()
Binaryena. Parametry
Module#addFunction()
:
name
:string
reprezentuje nazwę funkcji.functionType
:Signature
, reprezentuje podpis funkcji.varTypes
:Type[]
oznacza dodatkowe lokalne w podanej kolejności.body
:Expression
– zawartość funkcji.
Zostało jeszcze kilka detali, które pozwolą Ci się rozluźnić,
Dokumentacja pliku binarnego
może ułatwić poruszanie się po pokoju, ale z czasem w przypadku skryptu +
ExampleScript
wyświetli się metoda Module#i32.add()
jako jedna z kilku
dostępne
operacji liczb całkowitych.
Dodawanie wymaga 2 operandów: pierwszego i drugiego sumy. W przypadku atrybutu
aby funkcja była rzeczywiście wywoływana, musi to być
wyeksportowane
dzięki funkcji 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');
Po przetworzeniu abstrakcyjnego drzewa składni moduł zawiera 4 metody:
trzy działają z liczbami całkowitymi, a konkretnie add()
na podstawie Module#i32.add()
,
subtract()
na podstawie danych z Module#i32.sub()
, multiply()
na podstawie
Module#i32.mul()
oraz wartość odstająca divide()
na podstawie Module#f64.div()
ponieważ ExampleScript działa też w przypadku wyników zmiennoprzecinkowych.
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 `/`.
Jeśli zajmujesz się prawdziwymi bazami kodu, może się zdarzyć, że powstanie martwy kod, który nigdy zostanie wywołany. Sztuczne wprowadzanie martwego kodu (zoptymalizowanego i w późniejszym kroku) w uruchomionym przykładzie skryptu kompilację danych do Wasm, dodanie niewyeksportowanej funkcji załatwia sprawę.
// 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)),
]),
);
Kompilator jest już prawie gotowy. Nie jest to bezwzględnie konieczne, ale na pewno
sprawdzoną metodę
Sprawdź moduł
za pomocą metody Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Uzyskiwanie wynikowego kodu Wasm
Do
pozyskanie powstałego kodu Wasm,
w Binaryen istnieją dwie metody pobierania
reprezentacja tekstowa
jako plik .wat
w wyrażeniu S
w formacie zrozumiałym dla człowieka,
reprezentacja binarna
jako plik .wasm
, który można uruchomić bezpośrednio w przeglądarce. Kod binarny może być
które można uruchomić bezpośrednio w przeglądarce. Aby się upewnić, że wszystko działa, logowanie eksportów może
pomocy.
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);
Pełna reprezentacja tekstowa programu ExampleScript ze wszystkimi 4 elementami
poniżej. Zwróć uwagę na to, że kod martwy jest nadal dostępny.
ale nie widać jej na zrzucie ekranu.
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)
)
)
)
Optymalizacja WebAssembly
Binaryen oferuje 2 sposoby optymalizacji kodu Wasm. w samym pliku Binaryen.js, dla wiersza poleceń. Pierwszy z nich stosuje standardowy zestaw optymalizacji. reguły i pozwala ustawić poziom optymalizacji i zmniejszania ta druga opcja domyślnie nie używa żadnych reguł, ale umożliwia pełne dostosowanie, Oznacza to, że po przeprowadzeniu wystarczającej ilości eksperymentów można dostosować ustawienia na podstawie Twojego kodu.
Optymalizacja za pomocą Binaryen.js
Najprostszym sposobem optymalizacji modułu Wasm za pomocą narzędzia Binaryen jest
bezpośrednio wywoływać metodę Module#optimize()
w pliku Binaryen.js i opcjonalnie
ustawianie
optymalizacji i zmniejszania.
// 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();
Powoduje to usunięcie martwego kodu, który został sztucznie wprowadzony, przez co kod
wersja tekstowa przykładu zabawki exampleScript w wersji Wasm – nie
nie zawiera żadnego ciągu znaków. Zwróć też uwagę, jak pary local.set/get
są usuwane przez funkcję
kroki optymalizacji
SimplifyLocals
(różne optymalizacje związane z lokalnymi) oraz
Odkurzacz
(usuwa oczywiście niepotrzebny kod), a return
jest usuwany przez
RemoveUnusedBrs
(usuwa przerwy z miejsc, które nie są potrzebne).
(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)
)
)
)
Jest ich wiele
przebiegi optymalizacji,
a Module#optimize()
korzysta z konkretnej optymalizacji i zmniejszania poziomów” domyślna
zestawów. Aby w pełni dostosować tę usługę, użyj narzędzia wiersza poleceń wasm-opt
.
Optymalizacja za pomocą narzędzia wiersza poleceń wasm-opt
Aby w pełni dostosować karty, które mają być używane, Binaryen zawiera:
Narzędzie wiersza poleceń wasm-opt
. Aby uzyskać
pełną listę możliwych opcji optymalizacji,
przeczytaj komunikat pomocy narzędzia. Narzędzie wasm-opt
jest prawdopodobnie najpopularniejsze
jest używany przez kilka łańcuchów narzędzi kompilacji do optymalizacji kodu Wasm,
w tym Emscripten,
J2CL,
Kotlin/Wasm,
dart2wasm,
wasm-pack i inne.
wasm-opt --help
Aby ułatwić sobie posługiwanie się karnetami, oto fragment kilku z nich, są zrozumiałe bez wiedzy specjalistycznej:
- CodeFolding: unika powielania kodu przez jego scalanie (na przykład jeśli 2 obiekty
if
grupy eksperymentalnej mają po swojej stronie udostępnione instrukcje). - DeadArgumentElimission: połączenie czasu optymalizacji czasu w celu usunięcia argumentów. do funkcji, jeśli jest ona zawsze wywoływana z tymi samymi stałymi.
- MinifyImportsAndExports: minimalizuje je do formatu
"a"
w języku:"b"
. - DeadCodeElimission: usunięcie martwego kodu.
Jest
książka kucharska dotycząca optymalizacji
wraz z kilkoma wskazówkami dotyczącymi rozpoznawania, które flagi są bardziej
warto sprawdzić w YouTube. Na przykład czasami uruchomiona jest usługa wasm-opt
wielokrotnie zmniejsza wartość wejściową, W takich przypadkach uruchomienie
z
Flaga --converge
cały czas powtarza się, dopóki optymalizacja nie zostanie zakończona, a stały punkt
udało się dotrzeć.
Prezentacja
Aby zobaczyć w praktyce zasady przedstawione w tym poście, użyj z dowolnymi danymi wejściowymi ExampleScript. Pamiętaj też o wyświetl kod źródłowy wersji demonstracyjnej.
Podsumowanie
Binaryen zapewnia zaawansowany zestaw narzędzi do kompilowania języków do WebAssembly oraz na optymalizację powstałego kodu. Biblioteka JavaScript i narzędzia wiersza poleceń są elastyczne i łatwe w użyciu. W tym poście przedstawiliśmy podstawowe zasady Kompilacja Wasm przedstawiająca skuteczność i potencjał aplikacji Binaryen podczas optymalizacji. Chociaż wiele opcji dostosowywania interfejsu Binaryen wymagają dogłębnej wiedzy o elementach wewnętrznych Wasm, ustawienia domyślne działają świetnie. Życzymy udanego kompilowania i optymalizacji dzięki Binaryen.
Podziękowania
Ten post zweryfikował Alon Zakai, Thomas Lively i Rachel Andrew