Binaryen adalah library infrastruktur compiler dan toolchain
untuk WebAssembly, yang ditulis dalam C++. Library ini bertujuan membuat
kompilasi ke WebAssembly menjadi intuitif, cepat, dan efektif. Dalam postingan ini, dengan menggunakan contoh bahasa mainan sintetis yang disebut ExampleScript, pelajari cara menulis modul WebAssembly di JavaScript menggunakan Binaryen.js API. Anda akan mempelajari
dasar-dasar pembuatan modul, penambahan fungsi ke modul, dan mengekspor
fungsi dari modul. Hal ini akan memberi Anda pengetahuan tentang mekanisme keseluruhan kompilasi bahasa pemrograman sebenarnya ke WebAssembly. Selanjutnya, Anda akan mempelajari cara mengoptimalkan modul Wasm dengan Binaryen.js dan di command line dengan wasm-opt
.
Latar belakang Binaryen
Binaryen memiliki C API intuitif dalam satu header, dan juga dapat digunakan dari JavaScript. Kode ini menerima input dalam bentuk WebAssembly, tetapi juga menerima grafik alur kontrol umum untuk compiler yang lebih menyukainya.
Representasi perantara (IR) adalah struktur data atau kode yang digunakan secara internal oleh compiler atau virtual machine untuk merepresentasikan kode sumber. IR internal Binaryen menggunakan struktur data yang ringkas dan dirancang untuk pengoptimalan dan pembuatan kode yang sepenuhnya paralel, menggunakan semua core CPU yang tersedia. IR Binaryen dikompilasi ke WebAssembly karena merupakan subset WebAssembly.
Pengoptimal Binaryen memiliki banyak proses yang dapat meningkatkan ukuran dan kecepatan kode. Pengoptimalan ini bertujuan membuat Binaryen cukup andal untuk digunakan sebagai backend kompiler dengan sendirinya. Hal ini mencakup pengoptimalan khusus WebAssembly (yang mungkin tidak dilakukan oleh compiler serbaguna), yang dapat Anda anggap sebagai minifikasi Wasm.
AssemblyScript sebagai contoh pengguna Binaryen
Binaryen digunakan oleh sejumlah project, misalnya, AssemblyScript, yang menggunakan Binaryen untuk mengompilasi dari bahasa seperti TypeScript langsung ke WebAssembly. Coba contohnya 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
Rangkaian alat Binaryen menawarkan sejumlah alat berguna bagi developer JavaScript dan pengguna command line. Sebagian alat ini tercantum di
bawah; daftar lengkap alat yang disertakan
tersedia di file README
project.
binaryen.js
: Library JavaScript mandiri yang mengekspos metode Binaryen untuk membuat dan mengoptimalkan modul Wasm. Untuk build, lihat binaryen.js di npm (atau download langsung dari GitHub atau unpkg).wasm-opt
: Alat command line yang memuat WebAssembly dan menjalankan proses Binaryen IR di dalamnya.wasm-as
danwasm-dis
: Alat command line yang merakit dan membongkar WebAssembly.wasm-ctor-eval
: Alat command line yang dapat menjalankan fungsi (atau bagian dari fungsi) pada waktu kompilasi.wasm-metadce
: Alat command line untuk menghapus bagian file Wasm secara fleksibel bergantung pada cara modul digunakan.wasm-merge
: Alat command line yang menggabungkan beberapa file Wasm menjadi satu file, menghubungkan impor yang sesuai ke ekspor saat melakukannya. Seperti bundler untuk JavaScript, tetapi untuk Wasm.
Mengompilasi ke WebAssembly
Mengompilasi satu bahasa ke bahasa lain biasanya melibatkan beberapa langkah, yang paling penting tercantum dalam daftar berikut:
- Analisis leksikal: Memecah kode sumber menjadi token.
- Analisis sintaksis: Buat hierarki sintaksis abstrak.
- Analisis semantik: Memeriksa error dan menerapkan aturan bahasa.
- Pembuatan kode menengah: Membuat representasi yang lebih abstrak.
- Pembuatan kode: Terjemahkan ke bahasa target.
- Pengoptimalan kode khusus target: Mengoptimalkan target.
Dalam dunia Unix, alat yang sering digunakan untuk mengompilasi adalah
lex
dan
yacc
:
lex
(Lexical Analyzer Generator):lex
adalah alat yang menghasilkan analisis leksikal, yang juga dikenal sebagai lexer atau scanner. Alat ini memerlukan serangkaian ekspresi reguler dan tindakan yang sesuai sebagai input, serta menghasilkan kode untuk analisis leksikal yang mengenali pola dalam kode sumber input.yacc
(Yet Another Compiler Compiler):yacc
adalah alat yang menghasilkan parser untuk analisis sintaksis. Alat ini menggunakan deskripsi tata bahasa formal dari bahasa pemrograman sebagai input dan menghasilkan kode untuk parser. Parser biasanya menghasilkan pohon sintaksis abstrak (AST) yang merepresentasikan struktur hierarki kode sumber.
Contoh yang sudah dikerjakan
Mengingat cakupan postingan ini, tidak mungkin untuk membahas bahasa pemrograman lengkap, jadi demi kesederhanaan, pertimbangkan bahasa pemrograman sintetis yang sangat terbatas dan tidak berguna yang disebut ExampleScript yang berfungsi dengan mengekspresikan 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 sebelumnya, sama sekali tidak berguna, tetapi cukup sederhana untuk analisis
leksikalnya menjadi satu ekspresi reguler: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Selanjutnya, harus ada parser. Sebenarnya, versi sangat sederhana dari
pohon sintaksis abstrak dapat dibuat dengan menggunakan ekspresi reguler dengan
grup pengambilan bernama:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Perintah ExampleScript masing-masing satu per baris, sehingga parser dapat memproses kode baris demi baris dengan membagi pada karakter baris baru. Ini sudah cukup untuk memeriksa tiga langkah pertama dari daftar berbutir sebelumnya, yaitu analisis leksikal, analisis sintaksis, dan analisis semantik. Kode untuk langkah-langkah ini ada dalam daftar berikut.
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
Sekarang setelah program ExampleScript dapat direpresentasikan sebagai pohon sintaksis abstrak (meskipun cukup sederhana), langkah berikutnya adalah membuat representasi perantara abstrak. Langkah pertama adalah membuat modul baru di Binaryen:
const module = new binaryen.Module();
Setiap baris pohon sintaksis abstrak berisi tiga tuple yang terdiri dari
firstOperand
, operator
, dan secondOperand
. Untuk setiap empat kemungkinan
operator di ExampleScript, yaitu +
, -
, *
, /
, fungsi baru perlu ditambahkan ke modul
dengan metode Module#addFunction()
Binaryen. Parameter metode
Module#addFunction()
adalah sebagai berikut:
name
:string
, merepresentasikan nama fungsi.functionType
:Signature
, mewakili tanda tangan fungsi.varTypes
:Type[]
, menunjukkan lokalitas tambahan, dalam urutan yang diberikan.body
:Expression
, konten fungsi.
Ada beberapa detail lagi yang perlu diuraikan dan dipecah, dan
dokumentasi Binaryen
dapat membantu Anda memahami ruang ini, tetapi pada akhirnya, untuk operator +
ExampleScript, Anda akan menggunakan metode Module#i32.add()
sebagai salah satu dari beberapa
operasi bilangan bulat yang tersedia.
Penambahan memerlukan dua operand, yaitu jumlah pertama dan jumlah kedua. Agar fungsi benar-benar dapat dipanggil, fungsi tersebut harus
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 pohon sintaksis abstrak, modul berisi empat metode, tiga di antaranya bekerja dengan 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 basis kode yang sebenarnya, terkadang ada kode tidak terpakai yang tidak pernah dipanggil. Untuk memperkenalkan kode tidak terpakai secara buatan (yang akan dioptimalkan dan dihilangkan pada langkah selanjutnya) dalam contoh kompilasi ExampleScript yang sedang berjalan ke Wasm, menambahkan fungsi yang tidak diekspor akan melakukan tugas tersebut.
// 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)),
]),
);
Kini compiler hampir siap. Hal ini tidak benar-benar diperlukan, tetapi merupakan praktik yang baik untuk memvalidasi modul dengan metode Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Mendapatkan kode Wasm yang dihasilkan
Untuk
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 browser. Untuk memastikan bahwa ekspor berhasil, pencatatan log 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 keempat
operasi tercantum di bawah ini. Perhatikan bagaimana kode tidak terpakai masih ada,
tetapi tidak diekspos seperti yang terlihat pada 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 itu sendiri, dan satu untuk command line. Yang pertama menerapkan serangkaian aturan pengoptimalan standar secara default dan memungkinkan Anda menetapkan tingkat pengoptimalan dan penciutan, dan yang kedua secara default tidak menggunakan aturan, tetapi memungkinkan penyesuaian penuh, yang berarti bahwa 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 secara opsional
menetapkan
tingkat optimasi dan 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();
Dengan demikian, kode tidak terpakai yang sebelumnya dimasukkan secara artifisial akan dihapus, sehingga representasi tekstual versi Wasm dari contoh mainan ExampleScript tidak lagi memuatnya. Perhatikan juga bagaimana pasangan local.set/get
dihapus oleh
langkah-langkah pengoptimalan
SimplifyLocals
(berbagai pengoptimalan terkait lokalitas) dan
Vacuum
(menghapus kode yang 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
proses pengoptimalan,
dan Module#optimize()
menggunakan set default tingkat pengoptimalan dan penyusutan tertentu. Untuk penyesuaian penuh, Anda harus menggunakan alat command line wasm-opt
.
Mengoptimalkan dengan alat command line wasm-opt
Untuk penyesuaian penuh pada pass yang akan digunakan, Binaryen menyertakan alat command line
wasm-opt
. Untuk mendapatkan
daftar lengkap opsi pengoptimalan yang mungkin,
periksa pesan bantuan alat. Alat wasm-opt
mungkin merupakan alat yang paling populer dan digunakan oleh beberapa toolchain compiler untuk mengoptimalkan kode Wasm, termasuk Emscripten, J2CL, Kotlin/Wasm, dart2wasm, wasm-pack, dan lainnya.
wasm-opt --help
Untuk memberi Anda gambaran tentang kartu ini, berikut kutipan beberapa kartu yang dapat dipahami tanpa pengetahuan ahli:
- CodeFolding: Menghindari kode duplikat dengan menggabungkannya (misalnya, jika dua
if
lengan memiliki beberapa petunjuk yang sama di ujungnya). - DeadArgumentElimination: Pengoptimalan waktu penautan meneruskan untuk menghapus argumen ke fungsi jika selalu dipanggil dengan konstanta yang sama.
- MinifyImportsAndExports: Meminimalkan impor dan ekspor ke
"a"
,"b"
. - DeadCodeElimination: Menghapus kode yang tidak digunakan.
Ada
buku panduan pengoptimalan
yang berisi beberapa tips untuk mengidentifikasi flag mana yang lebih
penting dan patut dicoba terlebih dahulu. Misalnya, terkadang menjalankan wasm-opt
berulang kali akan semakin mengecilkan input. Dalam kasus tersebut, menjalankan
dengan tanda
--converge
akan terus melakukan iterasi hingga tidak ada pengoptimalan lebih lanjut dan titik tetap tercapai.
Demo
Untuk melihat penerapan konsep yang diperkenalkan dalam postingan ini, coba demo yang disematkan dengan memberikan input ExampleScript apa pun yang dapat Anda pikirkan. Pastikan juga untuk melihat kode sumber demo.
Kesimpulan
Binaryen menyediakan toolkit canggih untuk mengompilasi bahasa ke WebAssembly dan mengoptimalkan kode yang dihasilkan. Library JavaScript dan alat command line-nya menawarkan fleksibilitas dan kemudahan penggunaan. Postingan ini menunjukkan prinsip inti kompilasi Wasm, yang menyoroti efektivitas dan potensi Binaryen untuk pengoptimalan maksimum. Meskipun banyak opsi untuk menyesuaikan pengoptimalan Binaryen memerlukan pengetahuan mendalam tentang internal Wasm, biasanya setelan default sudah berfungsi dengan baik. Dengan demikian, selamat mengompilasi dan mengoptimalkan dengan Binaryen.
Ucapan terima kasih
Postingan ini ditinjau oleh Alon Zakai, Thomas Lively, dan Rachel Andrew.