Binaryen, WebAssembly için kullanılan ve C++ dilinde yazılmış bir derleyici ve araç zinciri altyapı kitaplığıdır. WebAssembly'yi derleme işlemini sezgisel, hızlı ve etkili hale getirmeyi amaçlar. Bu yayında, ExampleScript adlı sentetik oyuncak dili örneğini kullanarak Binaryen.js API'yi kullanarak JavaScript'te WebAssembly modüllerini yazmayı öğrenin. Modül oluşturma ile ilgili temel bilgileri, modüle ek işlev eklemeyi ve modülden dışa aktarma işlevlerini işleyeceğiz. Bu bölümde, gerçek programlama dillerini WebAssembly'de derleyen genel mekanikler hakkında bilgi edineceksiniz. Ayrıca, Wasm modüllerini hem Binaryen.js hem de wasm-opt
ile komut satırında optimize etmeyi öğreneceksiniz.
Binaryen arka planı
Binaryen, tek bir başlıkta sezgisel bir C API'sine sahiptir ve JavaScript'ten de kullanılabilir. WebAssembly formunda girişleri kabul eder ancak bunu tercih eden derleyiciler için genel bir kontrol akışı grafiğini de kabul eder.
Ara gösterim (IR), kaynak kodu temsil etmek için bir derleyici veya sanal makine tarafından dahili olarak kullanılan veri yapısı veya koddur. Binaryen'in dahili IR sistemi, kompakt veri yapıları kullanır ve mevcut tüm CPU çekirdeklerini kullanarak tamamen paralel kod üretimi ve optimizasyonu için tasarlanmıştır. Binaryen'in IR'si, WebAssembly'nin alt kümesi olduğu için WebAssembly'ye doğru derlenir.
Binaryen'in optimize edicisinde, kod boyutunu ve hızını iyileştirebilecek birçok geçiş bulunmaktadır. Bu optimizasyonların amacı, Binaryen'i kendi başına derleyici arka ucu olarak kullanılabilecek kadar güçlü hale getirmektir. Wasm küçültme olarak düşünebileceğiniz WebAssembly'ye özgü optimizasyonlar (genel amaçlı derleyicilerin yapamadığı) içerir.
Örnek Binaryen kullanıcısı olarak AssemblyScript
Binaryen çeşitli projelerde kullanılır. Örneğin, TypeScript benzeri bir dilden doğrudan WebAssembly'ye derlemek için Binaryen'i kullanan AssemblyScript. 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çiminde karşılık gelen 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
)
)
İkili araç zinciri
Binaryen araç zinciri, hem JavaScript geliştiricileri hem de komut satırı kullanıcıları için birçok faydalı araç sunar. Bu araçların bir alt kümesi aşağıda listelenmiştir; içerilen araçların tam listesini projenin README
dosyasında bulabilirsiniz.
binaryen.js
: Wasm modüllerini oluşturmak ve optimize etmek için Binaryen yöntemlerini sunan bağımsız bir JavaScript kitaplığı. Derlemeler için npm'debinaryen.js'ye bakın (veya doğrudan GitHub'dan ya da unpkg'dan indirin).wasm-opt
: WebAssembly'yi yükleyen ve Binaryen IR geçişlerini çalıştıran komut satırı aracı.wasm-as
vewasm-dis
: WebAssembly'yi monte eden ve söken komut satırı araçları.wasm-ctor-eval
: Derleme zamanında işlevleri (veya işlev bölümlerini) yürütebilen komut satırı aracı.wasm-metadce
: Modülün nasıl kullanıldığına bağlı olarak Wasm dosyalarının parçalarını esnek bir şekilde kaldıran komut satırı aracı.wasm-merge
: Birden fazla Wasm dosyasını tek bir dosyada birleştiren komut satırı aracı. İlgili içe aktarmaları, olduğu gibi dışa aktarmalara bağlar. JavaScript için bir paketleyici gibi, ancak Wasm için.
WebAssembly'ye derleme
Bir dili bir başka dile derlemek genellikle birkaç adımdan oluşur. En önemli adımlar aşağıdaki listede listelenmiştir:
- Leksik analiz: Kaynak kodunu jetonlara ayırın.
- Söz dizimi analizi: Soyut bir söz dizimi ağacı oluşturun.
- Anlamsal analiz: Hataları kontrol edin ve dil kurallarını uygulayın.
- Orta düzey 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ça kullanılan araçlar şunlardır: lex
ve yacc
:
lex
(Leksik Analiz Oluşturucu):lex
, lexer veya tarayıcı olarak da bilinen sözlük analiz araçları üreten bir araçtır. Bir dizi normal ifadeyi ve karşılık gelen işlemleri giriş olarak alır ve giriş kaynağı kodundaki kalıpları tanıyan bir sözlük analizcisi için kod oluşturur.yacc
(Yine de Başka Bir Derleyici Derleyici):yacc
, söz dizimi analizi için ayrıştırıcılar oluşturan bir araçtır. Giriş olarak bir programlama dilinin resmi bir dilbilgisi açıklamasını alır ve ayrıştırıcı için kod oluşturur. Ayrıştırıcılar genellikle kaynak kodun hiyerarşik yapısını temsil eden soyut söz dizimi ağaçları (AST'ler) üretir.
İşe yarayan bir örnek
Bu yayının kapsamı düşünüldüğünde, bir programlama dilini bütünüyle ele almak mümkün değildir. Bu nedenle, basitlik açısından, genel işlemleri somut örneklerle ifade ederek çalışan ExampleScript adlı, son derece sınırlı ve yararsız bir sentetik programlama dili kullanmayı düşünün.
- Bir
add()
işlevi yazmak için2 + 3
gibi herhangi bir eklemenin bir örneğini kodluyorsunuz. - Bir
multiply()
işlevi yazmak için6 * 12
yazın.
Ön uyarıya göre, tamamen faydasız, ancak sözlük analizini tek bir normal ifade olacak kadar basit: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Daha sonra, bir ayrıştırıcı olması gerekir. Aslında, soyut söz dizimi ağacının çok basitleştirilmiş bir sürümü, adlandırılmış yakalama gruplarına sahip bir normal ifade kullanılarak oluşturulabilir: /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
.
ExampleScript komutları her satıra bir giriştir. Böylece ayrıştırıcı, kodu yeni satır karakterlerine göre satır bazında işleyebilir. Bu işlem, madde işaretli listedeki ilk üç adımı (söz dizimi analizi, sözdizimi analizi ve anlamsal analiz) kontrol etmek için yeterlidir. Bu adımların kodunu aşağıdaki listede bulabilirsiniz.
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),
};
});
}
}
Orta düzey kod oluşturma
ExampleScript programları artık soyut bir söz dizimi ağacı olarak temsil edilebildiğine göre (son derece basit olsalar da) bir sonraki adım soyut bir ara gösterim 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
içeren bir üçlü içerir. ExampleScript'teki olası dört operatör (+
, -
, *
, /
) için Binaryen'in Module#addFunction()
yöntemiyle modüle yeni bir işlev eklenmesi gerekir. Module#addFunction()
yöntemlerinin parametreleri aşağıdaki gibidir:
name
:string
değeri, işlevin adını temsil eder.functionType
:Signature
, işlevin imzasını temsil eder.varTypes
:Type[]
, belirtilen sırada ek yerel olduğunu gösterir.body
: birExpression
, işlevin içeriği.
Gevşeyip rahatlamak için başka ayrıntılar daha vardır ve Binaryen dokümanları bu alanda gezinmenize yardımcı olabilir ancak nihayetinde ExampleScript'in +
operatörü için mevcut birçok tamsayı işlemden biri olarak Module#i32.add()
yöntemini kullanmaya başlarsınız.
Toplama işlemi için, birinci ve ikinci toplam olmak üzere iki işlem gören gerekir. İşlevin ç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 üçü tamsayı sayılarla çalışır, diğer bir deyişle Module#i32.add()
için add()
, Module#i32.sub()
temelli subtract()
, Module#i32.mul()
temeline göre multiply()
ve Module#f64.div()
değerine göre aykırı divide()
değeri kullanılır. Bunun nedeni, ExampleScript'in kayan nokta sonuçlarıyla da çalışmasıdır.
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 uğraşıyorsanız bazen asla çağrılmayan ölü kodlar olabilir. ExampleScript'in Wasm derlemesinin çalışan örneğine ölü kodu (sonraki bir adımda optimize edilecek ve kaldırılacak) yapay olarak eklemek için dışa aktarılmayan bir işlevin eklenmesi işi yapar.
// 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 şimdi neredeyse hazır. Mutlaka gerekli değildir, ancak Module#validate()
yöntemiyle modülü doğrulamak kesinlikle iyi bir uygulamadır.
if (!module.validate()) {
throw new Error('Validation error');
}
Ortaya çıkan Wasm kodunu elde etme
Elde edilen Wasm kodunu elde etmek için İkili'de metinsel temsili kullanıcılar tarafından okunabilir biçimde S ifadesinde .wat
dosyası olarak, doğrudan tarayıcıda çalıştırılabilen bir .wasm
dosyası olarak ikili gösterim'i almak için iki yöntem bulunur. İkili kod doğrudan tarayıcıda çalıştırılabilir. İşe yarayıp yaramadığını görmek için dışa aktarma işlemlerini günlüğe kaydetmek 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);
Bir ExampleScript programının dört işlemin tamamı ile metinsel gösterimi aşağıda listelenmiştir. Geçersiz kodun hâlâ orada olduğunu, ancak WebAssembly.Module.exports()
ekran görüntüsündeki gibi görünmediğ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'yi optimize etme
Binaryen, Wasm kodunu optimize etmek için iki yol sunuyor. Biri Binaryen.js'nin kendisinde, diğeri de komut satırı içindir. İlk kampanya, varsayılan olarak standart optimizasyon kuralları grubunu uygular ve optimizasyon ile küçültme düzeylerini ayarlamanıza olanak tanır. İkincisi ise varsayılan olarak hiçbir kural kullanmaz. Tam özelleştirme olanağı sunar. Bu da yeterli miktarda deneme yaparak kodunuza göre ayarları en uygun sonuçlara göre uyarlayabileceğiniz anlamına gelir.
Binaryen.js ile Optimizasyon
Bir Wasm modülünü Binaryen ile optimize etmenin en basit yolu, Binaryen.js'nin Module#optimize()
yöntemini doğrudan çağırmak ve isteğe bağlı olarak optimize etme ve küçültme seviyesini 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, önceden yapay olarak eklenen geçersiz kodu kaldırır. Böylece, ExampleScript oyuncak örneğinin Wasm sürümünün metinsel gösterimi artık bu kodu içermez. Ayrıca local.set/get
çiftlerinin, SimplifyLocals (yerel bölgeyle ilgili çeşitli optimizasyonlar) ve Vacuum (bariz bir şekilde gereksiz kodu kaldırır) optimizasyon adımlarıyla ve return
'nin RemoveUnusedBrs (ihtiyaç duyulmayan konumlardaki araları kaldırır) ile nasıl kaldırıldığını da not edin.
(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)
)
)
)
Çok sayıda optimizasyon geçişi vardır ve Module#optimize()
, belirli optimizasyon ve daraltma seviyelerinin varsayılan kümelerini 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
Binaryen, kullanılacak kartların tam olarak özelleştirilebilmesi için wasm-opt
komut satırı aracını içeriyor. Olası optimizasyon seçeneklerinin tam listesini öğrenmek için aracın yardım mesajına göz atın. wasm-opt
aracı muhtemelen bu araçların en popüleridir ve Wasm kodunu optimize etmek için Emscripten, J2CL, Kotlin/Wasm, dart2wasm, wasm-pack gibi çeşitli derleyici araç zinciri tarafından kullanılır.
wasm-opt --help
Kartlar hakkında fikir vermek için, uzman bilgisi olmadan anlaşılabilir seçeneklerden bazılarını burada bulabilirsiniz:
- CodeFolding: Birleştirme yaparak yinelenen koddan kaçınır (örneğin, iki
if
kolunun sonunda paylaşılan talimatlar varsa). - DeadArgumentElimulation: Bir işlev her zaman aynı sabit değerlerle çağrılıyorsa işlevin bağımsız değişkenlerini kaldırmak için bağlantı zamanı optimizasyonu pası.
- MinifyImportsAndExports: Bunları
"a"
,"b"
olacak şekilde küçültür. - DeadCodeElimination: Ölü kodu kaldırın.
Çeşitli işaretlerden hangilerinin daha önemli ve ilk önce denemeye değer olduğunu belirlemek için çeşitli ipuçları içeren bir optimizasyon tarif defteri var. Örneğin, bazen wasm-opt
öğesini tekrar tekrar çalıştırmak girişi daha da daraltır. Bu tür durumlarda, --converge
işaretiyle çalıştırmak, başka optimizasyon yapılmayana ve sabit bir noktaya ulaşılana kadar yinelenmeye devam eder.
Demografi
Bu yayında ele alınan kavramları uygulamalı olarak görmek için, aklınıza gelen herhangi bir ExampleScript girişini sağlayarak yerleştirilmiş demoyu izleyin. Ayrıca demonun kaynak kodunu görüntülemeyi de unutmayın.
Sonuçlar
Binaryen, dilleri WebAssembly'ye derleyip ortaya çıkan kodu optimize etmek için güçlü bir araç seti sunar. JavaScript kitaplığı ve komut satırı araçları, esneklik ve kullanım kolaylığı sunar. Bu gönderi, Wasm derlemesinin temel ilkelerini gösterdi ve Binaryen'in etkililiğini ve maksimum optimizasyon için potansiyelini vurguladı. Binaryen'in optimizasyonlarını özelleştirme seçeneklerinin çoğu, Wasm'ın içindekiler hakkında derin bilgi sahibi olmayı gerektirse de genellikle varsayılan ayarlar zaten çok işe yarıyor. Sonuç olarak, Binaryen ile derleme ve optimizasyon yapmaktan mutluluk duyarız!
Teşekkür
Bu yayın Alon Zakai, Thomas Lively ve Rachel Andrew tarafından incelendi.