Binaryen, C++ ile yazılmış bir WebAssembly derleyici ve araç zinciri altyapı kitaplığıdır. WebAssembly'e derlemeyi sezgisel, hızlı ve etkili hale getirmeyi amaçlar. Bu yayında, ExampleScript adlı sentetik oyuncak dilinin örneğini kullanarak Binaryen.js API'yi kullanarak JavaScript'de WebAssembly modüllerini nasıl yazacağınızı öğreneceksiniz. Modül oluşturma, modüle işlev ekleme ve modülden işlev dışa aktarma ile ilgili temel bilgileri öğreneceksiniz. Bu sayede, gerçek programlama dillerini WebAssembly'e derlemenin genel mekanizması hakkında bilgi edinebilirsiniz. Ayrıca, Wasm modüllerini hem Binaryen.js ile hem de wasm-opt
ile komut satırında nasıl optimize edeceğinizi öğreneceksiniz.
Binaryen hakkında bilgi
Binaryen, tek bir üstbilgide sezgisel bir C API'ye sahiptir ve JavaScript'ten de kullanılabilir. WebAssembly biçiminde girişleri kabul eder ancak bunu tercih eden derleyiciler için genel bir kontrol akışı grafiği de kabul eder.
Ara temsil (IR), kaynak kodunu temsil etmek için bir derleyici veya sanal makine tarafından dahili olarak kullanılan veri yapısı ya da koddur. Binaryen'in dahili IR'si kompakt veri yapıları kullanır ve mevcut tüm CPU çekirdeklerini kullanarak tamamen paralel kod oluşturma ve optimizasyon için tasarlanmıştır. Binaryen'in IR'si, WebAssembly'in alt kümesi olduğu için WebAssembly'e derlenir.
Binaryen'in optimizasyon aracı, kod boyutunu ve hızını artırabilecek birçok geçişe sahiptir. Bu optimizasyonlar, Binaryen'i tek başına bir derleyici arka ucu olarak kullanılabilecek kadar güçlü hale getirmeyi amaçlar. Wasm minifleştirmesi olarak düşünebileceğiniz, WebAssembly'ye özgü optimizasyonlar (genel amaçlı derleyicilerin yapamayacağı) içerir.
Binaryen'in örnek kullanıcısı olarak AssemblyScript
Binaryen, TypeScript benzeri bir dilden doğrudan WebAssembly'e derlemek için Binaryen'i kullanan AssemblyScript gibi çeşitli projeler tarafından kullanılır. AssemblyScript oyun alanındaki örneği deneyin.
AssemblyScript girişi:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Binaryen tarafından oluşturulan metin biçimindeki ilgili WebAssembly kodu:
(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
)
)
Binaryen araç zinciri
Binaryen araç seti, hem JavaScript geliştiricileri hem de komut satırı kullanıcıları için çeşitli faydalı araçlar sunar. Bu araçların bir alt kümesi aşağıda listelenmiştir. İçerilen araçların tam listesini projenin README
dosyasında bulabilirsiniz.
binaryen.js
: Wasm modülleri oluşturmak ve optimize etmek için Binaryen yöntemlerini gösteren bağımsız bir JavaScript kitaplığı. Derlemeler için npm'deki binaryen.js başlıklı makaleyi inceleyin (veya doğrudan GitHub veya unpkg'den indirin).wasm-opt
: WebAssembly'i yükleyen ve üzerinde Binaryen IR geçişleri çalıştıran komut satırı aracı.wasm-as
vewasm-dis
: WebAssembly'i derleyen ve derlemeyi bozan komut satırı araçları.wasm-ctor-eval
: Derleme zamanında işlevleri (veya işlevlerin bölümlerini) yürütebilen komut satırı aracı.wasm-metadce
: Wasm dosyalarının bölümlerini, modülün nasıl kullanıldığına bağlı olarak esnek bir şekilde kaldırmak için kullanılan komut satırı aracı.wasm-merge
: Birden fazla Wasm dosyasını tek bir dosyada birleştiren ve bu işlemi yaparken ilgili içe aktarma işlemlerini dışa aktarma işlemlerine bağlayan komut satırı aracı. JavaScript için bir paketleyici gibidir ancak Wasm içindir.
WebAssembly'e derleme
Bir dili başka bir dile derlemek genellikle birkaç adımdan oluşur. En önemlileri aşağıdaki listede verilmiştir:
- Kelime analizi: Kaynak kodunu jetonlara bölün.
- Söz dizimi analizi: Soyut söz dizimi ağacı oluşturun.
- Anlamsal analiz: Hataları kontrol eder ve dil kurallarını uygular.
- Ara kod oluşturma: Daha soyut bir temsil oluşturun.
- Kod oluşturma: Hedef dile çevirin.
- Hedefe özel kod optimizasyonu: Hedef için optimizasyon yapın.
Unix dünyasında derleme için sık kullanılan araçlar lex
ve yacc
'tir:
lex
(Kelime Dizinli Analiz Aracı Oluşturucu):lex
, kelime dizili analiz araçları (lexer veya tarayıcı olarak da bilinir) oluşturan bir araçtır. Giriş olarak bir dizi normal ifade ve ilgili işlem alır ve giriş kaynak kodundaki kalıpları tanıyan bir söz dizimi analizörü için kod oluşturur.yacc
(Yet Another Compiler Compiler):yacc
, söz dizimi analizi için ayrıştırıcılar oluşturan bir araçtır. Giriş olarak bir programlama dilinin resmi dil bilgisi açıklamasını alır ve ayrıştırıcı için kod oluşturur. Ayıraçlayıcılar genellikle kaynak kodun hiyerarşik yapısını temsil eden soyut söz dizimi ağaçları (AST'ler) oluşturur.
Çözümlü örnek
Bu makalenin kapsamı göz önüne alındığında, bir programlama dilinin tamamını ele almak mümkün değildir. Bu nedenle, basitlik açısından, genel işlemleri somut örneklerle ifade ederek çalışan ve çok sınırlı ve işe yaramaz bir sentetik programlama dili olan ExampleScript'i ele alacağız.
add()
işlevi yazmak için herhangi bir toplama örneği (ör.2 + 3
) kodlarsınız.multiply()
işlevi yazmak için örneğin6 * 12
yazarsınız.
Önceki uyarıya göre tamamen işe yaramaz ancak leksik analizörünün tek bir normal ifade (/\d+\s*[\+\-\*\/]\s*\d+\s*/
) olması için yeterince basittir.
Ardından bir ayrıştırıcı olmalıdır. Aslında, adlandırılmış yakalama grupları içeren bir normal ifade kullanılarak soyut söz dizimi ağacının çok basitleştirilmiş bir sürümü oluşturulabilir:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
ExampleScript komutları her satırda bir tane olduğundan ayrıştırıcı, yeni satır karakterlerine göre bölünerek kodu satır bazında işleyebilir. Bu, önceden madde listesinde yer alan ilk üç adımı (leksik analiz, söz dizimi analizi ve anlamsal analiz) kontrol etmek için yeterlidir. Bu adımların kodu aşağıdaki girişte yer almaktadır.
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),
};
});
}
}
Ara kod oluşturma
ExampleScript programları artık soyut bir söz dizimi ağacı olarak (oldukça basitleştirilmiş olsa da) temsil edilebildiğine göre, bir sonraki adım soyut bir ara temsil oluşturmaktır. İlk adım, Binaryen'de yeni bir modül oluşturmaktır:
const module = new binaryen.Module();
Soyut söz dizimi ağacının her satırı firstOperand
, operator
ve secondOperand
öğelerinden oluşan bir üçlü içerir. ExampleScript'teki dört olası operatör (+
, -
, *
, /
) için Binaryen'in Module#addFunction()
yöntemiyle modüle yeni bir işlev eklenmelidir. Module#addFunction()
yöntemlerinin parametreleri aşağıdaki gibidir:
name
:string
, işlevin adını temsil eder.functionType
:Signature
, işlevin imzasını temsil eder.varTypes
:Type[]
, belirtilen sırada ek yerel öğeleri belirtir.body
: işlevin içeriği olan birExpression
.
Ayrıntılara inmek ve bunları incelemek için daha fazla bilgi var. Binaryen belgeleri bu alanda gezinmenize yardımcı olabilir. Ancak sonunda, ExampleScript'in +
operatörü için mevcut tam sayı işlemlerinden biri olan Module#i32.add()
yöntemine ulaşırsınız.
Toplama işlemi için ilk ve ikinci toplama terimi olmak üzere iki operand gerekir. İşlevin gerçekten çağrılabilir olması için Module#addFunctionExport()
ile dışa aktarılması gerekir.
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');
Soyut söz dizimi ağacı işlendikten sonra modül dört yöntem içerir. Bu yöntemlerden üçü tam sayılarla çalışır: Module#i32.add()
tabanlı add()
, Module#i32.sub()
tabanlı subtract()
, Module#i32.mul()
tabanlı multiply()
. Örnek komut dosyası, kayan noktalı sonuçlarla da çalıştığından dördüncü yöntem, Module#f64.div()
tabanlı aykırı değer divide()
'dir.
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 `/`.
Gerçek kod tabanlarıyla çalışıyorsanız bazen hiç çağrılmayan ölü kodlar olabilir. ExampleScript'in Wasm'e derlenmesine ilişkin çalışan örnekte yapay olarak ölü kod (daha sonraki bir adımda optimize edilip kaldırılacak) eklemek için dışa aktarılmamış bir işlev eklemek yeterlidir.
// 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)),
]),
);
Derleyici neredeyse hazır. Bu kesinlikle gerekli değildir ancak Module#validate()
yöntemiyle modülü doğrulamak iyi bir uygulamadır.
if (!module.validate()) {
throw new Error('Validation error');
}
Elde edilen Wasm kodunu alma
Elde edilen Wasm kodunu almak için Binaryen'de, S ifadesi biçiminde insan tarafından okunabilir bir .wat
dosyası olarak metin temsilini ve doğrudan tarayıcıda çalıştırılabilen bir .wasm
dosyası olarak ikili temsili almak için iki yöntem vardır. İkili kod doğrudan tarayıcıda çalıştırılabilir. İşlemin başarılı olup olmadığını görmek için dışa aktarma işlemlerini günlüğe kaydetmeniz faydalı olabilir.
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);
Dört işlemin de yer aldığı bir ExampleScript programının tam metinsel gösterimi aşağıda listelenmiştir. Ölü kodun hâlâ mevcut olduğunu ancak WebAssembly.Module.exports()
ekran görüntüsüne göre gösterilmediğini unutmayın.
(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)
)
)
)
WebAssembly'i optimize etme
Binaryen, Wasm kodunu optimize etmenin iki yolunu sunar. Biri Binaryen.js'de, diğeri komut satırı içindir. İlki varsayılan olarak standart optimizasyon kuralları grubunu uygular ve optimize etme ve küçültme düzeyini ayarlamanıza olanak tanır. İkincisi ise varsayılan olarak hiçbir kural kullanmaz ancak bunun yerine tam özelleştirmeye olanak tanır. Yani yeterli denemeyle ayarları kodunuza göre optimum sonuçlar için özelleştirebilirsiniz.
Binaryen.js ile optimizasyon
Bir Wasm modülünü Binaryen ile optimize etmenin en kolay yolu, Binaryen.js'nin Module#optimize()
yöntemini doğrudan çağırmak ve isteğe bağlı olarak optimizasyon ve sıkıştırma düzeyini ayarlamaktır.
// 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();
Bu işlem, daha önce yapay olarak tanıtılan ölü kodu kaldırır. Böylece, ExampleScript oyun örneğinin Wasm sürümünün metinsel temsili artık bu kodu içermez. local.set/get
çiftlerinin, SimplifyLocals (yerel değişkenlerle ilgili çeşitli optimizasyonlar) ve Vacuum (açıkça gereksiz kodları kaldırır) optimizasyon adımları tarafından nasıl kaldırıldığını ve return
'in RemoveUnusedBrs (gereksiz yerlerdeki araları kaldırır) tarafından nasıl kaldırıldığını da unutmayın.
(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)
)
)
)
Birçok optimizasyon geçişi vardır ve Module#optimize()
, belirli optimizasyon ve küçültme düzeylerinin varsayılan ayarlarını kullanır. Tam özelleştirme için wasm-opt
komut satırı aracını kullanmanız gerekir.
wasm-opt komut satırı aracıyla optimizasyon yapma
Kullanılacak geçişlerin tam olarak özelleştirilmesi için Binaryen'de wasm-opt
komut satırı aracı bulunur. Olası optimizasyon seçeneklerinin tam listesini görmek için aracın yardım mesajını inceleyin. wasm-opt
aracı, muhtemelen araçlar arasında en popüler olanıdır ve Emscripten, J2CL, Kotlin/Wasm, dart2wasm, wasm-pack ve diğerleri gibi çeşitli derleyici araç zincirleri tarafından Wasm kodunu optimize etmek için kullanılır.
wasm-opt --help
Kartlar hakkında fikir edinmeniz için uzman bilgisi olmadan anlaşılabilen kartlardan bazılarının bir kısmını aşağıda bulabilirsiniz:
- CodeFolding: Kodu birleştirerek yinelenen koddan kaçınır (örneğin, iki
if
kolun sonunda bazı ortak talimatlar varsa). - DeadArgumentElimination: Her zaman aynı sabitlerle çağrılırsa bir işleve ait bağımsız değişkenleri kaldırmak için bağlantı zamanı optimizasyon geçişi.
- MinifyImportsAndExports: Bunları
"a"
,"b"
olarak küçültür. - DeadCodeElimination: Ölü kodu kaldırın.
Çeşitli işaretlerden hangilerinin daha önemli olduğunu ve önce denenmeye değer olduğunu belirlemek için çeşitli ipuçları içeren bir optimizasyon yemek kitabı mevcuttur. Örneğin, bazen wasm-opt
tekrar tekrar çalıştırmak girişi daha da küçütür. Bu tür durumlarda, --converge
işaretiyle çalıştırma işlemi, daha fazla optimizasyon gerçekleşmeyene ve sabit bir noktaya ulaşılana kadar yinelemeye devam eder.
Demo
Bu gönderide açıklanan kavramları çalışırken görmek için yerleşik deneme sürümünü kullanarak aklınıza gelen her ExampleScript girişini sağlayın. Ayrıca demonun kaynak kodunu görüntülemeyi unutmayın.
Sonuçlar
Binaryen, dilleri WebAssembly'e derlemek ve elde edilen kodu optimize etmek için güçlü bir araç seti sağlar. JavaScript kitaplığı ve komut satırı araçları, esneklik ve kullanım kolaylığı sunar. Bu gönderide, Wasm derlemenin temel ilkeleri gösterilerek Binaryen'in etkinliği ve maksimum optimizasyon potansiyeli vurgulanmıştır. Binaryen'in optimizasyonlarını özelleştirme seçeneklerinin çoğu Wasm'in iç yapısı hakkında derin bilgi gerektirse de genellikle varsayılan ayarlar zaten mükemmel çalışır. Bu noktada, Binaryen ile derleme ve optimizasyon yapmanın keyfini çıkarabilirsiniz.
Teşekkür ederiz
Bu yayın Alon Zakai, Thomas Lively ve Rachel Andrew tarafından incelendi.