Binaryen adalah compiler dan toolchain
library infrastruktur untuk WebAssembly, yang ditulis dalam C++. Pendekatan ini bertujuan untuk membuat
mengompilasi ke WebAssemb yang intuitif, cepat, dan efektif. Dalam postingan ini, menggunakan
contoh bahasa mainan sintetis yang disebut ExampleScript, pelajari cara menulis
Modul WebAssembly di JavaScript menggunakan Binaryen.js API. Anda akan membahas
dasar-dasar pembuatan modul, penambahan fungsi ke modul, dan mengekspor
fungsi-fungsi lainnya dari modul. Ini akan memberi Anda pengetahuan
tentang keseluruhan
mekanisme kompilasi bahasa pemrograman
sebenarnya ke WebAssembly. Selain itu,
Anda akan mempelajari cara mengoptimalkan modul Wasm baik dengan Binaryen.js maupun di
command line dengan wasm-opt
.
Latar Belakang tentang Biner
Biner memiliki API C dalam satu {i>header<i}, dan juga dapat digunakan dari JavaScript. Model ini menerima input di Formulir WebAssembly, tetapi juga menerima persyaratan grafik alur kontrol untuk kompiler yang lebih memilih opsi tersebut.
Representasi perantara (IR) adalah struktur data atau kode yang digunakan secara internal oleh kompilator atau mesin virtual untuk merepresentasikan kode sumber. Biner IR internal menggunakan struktur data yang ringkas dan dirancang untuk paralel pembuatan dan pengoptimalan kode, menggunakan semua inti CPU yang tersedia. IR Biner dikompilasi ke WebAssembly karena menjadi bagian dari WebAssembly.
Pengoptimal biner memiliki banyak penerusan yang dapat meningkatkan ukuran dan kecepatan kode. Ini pengoptimalan bertujuan untuk membuat Biner cukup handal untuk digunakan sebagai compiler backend itu sendiri. Alat ini mencakup pengoptimalan khusus WebAssembly (yang kompilator serbaguna mungkin tidak bisa dilakukan), yang bisa Anda anggap sebagai Wasm minifikasi.
AssemblyScript sebagai contoh pengguna Binaryen
Biner digunakan oleh sejumlah proyek, misalnya, AssemblyScript, yang menggunakan Binaryen untuk mengompilasi dari bahasa mirip TypeScript langsung ke WebAssembly. Coba contoh di playground AssemblyScript.
Input AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Kode WebAssembly yang sesuai dalam bentuk tekstual yang dihasilkan oleh 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
)
)
Toolchain Binaryen
Toolchain Binaryen menawarkan sejumlah alat yang berguna untuk JavaScript
pengembang dan pengguna baris perintah. {i>Subset<i} dari alat ini tercantum dalam
mengikuti; tindakan
daftar lengkap alat yang ada
tersedia di file README
project.
binaryen.js
: Library JavaScript mandiri yang mengekspos metode Biner untuk membuat dan mengoptimalkan modul Wasm. Untuk build, lihat binaryen.js di npm (atau mengunduhnya langsung dari GitHub atau unpkg).wasm-opt
: Alat command line yang memuat WebAssembly dan menjalankan Binaryen IR meneruskannya.wasm-as
danwasm-dis
: Alat command line untuk merakit dan membongkar WebAssembly.wasm-ctor-eval
: Alat command line yang dapat menjalankan fungsi (atau bagian dari ) pada waktu kompilasi.wasm-metadce
: Alat command line untuk menghapus bagian dari file Wasm secara fleksibel cara yang tergantung pada bagaimana modul digunakan.wasm-merge
: Alat command line yang menggabungkan beberapa file Wasm menjadi satu file file, yang menghubungkan impor terkait ke ekspor. Seperti untuk JavaScript, tapi untuk Wasm.
Mengompilasi ke WebAssembly
Mengompilasi satu bahasa ke bahasa lain umumnya melibatkan beberapa langkah, yang paling yang penting tercantum dalam daftar berikut:
- Analisis leksikal: Pisahkan kode sumber menjadi token.
- Analisis sintaksis: Membuat hierarki sintaksis abstrak.
- Analisis semantik: Periksa error dan terapkan aturan bahasa.
- Pembuatan kode menengah: Membuat representasi yang lebih abstrak.
- Pembuatan kode: Menerjemahkan ke bahasa target.
- Pengoptimalan kode khusus target: Mengoptimalkan target.
Di dunia Unix, alat yang
sering digunakan untuk mengompilasi adalah
lex
dan
yacc
:
lex
(Lexical Analyzer Generator):lex
adalah alat yang menghasilkan leksikal analisa, juga dikenal sebagai lexer atau pemindai. Dibutuhkan serangkaian ekspresi dan tindakan terkait sebagai input, dan menghasilkan kode untuk penganalisis leksikal yang mengenali pola dalam kode sumber input.yacc
(Yet Another Compiler Compiler):yacc
adalah alat yang membuat parser untuk analisis sintaksis. Dibutuhkan deskripsi tata bahasa formal dari bahasa pemrograman sebagai input dan menghasilkan kode untuk parser. Parser biasanya menghasilkan hierarki sintaksis abstrak (AST) yang mewakili struktur hierarki kode sumber.
Contoh yang berhasil
Mengingat cakupan postingan ini, tidak mungkin membahas pemrograman yang lengkap bahasa, jadi demi kesederhanaan, pertimbangkan bahasa pemrograman sintetis yang disebut ExampleScript yang bekerja dengan mengekspresikan dan operasi generik melalui contoh konkret.
- Untuk menulis fungsi
add()
, Anda membuat kode contoh penambahan apa pun, misalnya2 + 3
. - Untuk menulis fungsi
multiply()
, Anda menulis, misalnya,6 * 12
.
Sesuai dengan peringatan dini, sama sekali tidak berguna, namun cukup sederhana sehingga leksikalnya
analyzer menjadi ekspresi reguler tunggal: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Selanjutnya, perlu ada parser. Sebenarnya, versi yang sangat sederhana dari
pohon sintaks abstrak dapat dibuat
menggunakan ekspresi reguler dengan
grup pengambilan bernama:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Perintah ExampleScript adalah satu perintah per baris, sehingga parser dapat memproses kode berdasarkan baris dengan memisahkan karakter baris baru. Ini cukup untuk memeriksa tiga langkah dari daftar berbutir sebelumnya, yaitu analisis leksikal, sintaksis analisis , dan analisis semantik. Kode untuk langkah-langkah ini ada di mengikuti listingan.
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),
};
});
}
}
Pembuatan kode menengah
Program ExampleScript dapat direpresentasikan sebagai hierarki sintaksis abstrak (meskipun cukup sederhana), langkah selanjutnya adalah membuat dan representasi perantara. Langkah pertama adalah buat modul baru di Binaryen:
const module = new binaryen.Module();
Setiap baris hierarki sintaks abstrak berisi triple yang terdiri dari
firstOperand
, operator
, dan secondOperand
. Untuk masing-masing keempat model
operator di ExampleScript, yaitu +
, -
, *
, /
,
fungsi perlu ditambahkan ke modul
dengan metode Module#addFunction()
Binaryen. Parameter
Metode Module#addFunction()
adalah sebagai berikut:
name
:string
, mewakili nama fungsi.functionType
:Signature
, mewakili tanda tangan fungsi.varTypes
:Type[]
, menunjukkan lokal tambahan, dalam urutan tertentu.body
:Expression
, konten fungsi.
Ada beberapa detail lainnya untuk
dilepaskan dan diuraikan dan
Dokumentasi biner
dapat membantu Anda menavigasi ruang, tetapi pada akhirnya, untuk +
ExampleScript
Anda akan menggunakan metode Module#i32.add()
sebagai salah satu dari
tersedia
operasi bilangan bulat.
Penambahan membutuhkan dua operand, summand pertama dan kedua. Untuk
agar benar-benar dapat dipanggil, harus disediakan
diekspor
dengan 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');
Setelah memproses hierarki sintaks abstrak, modul ini berisi empat metode,
tiga model yang menggunakan bilangan bulat, yaitu add()
berdasarkan Module#i32.add()
,
subtract()
berdasarkan Module#i32.sub()
, multiply()
berdasarkan
Module#i32.mul()
, dan pencilan divide()
berdasarkan Module#f64.div()
karena ExampleScript juga berfungsi dengan hasil floating point.
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 `/`.
Jika Anda berurusan dengan code base yang sebenarnya, terkadang akan ada kode mati yang tidak akan dipanggil. Untuk memasukkan kode mati secara artifisial (yang akan dioptimalkan dan dihilangkan di langkah berikutnya) dalam contoh berjalan contohScript kompilasi ke Wasm, maka fungsi yang tidak diekspor akan berfungsi.
// 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)),
]),
);
Compiler hampir siap sekarang. Hal ini tidak sepenuhnya diperlukan, tetapi pastinya
praktik yang baik untuk
memvalidasi modul
dengan metode Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Mendapatkan kode Wasm yang dihasilkan
Kepada
mendapatkan kode Wasm yang dihasilkan,
ada dua metode di Binaryen untuk mendapatkan
representasi tekstual
sebagai file .wat
dalam S-expression
sebagai format yang dapat
dibaca manusia, dan
representasi biner
sebagai file .wasm
yang dapat
langsung dijalankan di browser. Kode biner dapat
dijalankan langsung di {i>browser<i}. Untuk melihat bahwa perintah itu berhasil, mencatat ekspor dapat
membantu.
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);
Representasi tekstual lengkap untuk program ExampleScript dengan keempatnya
tercantum dalam daftar berikut ini. Perhatikan bagaimana kode yang mati masih ada,
tetapi tidak ditampilkan sesuai dengan 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)
)
)
)
Mengoptimalkan WebAssembly
Binaryen menawarkan dua cara untuk mengoptimalkan kode Wasm. Satu di Binaryen.js, dan satu untuk baris perintah. Solusi yang pertama menerapkan kumpulan standar pengoptimalan aturan secara default dan memungkinkan Anda menetapkan tingkat pengoptimalan dan penyusutan, kedua secara {i>default<i} tidak menggunakan aturan, tetapi memungkinkan penyesuaian penuh, artinya, dengan eksperimen yang memadai, Anda dapat menyesuaikan setelan untuk hasil yang optimal berdasarkan kode Anda.
Mengoptimalkan dengan Binaryen.js
Cara paling mudah untuk mengoptimalkan modul Wasm dengan Binaryen adalah dengan
memanggil metode Module#optimize()
Binaryen.js secara langsung, dan jika perlu
menyetel
optimalkan dan tingkat penyusutan.
// 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();
Cara ini akan menghapus kode mati yang diperkenalkan sebelumnya, sehingga
representasi tekstual versi Wasm dari mainan ExampleScript contoh tidak
lagi memuatnya. Perhatikan juga bagaimana pasangan local.set/get
dihapus oleh
langkah pengoptimalan
SimplifyLocals
(berbagai pengoptimalan terkait lokal) dan
Penyedot Debu
(menghapus kode yang jelas-jelas tidak diperlukan), dan return
dihapus oleh
RemoveUnusedBrs
(menghapus jeda dari lokasi yang tidak diperlukan).
(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)
)
)
)
Ada banyak
kartu pengoptimalan,
dan Module#optimize()
menggunakan fitur optimalkan dan penyusutan tertentu, bawaan
yang sudah ditetapkan. Untuk penyesuaian sepenuhnya, Anda perlu menggunakan alat command line wasm-opt
.
Mengoptimalkan dengan alat command line wasm-opt
Untuk penyesuaian penuh kartu yang akan digunakan, Binaryen menyertakan
Alat command line wasm-opt
. Untuk mendapatkan
daftar lengkap opsi pengoptimalan yang memungkinkan,
periksa pesan bantuan alat tersebut. Alat wasm-opt
mungkin yang paling populer
alat, dan digunakan oleh beberapa toolchain compiler untuk mengoptimalkan kode Wasm,
termasuk Emscripten,
J2CL,
Kotlin/Wasm,
dart2wasm,
wasm-pack, dan lainnya.
wasm-opt --help
Untuk memberikan gambaran tentang kartu-kartu tersebut, berikut adalah kutipan dari beberapa kartu yang dapat dipahami tanpa pengetahuan ahli:
- CodeFolding: Menghindari kode duplikat dengan menggabungkannya (misalnya, jika dua
if
grup memiliki beberapa petunjuk bersama di ujungnya). - DeadArgumentElimination: Penerusan pengoptimalan waktu link untuk menghapus argumen ke suatu fungsi jika selalu dipanggil dengan konstanta yang sama.
- MinifyImportsAndExports: Memperkecilnya ke
"a"
,"b"
. - DeadCodeElimination: Menghapus kode yang mati.
Terdapat
buku resep pengoptimalan
tersedia dengan beberapa tips untuk mengidentifikasi
beragam penanda yang lebih
penting dan layak dicoba terlebih dahulu. Misalnya, terkadang menjalankan wasm-opt
berulang kali berulang kali akan menyusutkan
input tersebut semakin banyak. Dalam kasus semacam itu, menjalankan
dengan
Flag --converge
terus melakukan iterasi sampai tidak ada
pengoptimalan lebih lanjut dan titik tetap adalah
tercapai.
Demo
Untuk melihat cara kerja konsep yang diperkenalkan dalam postingan ini, coba gunakan fungsi demo yang menyediakan input ExampleScript yang dapat Anda pikirkan. Juga pastikan untuk lihat kode sumber demo.
Kesimpulan
Binaryen menyediakan toolkit yang canggih untuk mengompilasi bahasa ke WebAssembly dan mengoptimalkan kode yang dihasilkan. Library JavaScript dan alat command line-nya menawarkan fleksibilitas dan kemudahan penggunaan. Posting ini menunjukkan prinsip-prinsip inti dari Kompilasi Wasm, yang menyoroti efektivitas dan potensi Binaryen untuk pengoptimalan maksimum. Meskipun banyak opsi untuk menyesuaikan antarmuka pengoptimalan memerlukan pengetahuan mendalam tentang internal Wasm, biasanya pengaturan {i>default<i} sudah bekerja dengan baik. Dengan demikian, selamat mengompilasi dan mengoptimalkan dengan Binaryen!
Ucapan terima kasih
Postingan ini ditinjau oleh Alon Zakai, Thomas Lively dan Rachel Andrew.