Binaryen ile Wasm için derleme ve optimize etme

Binaryen, bir derleyici ve araç zinciridir WebAssembly için altyapı kitaplığı olan bir web sitesi. Amaçlanan sezgisel, hızlı ve etkili WebAssembly derlemesi yapmaya hazırdır. Bu yayında, adlı sentetik bir oyuncak dili örneği olarak yazılım örneklerine bakalım. Nasıl yazılacağını Binaryen.js API'sini kullanan JavaScript'teki WebAssembly modülleri. Hem modül oluşturma, modüle işlev ekleme ve dışa aktarma konularında temel bilgiler fonksiyonlarının nasıl kullanıldığını gösterir. Bu size genel olarak gerçek programlama dillerini WebAssembly'de derleme mekaniği. Ayrıca, Wasm modüllerini hem Binaryen.js ile hem de komut satırından wasm-opt ekleyin.

Binaryen ile ilgili arka plan

Binaryen, C API tek bir başlıkta gösterilebilir. Ayrıca JavaScript'ten kullanılır. Giriş kabul eder WebAssembly formu, ama aynı zamanda genel kontrol akışı grafiği kullanabilirsiniz.

Ara temsil (IR), kullanılan veri yapısı veya kodudur . Binaryen'in dahili IR, kompakt veri yapılarını kullanır ve tamamen paralel kullanım için tasarlanmıştır. kod oluşturma ve optimizasyon. Binaryen IR WebAssembly'nin bir alt kümesi olduğundan WebAssembly'de derler.

Binaryen'in optimize edicisi, kod boyutunu ve hızını artırabilecek birçok karta sahip. Bu optimizasyonlar, Binaryen'i derleyici olarak kullanılabilecek kadar güçlü yapmayı amaçlar arka uçta çalışır. WebAssembly'ye özel optimizasyonlar içerir. derleyiciler bunu yapmayabilir) [Wasm] olarak düşünebilirsiniz. basitleştirir.

Binaryen'in örnek kullanıcısı olarak AssemblyScript

Binaryen çeşitli projelerde kullanılıyor. Örneğin, AssemblyScript, Binaryen'i TypeScript benzeri bir dilden doğrudan WebAssembly'ye derleyin. Örneği deneyin komut dosyasını çalıştırın.

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
 )
)

Önceki örneğe göre oluşturulan WebAssembly kodunu gösteren AssemblyScript oyun alanı.

Binaryen araç zinciri

Binaryen araç zinciri, JavaScript'in hem JavaScript hem de dönüşüm işlemleri için geliştiriciler ve komut satırı kullanıcıları için geçerlidir. Bu araçların bir kısmı takip etme; "the" içerilen araçların tam listesi projenin README dosyasında kullanılabilir.

  • binaryen.js: Binaryen yöntemlerini sunan bağımsız bir JavaScript kitaplığı şunun için: Wasm modüllerini oluşturun ve optimize edin. Derlemeler için npm'de binaryen.js bölümüne bakın (veya doğrudan GitHub veya unpkg) belirtin.
  • wasm-opt: WebAssembly'yi yükleyen ve Binaryen IR'yi çalıştıran komut satırı aracı devreye girer.
  • wasm-as ve wasm-dis: monte eden ve ayıran komut satırı araçları WebAssembly.
  • wasm-ctor-eval: İşlevleri (veya öğeleri) yürütebilen komut satırı aracı işlevleri) sunar.
  • wasm-metadce: Wasm dosyalarının bölümlerini esnek bir şekilde kaldırmak için komut satırı aracı Bu, modülün nasıl kullanıldığına göre değişir.
  • wasm-merge: Birden fazla Wasm dosyasını tek bir dosyada birleştiren komut satırı aracı dosyası oluşturun. Bu işlem, karşılık gelen içe aktarmaları dışa aktarma işlemlerine bağlar. Beğen paketleyiciyi JavaScript için kullanırken Wasm için olduğunu varsayalım.

WebAssembly için derleme

Bir dili başka bir dilde derlemek için genellikle birkaç adım vardır. önemli olanları aşağıdaki listede belirtilmiştir:

  • Sözlük analizi: Kaynak kodu jetonlara ayırma.
  • 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 seviye kod oluşturma: Daha soyut bir temsil oluşturun.
  • Kod oluşturma: Hedef dile çevirin.
  • Hedefe özel kod optimizasyonu: Hedef için optimize edin.

Unix dünyasında, derleme için en sık kullanılan araçlar şunlardır: lex ve yacc:

  • lex (Lexical Analyzer Generator): lex, kelimeler üreten bir araçtır analiz araçları (lexer veya tarayıcı olarak da bilinir). Bir dizi düzenli ve karşılık gelen işlemleri girdi olarak kullanır ve bir giriş kaynak kodundaki kalıpları algılayan sözlük analizcisi.
  • yacc (Başka Bir Derleyici): yacc, isimlerin doğru bir şekilde ayrıştırıcılar dahil edilmiştir. Bir kelimenin resmi dilbilgisi açıklamasını programlama dilini kullanır ve ayrıştırıcı için kod oluşturur. Ayrıştırıcılar üretmek için soyut söz dizimi ağaçları (AST'ler) olarak adlandırılır.
ziyaret edin.

Başarılı bir örnek

Bu gönderinin kapsamı göz önünde bulundurulduğunda bir programlamayı baştan sona ele almak mümkün değil. Öyleyse sadelik açısından, çok sınırlı ve kullanışlı olmayan ve ifade ederek çalışan exampleScript adlı sentetik programlama dili somut örnekler üzerinden genel işlemler yapmasına olanak tanır.

  • Bir add() işlevi yazmak için herhangi bir eklemenin bir örneğini kodlayın. Örneğin, 2 + 3.
  • multiply() işlevi yazmak için örneğin 6 * 12 yazarsınız.

Ön uyarıya göre tamamen işe yaramaz, ancak sözcük açısından analiz edici, tek bir normal ifade olur: /\d+\s*[\+\-\*\/]\s*\d+\s*/.

Daha sonra, bir ayrıştırıcı olmalıdır. Aslında çok basitleştirilmiş bir sürümü soyut bir söz dizimi ağacı, adlandırılmış yakalama grupları: /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/.

ExampleScript komutları her satıra bir tane gelecektir, böylece ayrıştırıcı kodu işleyebilir satır bazında ikiye ayrılır. Bu, ilk adımı kontrol etmek için şu üç adımdan oluşur: sözdizimi analiz, söz dizimi analiz ve anlamsal analiz. Bu adımların kodunu aşağıdaki girişte 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),
      };
    });
  }
}

Ara kod oluşturma

Artık ExampleScript programları soyut bir söz dizimi ağacı olarak temsil edilebiliyor. (oldukça basit olsa da) bir sonraki adım soyut bir soyut orta düzey temsildir. İlk adım Binaryen'de yeni bir modül oluşturun:

const module = new binaryen.Module();

Soyut söz dizimi ağacının her satırı firstOperand, operator ve secondOperand. Olası dört durumun her biri için exampleScript'teki operatörler, yani +, -, *, /, yeni bir modüle eklenmesi gerekir yöntemini Binaryen'in Module#addFunction() yöntemiyle belirleriz. Etiketin parametreleri Module#addFunction() yöntemleri şunlardır:

  • name: string, işlevin adını temsil eder.
  • functionType: Signature, işlevin imzasını temsil eder.
  • varTypes: Type[], belirtilen sırada ek yerelleri gösterir.
  • body: Expression, işlevin içeriği.

Biraz gevşeyip kafanızı dağıtacağınız ayrıntılar Binaryen belgeleri yardımcı olabilir, ancak sonunda ExampleScript'in + operatörüne göre, birden fazla yöntemden biri olarak Module#i32.add() yöntemine mevcut tamsayı işlemler. Toplama için birinci ve ikinci toplam olmak üzere iki işlenen gerekir. fonksiyonunun gerçekten çağrılabilmesi için, dışa aktarıldı Module#addFunctionExport() ile.

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: tam sayılarla çalışan üç tanesi, yani Module#i32.add() değerine göre add(), Module#i32.sub() - subtract() / multiply() Module#i32.mul() ve Module#f64.div() bazında aykırı değer divide() çünkü ExampleScript, kayan nokta sonuçlarıyla da çalışı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 hiçbir zaman aranır. Ölü kodu yapay olarak sunmak (bunun için optimize edilmiş sonraki bir adımda ele alınacaktır) derleme işlemini Wasm'e yükleyin, dışa aktarılmayan bir işlev ekleyerek işi halleder.

// 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. Çok gerekli olmasa da kesinlikle iyi bir uygulamadır modülü doğrulama Module#validate() yöntemiyle işlem yapabilirsiniz.

if (!module.validate()) {
  throw new Error('Validation error');
}

Ortaya çıkan Wasm kodunu elde etme

Alıcı: oluşturan Wasm kodunu elde edin, Binaryen'de verileri almak için metinsel temsil S ifadesinde .wat dosyası olarak bir biçimde sunmasını sağlamak ve ikili temsil doğrudan tarayıcıda çalıştırılabilen bir .wasm dosyası olarak. İkili program kodu, doğrudan tarayıcıda çalıştırılması gerekir. Çalıştığından emin olmak için dışa aktarma işlemlerini günlüğe kaydederek yardım.

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);

ExampleScript programının dördü de içeren eksiksiz metin gösterimi işlemleri aşağıda listelenmiştir. Ölü kodun nasıl hâlâ orada olduğuna dikkat edin. ancak ekran görüntüsüne göre 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)
  )
 )
)

Dört işlevin (toplama, bölme, çarpma ve çıkarma) gösterildiği WebAssembly modülü dışa aktarmalarının Geliştirici Araçları konsolu ekran görüntüsü (ancak açık olmayan ölü kodu değil).

WebAssembly'yi optimize etme

Binaryen, Wasm kodunu optimize etmek için iki yol sunar. Birincisi, Binaryen.js, diğeri de komut satırı için. İlk yöntemde, standart optimizasyon grubu uygulanır göre ayarlar ve optimizasyon ve daraltma düzeylerini ayarlamanıza olanak tanır. ikincisi varsayılan olarak hiçbir kural kullanmaz, bunun yerine tam özelleştirme sağlar, Diğer bir deyişle, yeterli denemeyle ayarları duruma göre en iyi sonuçları alabilirsiniz.

Binaryen.js ile optimizasyon

Binaryen ile bir Wasm modülünü optimize etmenin en basit yolu doğrudan Binaryen.js'nin Module#optimize() yöntemini çağırabilir ve isteğe bağlı olarak ayarlamak optimize etme ve küçültme seviyesini ayarlayın.

// 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 eklenen ölü kodu siler. Dolayısıyla, ExampleScript oyuncak örneğinin Wasm sürümünün metinsel gösterimi otomatik olarak oluşturur. local.set/get çiftlerinin optimizasyon adımları SimplifyLocals (yerellikle ilgili çeşitli optimizasyonlar) ve Elektrik süpürgesi (açıkça ihtiyaç duyulmayan kodu kaldırır) ve return, RemoveUnusedBrs (gerekmeyen konumlardaki araları kaldırır).

 (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)
  )
 )
)

Pek çok optimizasyon kartları, Module#optimize(), özel optimize etme ve daraltma seviyelerini kullanıyor. varsayılan belirler. Tam özelleştirme için wasm-opt komut satırı aracını kullanmanız gerekir.

Wasm-opt komut satırı aracıyla optimizasyon

Binaryen, kullanılacak kartların tamamen özelleştirilmesi için wasm-opt komut satırı aracı. Bir olası optimizasyon seçeneklerinin tam listesini aracın yardım mesajına bakın. wasm-opt aracı muhtemelen en popüler vardır ve Wasm kodunu optimize etmek için çeşitli derleyici araç zincirleri tarafından kullanılır. Emscripten, J2CL, Kotlin/Wasm, dart2wasm, Wasm-pack ve diğerleri.

wasm-opt --help

Pasajlar hakkında bir fikir vermek için, en başarılı olan ve anlaşılmaz olmasına dikkat edin.

  • Kod Katlama: Birleştirme yaparak yinelenen kodu önler (örneğin, iki if kodu varsa) ilişkin bazı ortak talimatlar vardır).
  • DeadArgumentEliification: Bağımsız değişkenleri kaldırmak için zaman optimizasyon geçişini bağlayın bir işleve dönüştürür.
  • MinifyImportsAndExports: Bunları "a", "b" olarak küçültür.
  • DeadCodeEliification: Ölü kodu kaldırın.

Bir optimizasyon kılavuzu çeşitli işaretler arasından hangisinin daha önemli olduğunu belirlemeye yönelik ve ilk olarak denemeye değer. Örneğin, bazen wasm-opt çalıştırılıyor sürekli tekrar tekrar, girişi daha da küçültür. Böyle durumlarda şununla: --converge işaret tekrar bir optimizasyon yapılmaya devam edene ve ulaştı.

Demo

Bu gönderide ele alınan kavramları uygulamalı olarak görmek için, yerleştirilmiş demo yapabilirsiniz. Bunun için aklınıza gelebilecek herhangi bir örnek Ayrıca şunlara dikkat edin: demonun kaynak kodunu görüntüleyin.

Sonuçlar

Binaryen, dilleri WebAssembly ve web derlemelerinde derleyen güçlü bir araç seti en iyi uygulamaları paylaşacağız. JavaScript kitaplığı ve komut satırı araçları esnek ve kullanım kolaylığını sunar. Bu gönderi, Çevik’in temel ilkelerini Binaryen'in etkinliğini ve potansiyelini vurgulayan Wasm derlemesi Maksimum optimizasyon. Binaryen'in teklifini özelleştirme seçeneklerinin çoğu optimizasyon için Wasm'ın dahili bileşenleri hakkında varsayılan ayarlar zaten sorunsuz çalışıyor. Böylece, başarılı derleme ve optimizasyonlar yapabilirsiniz. bu video hazır!

Teşekkür

Bu yayın Alon Zakai tarafından incelendi, Thomas Lively ve Rıza Ahmet