Binaryen là một trình biên dịch và chuỗi công cụ
thư viện cơ sở hạ tầng cho WebAssembly, được viết bằng C++. Hộp cát về quyền riêng tư sẽ giúp
biên dịch WebAssemb một cách trực quan, nhanh chóng và hiệu quả. Trong bài đăng này, việc sử dụng
ví dụ về một ngôn ngữ đồ chơi tổng hợp có tên là ExampleScript, hãy tìm hiểu cách viết
Các mô-đun WebAssembly trong JavaScript sử dụng API Binaryen.js. Bạn sẽ đề cập đến
kiến thức cơ bản về việc tạo mô-đun, thêm hàm vào mô-đun và xuất mô-đun
khỏi mô-đun. Việc này sẽ cung cấp cho bạn kiến thức về
cơ chế biên dịch ngôn ngữ lập trình thực tế sang WebAssembly. Hơn nữa,
bạn sẽ tìm hiểu cách tối ưu hoá các mô-đun Wasm bằng cả Binaryen.js và trên
dòng lệnh bằng wasm-opt
.
Thông tin cơ bản về Binaryen
Binaryen có tính năng trực quan API C trong một tiêu đề duy nhất và cũng có thể được dùng từ JavaScript. Hàm này chấp nhận dữ liệu đầu vào trong Biểu mẫu WebAssembly, nhưng cũng chấp nhận các nguyên tắc chung biểu đồ luồng điều khiển cho các trình biên dịch thích điều đó.
Biểu diễn trung gian (IR) là cấu trúc dữ liệu hoặc mã được sử dụng được trình biên dịch hoặc máy ảo nội bộ để biểu thị mã nguồn. của Binaryen IR nội bộ sử dụng cấu trúc dữ liệu nhỏ gọn và được thiết kế để hoàn toàn song song tạo và tối ưu hoá mã, sử dụng tất cả các lõi CPU có sẵn. IR của Binaryen biên dịch xuống WebAssembly do là một tập hợp con của WebAssembly.
Trình tối ưu hoá của Binaryen có nhiều lần truyền có thể cải thiện kích thước và tốc độ của mã. Các tối ưu hoá nhằm làm cho Binaryen đủ mạnh để được dùng làm trình biên dịch phần phụ trợ. Nó bao gồm các hoạt động tối ưu hoá dành riêng cho WebAssembly ( các trình biên dịch đa năng có thể không làm được), mà bạn có thể coi là Wasm giảm kích thước.
hộiScript, với tư cách người dùng mẫu của Binaryen
Binaryen được sử dụng bởi một số dự án, ví dụ: AssemblyScript, sử dụng Binaryen để biên dịch trực tiếp từ ngôn ngữ giống TypeScript sang WebAssembly. Thử ví dụ trong sân chơi hội tập lệnh.
Đầu vào MultiplexScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Mã WebAssembly tương ứng ở dạng văn bản do Binaryen tạo:
(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
)
)
Chuỗi công cụ Binaryen
Chuỗi công cụ Binaryen cung cấp một số công cụ hữu ích cho cả JavaScript
và người dùng dòng lệnh. Một nhóm nhỏ các công cụ này được liệt kê trong
đang theo dõi; thời gian
danh sách đầy đủ các công cụ bên trong
có sẵn trên tệp README
của dự án.
binaryen.js
: Một thư viện JavaScript độc lập hiển thị các phương thức nhị phân với tạo và tối ưu hoá các mô-đun Wasm. Đối với bản dựng, hãy xem binaryen.js trên npm (hoặc tải xuống trực tiếp từ GitHub hoặc unpkg).wasm-opt
: Công cụ dòng lệnh tải WebAssembly và chạy Binaryen IR chuyển qua.wasm-as
vàwasm-dis
: Các công cụ dòng lệnh tập hợp và tháo rời WebAssembly.wasm-ctor-eval
: Công cụ dòng lệnh có thể thực thi các hàm (hoặc các phần của hàm) tại thời điểm biên dịch.wasm-metadce
: Công cụ dòng lệnh để xoá các phần của tệp Wasm một cách linh hoạt phụ thuộc vào cách sử dụng mô-đun.wasm-merge
: Công cụ dòng lệnh hợp nhất nhiều tệp Wasm thành một tệp duy nhất kết nối dữ liệu nhập tương ứng với tệp xuất khi thực hiện việc này. Thích gói cho JavaScript, nhưng dành cho Wasm.
Biên dịch lên WebAssembly
Việc biên dịch một ngôn ngữ sang một ngôn ngữ khác thường bao gồm nhiều bước, cách quan trọng được liệt kê trong danh sách sau:
- Phân tích thuật ngữ: Chia mã nguồn thành mã thông báo.
- Phân tích cú pháp: Tạo cây cú pháp trừu tượng.
- Phân tích ngữ nghĩa: Kiểm tra lỗi và thực thi các quy tắc về ngôn ngữ.
- Tạo mã trung gian: Tạo một bản trình bày trừu tượng hơn.
- Tạo mã: Dịch sang ngôn ngữ đích.
- Tối ưu hoá mã theo mục tiêu cụ thể: Tối ưu hoá cho mục tiêu.
Trong thế giới Unix, các công cụ thường được dùng để biên dịch là
lex
và
yacc
:
lex
(Trình tạo trình phân tích từ vựng):lex
là một công cụ tạo từ vựng trình phân tích, còn được gọi là từ điển hoặc máy quét. Bạn sẽ cần một tập hợp các tham số biểu thức và hành động tương ứng làm đầu vào, đồng thời tạo mã cho một công cụ phân tích từ vựng nhận dạng các mẫu trong mã nguồn đầu vào.yacc
(Yet Another Compiler Compiler):yacc
là một công cụ tạo trình phân tích cú pháp để phân tích cú pháp. Mã này sử dụng một đoạn mô tả ngữ pháp chính thức của ngôn ngữ lập trình làm đầu vào và tạo mã cho một trình phân tích cú pháp. Trình phân tích cú pháp thường sản xuất cây cú pháp trừu tượng (AST) đại diện cho cấu trúc phân cấp của mã nguồn.
Một ví dụ hiệu quả
Với phạm vi của bài đăng này, bạn không thể đề cập đến một chương trình hoàn chỉnh ngôn ngữ, vì vậy để đơn giản, hãy cân nhắc một cách rất hạn chế và vô ích ngôn ngữ lập trình tổng hợp có tên là ExampleScript, hoạt động bằng cách biểu thị phép toán chung thông qua ví dụ cụ thể.
- Để viết hàm
add()
, bạn viết mã ví dụ về phép cộng bất kỳ nào, giả sử2 + 3
. - Để viết một hàm
multiply()
, bạn có thể viết6 * 12
chẳng hạn.
Theo cảnh báo trước, việc này hoàn toàn vô ích, nhưng đủ đơn giản để từ vựng
trình phân tích thành một biểu thức chính quy duy nhất: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Tiếp theo, cần có một trình phân tích cú pháp. Trên thực tế, phiên bản rất đơn giản của
bạn có thể tạo cây cú pháp trừu tượng bằng cách sử dụng biểu thức chính quy với
nhóm thu thập được đặt tên:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
Các lệnh ExampleScript được nhập trên mỗi dòng một lần, do đó, trình phân tích cú pháp có thể xử lý mã theo từng dòng bằng cách phân tách các ký tự dòng mới. Điều này là đủ để kiểm tra ba bước so với danh sách dấu đầu dòng ở phần trước, cụ thể là phân tích từ vựng, cú pháp Analytics và phân tích ngữ nghĩa. Mã cho các bước này nằm trong danh sách sau đây.
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),
};
});
}
}
Tạo mã trung gian
Giờ đây, các chương trình ExampleScript có thể được biểu diễn dưới dạng một cây cú pháp trừu tượng (mặc dù là một cách thức khá đơn giản), bước tiếp theo là tạo một bản tóm tắt biểu diễn trung gian. Bước đầu tiên là tạo mô-đun mới trong Binaryen:
const module = new binaryen.Module();
Mỗi dòng của cây cú pháp trừu tượng chứa một bộ ba bao gồm
firstOperand
, operator
và secondOperand
. Cho mỗi yếu tố trong số bốn chỉ số có thể có
trong ExampleScript, tức là +
, -
, *
, /
, một
hàm cần được thêm vào mô-đun
bằng phương thức Module#addFunction()
của Binaryen. Các tham số của
Các phương thức Module#addFunction()
như sau:
name
:string
, đại diện cho tên hàm.functionType
:Signature
, đại diện cho chữ ký của hàm.varTypes
:Type[]
, cho biết các cục bộ khác, theo thứ tự nhất định.body
:Expression
, nội dung của hàm.
Bạn vẫn có thể thư giãn và phân tích một vài thông tin khác
Tài liệu về tệp nhị phân
có thể giúp bạn điều hướng trong không gian, nhưng cuối cùng, cho +
của ExampleScript
bạn sẽ kết thúc ở phương thức Module#i32.add()
dưới dạng một trong số
có sẵn
các phép toán số nguyên.
Phép cộng cần có hai toán hạng, tổng thứ nhất và tổng thứ hai. Đối với
một hàm thực sự có thể gọi,
xuất
cùng với 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');
Sau khi xử lý cây cú pháp trừu tượng, mô-đun này sẽ chứa 4 phương thức,
ba làm việc với các số nguyên, cụ thể là add()
dựa trên Module#i32.add()
,
subtract()
dựa trên Module#i32.sub()
, multiply()
dựa trên
Module#i32.mul()
và giá trị ngoại lệ divide()
dựa trên Module#f64.div()
vì ExampleScript cũng hoạt động với các kết quả dấu phẩy động.
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 `/`.
Nếu bạn xử lý các cơ sở mã thực tế, đôi khi sẽ có mã lỗi không bao giờ được gọi. Để đưa mã chết một cách giả tạo (sẽ được tối ưu hoá và bị loại bỏ ở bước sau) trong ví dụ đang chạy của ExampleScript biên dịch vào Wasm, thì việc thêm hàm không được xuất sẽ thực hiện công việc.
// 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)),
]),
);
Trình biên dịch gần như đã sẵn sàng. Không hoàn toàn cần thiết, nhưng chắc chắn
phương pháp hay để
xác thực mô-đun
bằng phương thức Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Lấy mã Wasm kết quả
Người nhận
nhận được mã Wasm kết quả,
có hai phương thức trong Binaryen để lấy thông tin
biểu thị bằng văn bản
dưới dạng tệp .wat
trong S-biểu thức
dưới dạng định dạng con người có thể đọc được và
đại diện nhị phân
dưới dạng tệp .wasm
có thể chạy trực tiếp trong trình duyệt. Mã nhị phân có thể là
chạy trực tiếp trong trình duyệt. Để biết phương pháp này có hoạt động không, hãy ghi nhật ký các tệp xuất có thể
của chúng tôi.
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);
Bản trình bày đầy đủ dạng văn bản cho chương trình ExampleScript với cả bốn
hoạt động được liệt kê trong bảng sau. Hãy để ý cách mã chết vẫn còn ở đó,
nhưng không được hiển thị như ảnh chụp màn hình của
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)
)
)
)
Tối ưu hoá WebAssembly
Binaryen cung cấp hai cách để tối ưu hoá mã Wasm. Một trong chính Binaryen.js, và một cho dòng lệnh. Phương pháp đầu vào áp dụng tập hợp chuẩn tối ưu hoá các quy tắc này theo mặc định và cho phép bạn đặt mức tối ưu hoá và mức độ thu nhỏ, cũng như theo mặc định, không sử dụng quy tắc nào, nhưng thay vào đó sẽ cho phép tuỳ chỉnh hoàn toàn, có nghĩa là với đủ thử nghiệm, bạn có thể điều chỉnh cài đặt cho phù hợp kết quả tối ưu dựa trên mã của bạn.
Tối ưu hoá bằng Binaryen.js
Cách đơn giản nhất để tối ưu hoá mô-đun Wasm bằng Binaryen là
gọi trực tiếp phương thức Module#optimize()
của Binaryen.js, và không bắt buộc
thiết lập
tối ưu hoá và mức độ thu nhỏ.
// 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();
Làm như vậy sẽ xoá mã chết được đưa vào một cách giả tạo trước đó, do đó
bản trình bày bằng văn bản của phiên bản Wasm của ví dụ về đồ chơi ExampleScript không
chứa tệp đó lâu hơn. Ngoài ra, xin lưu ý cách cặp local.set/get
bị xoá bởi
các bước tối ưu hoá
SimplifyLocals
(các hoạt động tối ưu hoá khác liên quan đến cục bộ) và
Hút chân không
(xoá mã rõ ràng không cần thiết) và return
bị xoá bằng
RemoveUnusedBrs
(xoá ngắt khỏi những vị trí không cần thiết).
(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)
)
)
)
Có rất nhiều
thẻ tối ưu hoá,
và Module#optimize()
sử dụng
các cấp độ tối ưu hoá và thu nhỏ cụ thể mặc định
bộ. Để tuỳ chỉnh hoàn toàn, bạn cần sử dụng công cụ dòng lệnh wasm-opt
.
Tối ưu hoá bằng công cụ dòng lệnh wasm-opt
Để tuỳ chỉnh đầy đủ các thẻ và vé sẽ sử dụng, Binaryen bao gồm
Công cụ dòng lệnh wasm-opt
. Để có một
danh sách đầy đủ các tuỳ chọn tối ưu hoá có thể có,
hãy xem thông báo trợ giúp của công cụ đó. Công cụ wasm-opt
có lẽ là công cụ phổ biến nhất
công cụ và được một số chuỗi công cụ biên dịch sử dụng để tối ưu hoá mã Wasm,
bao gồm Emscripten,
J2CL,
Kotlin/Wasm,
dart2wasm,
wasm-pack và các thuộc tính khác.
wasm-opt --help
Để giúp bạn hình dung về thẻ và vé, sau đây là phần trích dẫn của một số thẻ và vé đều có thể hiểu được nếu không cần kiến thức chuyên môn:
- CodeFolding: Tránh mã trùng lặp bằng cách hợp nhất mã đó (ví dụ: nếu hai
if
đều có một số hướng dẫn được chia sẻ). - DeadArgumentLoại bỏ: Truyền tối ưu hoá thời gian liên kết để xoá đối số đối với một hàm nếu hàm đó luôn được gọi với cùng hằng số.
- MinifyImportsAndExports: Giảm kích thước chúng xuống còn
"a"
,"b"
. - DeadCodeExclude: Xoá mã lỗi.
Có một
sổ tay tối ưu hoá
bạn sẽ thấy một số mẹo giúp xác định xem cờ nào hiệu quả hơn
quan trọng và đáng thử trước tiên. Ví dụ: đôi khi chạy wasm-opt
lặp đi lặp lại sẽ thu nhỏ dữ liệu đầu vào hơn nữa. Trong những trường hợp như vậy, việc chạy
với
--converge
cờ
tiếp tục lặp lại cho đến khi không tối ưu hoá thêm nữa và điểm cố định là
đạt được.
Bản minh hoạ
Để xem các khái niệm được giới thiệu trong bài đăng này trong thực tế, hãy thử nghiệm với các bản minh hoạ cung cấp cho nó bất kỳ đầu vào ExampleScript nào mà bạn có thể nghĩ đến. Đồng thời, hãy nhớ xem mã nguồn của bản minh hoạ.
Kết luận
Binaryen cung cấp một bộ công cụ mạnh mẽ để biên dịch ngôn ngữ sang WebAssembly và tối ưu hoá mã kết quả. Thư viện JavaScript và công cụ dòng lệnh đem lại sự linh hoạt và dễ sử dụng. Bài đăng này đã trình bày các nguyên tắc cốt lõi của Biên dịch Wasm, nêu bật hiệu quả và tiềm năng của Binaryen tối ưu hoá tối đa. Mặc dù có nhiều lựa chọn để tuỳ chỉnh tệp Binaryen tối ưu hoá đòi hỏi kiến thức chuyên sâu về bên trong Wasm, thường là cài đặt mặc định đã hoạt động tuyệt vời. Theo đó, chúc bạn biên dịch và tối ưu hoá thành công với Binaryen!
Xác nhận
Bài đăng này do Alon Zakai xem xét. Thomas Beau và Rachel Andrew.