Binaryen adalah library infrastruktur compiler dan toolchain untuk WebAssembly, yang ditulis dalam C++. Library ini bertujuan untuk membuat
kompilasi ke WebAssembly menjadi 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 dari modul. Hal ini akan memberi Anda pengetahuan tentang mekanisme
keseluruhan untuk mengompilasi bahasa pemrograman yang sebenarnya ke WebAssembly. Selain itu,
Anda akan mempelajari cara mengoptimalkan modul Wasm dengan Binaryen.js dan di
command line dengan wasm-opt
.
Latar belakang tentang Binaryen
Binaryen memiliki C API yang intuitif dalam satu header, dan juga dapat digunakan dari JavaScript. Compiler ini menerima input dalam bentuk WebAssembly, tetapi juga menerima grafik alur kontrol umum untuk compiler yang lebih memilihnya.
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 pembuatan dan pengoptimalan kode yang sepenuhnya paralel, menggunakan semua core CPU yang tersedia. IR Binaryen dikompilasi ke WebAssembly karena merupakan subset dari WebAssembly.
Pengoptimal Binaryen memiliki banyak penerusan yang dapat meningkatkan ukuran dan kecepatan kode. Pengoptimalan ini bertujuan untuk membuat Binaryen cukup canggih untuk digunakan sebagai backend compiler itu sendiri. Hal ini mencakup pengoptimalan khusus WebAssembly (yang mungkin tidak dilakukan oleh compiler tujuan umum), yang dapat Anda anggap sebagai pengoptimalan 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 teks 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 bagi developer JavaScript
dan pengguna command line. Sebagian alat ini tercantum di
berikut;
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 pass IR Binaryen di dalamnya.wasm-as
danwasm-dis
: Alat command line yang menyusun 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 dengan cara yang fleksibel yang bergantung pada cara modul digunakan.wasm-merge
: Alat command line yang menggabungkan beberapa file Wasm menjadi satu file, yang 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 leksik: Membagi kode sumber menjadi token.
- Analisis sintaksis: Membuat hierarki sintaksis abstrak.
- Analisis semantik: Memeriksa error dan menerapkan 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 penganalisis leksikal, yang juga dikenal sebagai lexer atau pemindai. Ini memerlukan serangkaian ekspresi reguler dan tindakan yang sesuai sebagai input, dan menghasilkan kode untuk penganalisis leksikografis yang mengenali pola dalam kode sumber input.yacc
(Yet Another Compiler Compiler):yacc
adalah alat yang menghasilkan parser untuk analisis sintaksis. Model ini menggunakan 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 untuk membahas bahasa pemrograman lengkap, jadi untuk memudahkan, pertimbangkan bahasa pemrograman sintetis yang sangat terbatas dan tidak berguna yang disebut ExampleScript yang berfungsi dengan mengekspresikan operasi umum 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 awal, benar-benar tidak berguna, tetapi cukup sederhana untuk analis
leksikal-nya menjadi satu ekspresi reguler: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Selanjutnya, harus ada parser. Sebenarnya, versi sangat sederhana dari
hierarki sintaksis abstrak dapat dibuat menggunakan ekspresi reguler dengan
grup pengambilan bernama:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Perintah ExampleScript adalah satu per baris, sehingga parser dapat memproses kode per baris dengan memisahkan karakter baris baru. Hal ini cukup untuk memeriksa tiga langkah pertama dari daftar berbutir sebelumnya, yaitu analisis leksikografis, analisis sintaksis, dan analisis semantik. Kode untuk langkah-langkah ini ada dalam listingan 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
Setelah program ExampleScript dapat direpresentasikan sebagai hierarki sintaksis abstrak (meskipun cukup disederhanakan), langkah berikutnya adalah membuat representasi antara abstrak. Langkah pertama adalah membuat modul baru di Binaryen:
const module = new binaryen.Module();
Setiap baris hierarki sintaksis abstrak berisi tiga elemen yang terdiri dari
firstOperand
, operator
, dan secondOperand
. Untuk setiap dari 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
, mewakili nama fungsi.functionType
:Signature
, mewakili tanda tangan fungsi.varTypes
:Type[]
, menunjukkan lokalitas tambahan, dalam urutan yang diberikan.body
:Expression
, konten fungsi.
Ada beberapa detail lainnya yang perlu diurai dan dipecah, dan
dokumentasi Binaryen
dapat membantu Anda menjelajahi ruang, tetapi pada akhirnya, untuk operator +
ExampleScript, Anda akan berakhir di metode Module#i32.add()
sebagai salah satu dari beberapa
operasi bilangan bulat
yang tersedia.
Penambahan memerlukan dua operand, penjumlah pertama dan kedua. Agar fungsi 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 hierarki sintaksis abstrak, modul ini berisi empat metode,
tiga bekerja dengan bilangan bulat, yaitu add()
berdasarkan Module#i32.add()
,
subtract()
berdasarkan Module#i32.sub()
, multiply()
berdasarkan
Module#i32.mul()
, dan outlier 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 menangani basis kode sebenarnya, terkadang akan ada kode mati yang tidak pernah dipanggil. Untuk memperkenalkan kode mati secara artifisial (yang akan dioptimalkan dan dihilangkan pada langkah berikutnya) 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)),
]),
);
Sekarang compiler hampir siap. Hal ini tidak mutlak 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 ekspresi S
sebagai format yang dapat dibaca manusia, dan
representasi biner
sebagai file .wasm
yang dapat langsung dijalankan di browser. Kode biner dapat
berjalan langsung di browser. Untuk melihat apakah proses ini berhasil, logging 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 bahwa kode mati masih ada,
tetapi tidak ditampilkan sesuai 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 kumpulan aturan pengoptimalan standar secara default dan memungkinkan Anda menetapkan tingkat pengoptimalan dan penyingkatan, dan yang kedua secara default tidak menggunakan aturan, tetapi memungkinkan penyesuaian penuh, yang berarti bahwa dengan eksperimen yang cukup, 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
langsung memanggil metode Module#optimize()
dari Binaryen.js, dan secara opsional
menetapkan
tingkat pengoptimalan dan penyingkatan.
// 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();
Tindakan ini akan menghapus kode mati yang diperkenalkan secara artifisial sebelumnya, sehingga
representasi tekstual dari contoh mainan ExampleScript versi Wasm tidak
lagi berisinya. Perhatikan juga cara pasangan local.set/get
dihapus oleh
langkah pengoptimalan
SimplifyLocals
(pengoptimalan terkait lokalitas lainnya) 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
putaran pengoptimalan,
dan Module#optimize()
menggunakan kumpulan default
tingkat pengoptimalan dan penyingkatan tertentu. Untuk penyesuaian penuh, 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 optimasi yang mungkin,
periksa pesan bantuan alat. Alat wasm-opt
mungkin adalah 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, berikut adalah cuplikan dari beberapa kartu yang dapat dipahami tanpa pengetahuan pakar:
- CodeFolding: Menghindari kode duplikat dengan menggabungkannya (misalnya, jika dua arm
if
memiliki beberapa petunjuk bersama di bagian akhirnya). - DeadArgumentElimination: Link time optimization pass untuk menghapus argumen ke fungsi jika selalu dipanggil dengan konstanta yang sama.
- MinifyImportsAndExports: Melakukan minifikasi ke
"a"
,"b"
. - DeadCodeElimination: Menghapus kode mati.
Ada
cookbook pengoptimalan
yang tersedia dengan beberapa tips untuk mengidentifikasi flag mana yang lebih
penting dan layak dicoba terlebih dahulu. Misalnya, terkadang menjalankan wasm-opt
berulang kali akan memperkecil input lebih lanjut. 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, mainkan demo tersemat yang 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.