Web uygulamaları için WebAssembly performans kalıpları

WebAssembly'den yararlanmak isteyen web geliştiricilerine yönelik bu kılavuzda CPU yoğun görevler için harici kaynakları kullanarak Wasm'ı yardımcı olabilir. Bu kılavuzda, başarılı bir proje için en iyi uygulamalardan derlemelerini ve örneklendirmelerini optimize etmek için Wasm modüllerini yükleyin. Google CPU'yu yoğun şekilde kullanan görevlerin web çalışanlarına nasıl kaydırılacağını uygulama kararlarıyla (ör. web sitesini ne zaman Çalışan ve projenin kalıcı olarak mı aktif olacağı, yoksa gerektiğinde hızlandırılıp artırılmayacağı. İlgili içeriği oluşturmak için kullanılan kılavuzu yinelemeli olarak geliştirir ve tek bir performans kalıbı sunar bu da sorun için en iyi çözümü önerene kadar devam eder.

Varsayımlar

CPU'yu çok yoğun şekilde gerektiren ve yalnızca dış kaynaklardan yararlanmak istediğiniz bir göreviniz olduğunu varsayın. Yerele yakın performansı için WebAssembly'yi (Wasm) kullanın. CPU'yu yoğun olarak kullanan görev bu kılavuzda örnek olarak verilen bir sayının faktöriyelini hesaplayabilirsiniz. İlgili içeriği oluşturmak için kullanılan faktöriyel, bir tam sayının ve altındaki tüm tam sayıların çarpımıdır. Örneğin, örneğin, dörtün faktöriyeli (4! olarak yazılır) 24 değerine (yani, 4 * 3 * 2 * 1). Rakamlar çok hızlı bir şekilde büyür. Örneğin, 16! 2,004,189,184. CPU'yu yoğun şekilde kullanan bir görevin daha gerçekçi bir örneği bir barkodu tarayarak veya kafes resmini izleme.

factorial() öğesinin, performans gösteren yinelemeli (yinelemeli değil) uygulaması fonksiyonu, C++ dilinde yazılmış aşağıdaki kod örneğinde gösterilmektedir.

#include <stdint.h>

extern "C" {

// Calculates the factorial of a non-negative integer n.
uint64_t factorial(unsigned int n) {
    uint64_t result = 1;
    for (unsigned int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

}

Makalenin geri kalanı için derleme tabanlı bir Wasm modülü olduğunu factorial.wasm adlı dosyadaki Emscripten ile bu factorial() işlevi tümü kullanılıyor en iyi kod optimizasyonu uygulamalarını inceleyin. Bunun nasıl yapılacağıyla ilgili bilgilerinizi tazelemek için ccall/cWrap kullanarak JavaScript'ten derlenmiş C işlevlerini çağırma Aşağıdaki komut, factorial.wasm dosyasını bağımsız Wasm.

emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]'  --no-entry

HTML'de, output ile eşlenmiş input içeren bir form ve bir gönderme var button. Bu öğelere, adlarına göre JavaScript'ten referans verilir.

<form>
  <label>The factorial of <input type="text" value="12" /></label> is
  <output>479001600</output>.
  <button type="submit">Calculate</button>
</form>
const input = document.querySelector('input');
const output = document.querySelector('output');
const button = document.querySelector('button');

Modülün yüklenmesi, derleme ve örneklendirmesi

Bir Wasm modülünü kullanabilmeniz için önce modülü yüklemeniz gerekir. Web'de bu durum aracılığıyla fetch() API'ye gidin. Sizin de bildiğiniz gibi, web uygulamanız İşlemciyi yoğun olarak kullanan bir görevse Wasm dosyasını mümkün olduğunca erken bir şekilde önceden yüklemeniz gerekir. Siz bunu bir CORS özellikli getirme (uygulamanızın <head> bölümünde).

<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />

Aslında, fetch() API eşzamansızdır ve await işlemini yardımcı olur.

fetch('factorial.wasm');

Ardından, Wasm modülünü derleyin ve örneklendirin. İrili ufaklı bir ada sahip olan fonksiyonlar WebAssembly.compile() (artı WebAssembly.compileStreaming()) ve WebAssembly.instantiate() kavramak yerine WebAssembly.instantiateStreaming() yöntemi, doğrudan akıştan bir Wasm modülü derler ve fetch() gibi temel kaynağa await gerek yoktur. Bu, proje başlatma belgenizin ve Wasm kodunun yüklenmesi için optimize edilmiş bir yöntem. Wasm modülünün factorial() işlevini hemen kullanabilirsiniz.

const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);
const factorial = resultObject.instance.exports.factorial;

button.addEventListener('click', (e) => {
  e.preventDefault();
  output.textContent = factorial(parseInt(input.value, 10));
});

Görevi bir web çalışanına kaydırır

Bunu gerçekten CPU yoğun görevlerle ana iş parçacığında çalıştırırsanız uygulamanın tamamını engelliyor. Yaygın olarak kullanılan bir uygulama, bu tür görevleri bir web Çalışan.

Ana iş parçacığının yeniden yapılandırması

CPU'yu yoğun şekilde kullanan görevi bir web çalışanına taşımak için atılacak ilk adım, işlemi yeniden yapılandırmaktır. takip edebilirsiniz. Ana ileti dizisi artık bir Worker oluşturuyor ve bunun dışında yalnızca girişin Web İşçisine gönderilmesi ve ardından girişin ve o çıktıyı görüntüler.

/* Main thread. */

let worker = null;

// When the button is clicked, submit the input value
//  to the Web Worker.
button.addEventListener('click', (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker('worker.js');

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({ integer: parseInt(input.value, 10) });
});

Kötü: Görev Web Çalışanı'nda çalışıyor ancak kod müstehcen

Web çalışanı, Wasm modülünü örneklendirir ve bir mesaj aldıktan sonra işlemi gerçekleştirir ve sonucu ana iş parçacığına geri gönderir. Bu yaklaşımdaki sorun, bir Wasm modülünün mevcut durumla WebAssembly.instantiateStreaming() eşzamansız bir işlemdir. Bunun anlamı şudur: olduğunu belirtmiyor. En kötü durumda ana iş parçacığı, Web Worker henüz hazır değildir ve Web Worker, iletiyi hiçbir zaman almaz.

/* Worker thread. */

// Instantiate the Wasm module.
// 🚫 This code is racy! If a message comes in while
// the promise is still being awaited, it's lost.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);
const factorial = resultObject.instance.exports.factorial;

// Listen for incoming messages, run the task,
// and post the result.
self.addEventListener('message', (e) => {
  const { integer } = e.data;
  self.postMessage({ result: factorial(integer) });
});

Daha iyi: Görev, Web Çalışanı'nda çalışır, ancak gereksiz yükleme ve derleme olabilir

Eşzamansız Wasm modülü örneklendirmesi sorununun geçici bir çözümü Wasm modülü yükleme, derleme ve örneklendirme işlemlerinin tamamını etkinliğe taşıyın ancak bu, söz konusu çalışmanın her eğitimde mesaj alındı. HTTP önbelleği ve HTTP önbelleği sayesinde derlenmiş Wasm bayt kodu. Bu en kötü çözüm değil, ancak daha iyi sağlar.

Eşzamansız kodu Web Çalışanı'nın başına değil, aslında verdiğimiz sözü yerine getirmek için beklemek yerine, verdiğiniz sözü değişkeni olduğunda, program hemen etkinliğin etkinlik işleyici bölümüne ve ana ileti dizisinden gelen hiçbir mesaj kaybolmaz. Etkinliğin iç kısmı bu durumda vaat beklenebilir.

/* Worker thread. */

const importObject = {};
// Instantiate the Wasm module.
// 🚫 If the `Worker` is spun up frequently, the loading
// compiling, and instantiating work will happen every time.
const wasmPromise = WebAssembly.instantiateStreaming(
  fetch('factorial.wasm'),
  importObject,
);

// Listen for incoming messages
self.addEventListener('message', async (e) => {
  const { integer } = e.data;
  const resultObject = await wasmPromise;
  const factorial = resultObject.instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({ result });
});

İyi: Görev Web Çalışanı'nda çalışıyor ve yalnızca bir kez yüklenip derleniyor

Statik WebAssembly.compileStreaming() çözüme ulaştırılacak WebAssembly.Module. Bu nesnenin güzel bir özelliği, kullanabileceğiniz postMessage(). Bu, Wasm modülünün ana modda yalnızca bir kez yüklenip derlenebileceği anlamına gelir. iş parçacığı (hatta sadece yükleme ve derleme ile ilgilenen başka bir Web İşçisi bile), ve ardından yoğun CPU kullanımından sorumlu Web Çalışanına aktarılır görevi görebilir. Aşağıdaki kodda bu akış gösterilmektedir.

/* Main thread. */

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

let worker = null;

// When the button is clicked, submit the input value
// and the Wasm module to the Web Worker.
button.addEventListener('click', async (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker('worker.js');

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

Web çalışanı tarafında ise geriye kalan tek şey WebAssembly.Module öğesini ayıklamaktır. ve örneklendiririm. WebAssembly.Module içeren mesaj akışı gerçekleştirildiğinde, Web Çalışanı'ndaki kod WebAssembly.instantiate() (önceki değer instantiateStreaming() varyantına kıyasla). Örneklenen modülü bir değişkende önbelleğe alınır, böylece örneklendirme çalışmasının yalnızca bir kez de web’de çalışan bir ekiple çalışmaya başladım.

/* Worker thread. */

let instance = null;

// Listen for incoming messages
self.addEventListener('message', async (e) => {
  // Extract the `WebAssembly.Module` from the message.
  const { integer, module } = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via `postMessage()`.
  instance = instance || (await WebAssembly.instantiate(module, importObject));
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({ result });
});

Mükemmel: Görev, satır içi Web İşçisinde çalışır ve yalnızca bir kez yüklenip derlenir

HTTP önbelleğinde bile olsa (ideal olarak) önbelleğe alınan Web Worker kodunu almak ve ağa ulaşmak pahalıya mal olur. Performans konusunda yaygın olarak kullanılan püf noktalarından biri Web Çalışanını satır içine alın ve blob: URL'si olarak yükleyin. Bu da örneklendirmek için Web İşçisine geçirilecek Wasm modülünü derledi ana iş parçacığının bağlamları farklı olsa da aynı JavaScript kaynak dosyasına dayalı olarak.

/* Main thread. */

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

let worker = null;

const blobURL = URL.createObjectURL(
  new Blob(
    [
      `
let instance = null;

self.addEventListener('message', async (e) => {
  // Extract the \`WebAssembly.Module\` from the message.
  const {integer, module} = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via \`postMessage()\`.
  instance = instance || await WebAssembly.instantiate(module, importObject);
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({result});
});
`,
    ],
    { type: 'text/javascript' },
  ),
);

button.addEventListener('click', async (e) => {
  e.preventDefault();

  // Create the Web Worker lazily on-demand.
  if (!worker) {
    worker = new Worker(blobURL);

    // Listen for incoming messages and display the result.
    worker.addEventListener('message', (e) => {
      output.textContent = e.result;
    });
  }

  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

Tembel veya istekli Web Çalışanı oluşturma

Şimdiye kadar tüm kod örnekleri Web Çalışanı'nı istek üzerine, diğer bir deyişle, görebilirsiniz. Uygulamanıza bağlı olarak daha istekli bir şekilde oluşturmalarını sağlar. Örneğin, uygulama boştayken veya hatta bir parçası haline geldi. Bu nedenle, Web Worker oluşturma işlemini taşıyın. kodunu düğmenin etkinlik işleyicisinin dışına çıkarmalıdır.

const worker = new Worker(blobURL);

// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
  output.textContent = e.result;
});

Web çalışanını görev başında tutma veya göstermeme

Kendinize web çalışanını kullanmaya devam edip etmeyeceğiniz, veya ihtiyaç duyduğunuzda yeniden oluşturabilirsiniz. Her iki yaklaşım da avantaj ve dezavantajları var. Örneğin, bir web sitesinin Sürekli görevde bulunmak, uygulamanızın bellek ayak izini artırabilir ve eş zamanlı görevlerle uğraşmak daha zor olur. Çünkü, bir şekilde sonuçları bu isteklere geri dönüyor. Diğer yandan, Web Çalışanın önyükleme kodu oldukça karmaşık olabilir; bu nedenle her seferinde yeni bir metrik oluşturmanız gerekir. Neyse ki bunu kendi başınıza Böylece, User Timing API.

Şimdiye kadar kod örnekleri kalıcı bir Web Çalışanı tuttu. Aşağıdakiler kod örneği, gerektiğinde yeni bir Web Worker geçici olarak oluşturur. Şunların gerektiğini unutmayın: takip etmek için Web Çalışanı'nı feshetme kendiniz. (Kod snippet'i hata işlemeyi atlar, ancak (başarısız veya yanlış olması fark etmez), her durumda sonlandırın.)

/* Main thread. */

let worker = null;

const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));

const blobURL = URL.createObjectURL(
  new Blob(
    [
      `
// Caching the instance means you can switch between
// throw-away and permanent Web Worker freely.
let instance = null;

self.addEventListener('message', async (e) => {
  // Extract the \`WebAssembly.Module\` from the message.
  const {integer, module} = e.data;
  const importObject = {};
  // Instantiate the Wasm module that came via \`postMessage()\`.
  instance = instance || await WebAssembly.instantiate(module, importObject);
  const factorial = instance.exports.factorial;
  const result = factorial(integer);
  self.postMessage({result});
});  
`,
    ],
    { type: 'text/javascript' },
  ),
);

button.addEventListener('click', async (e) => {
  e.preventDefault();
  // Terminate a potentially running Web Worker.
  if (worker) {
    worker.terminate();
  }
  // Create the Web Worker lazily on-demand.
  worker = new Worker(blobURL);
  worker.addEventListener('message', (e) => {
    worker.terminate();
    worker = null;
    output.textContent = e.data.result;
  });
  worker.postMessage({
    integer: parseInt(input.value, 10),
    module: await modulePromise,
  });
});

Demolar

Üzerinde oynayabileceğiniz iki demo var. Bir anlık web işçisi (kaynak kod) diğerinde ise kalıcı web işçisi (kaynak kod). Chrome Geliştirici Araçları'nı açıp Console'u kontrol ederseniz kullanıcı Düğme tıklamasından tıklama işlemine kadar geçen süreyi ölçen Timing API günlükleri ekranda gösterilen sonucu verir. Ağ sekmesinde blob: URL'si gösterilir istek. Bu örnekte, geçici ve kalıcı arasındaki zamanlama farkı yaklaşık 3×. Pratikte, insan gözüne göre bu sistemde ikisi de birbirinden ayırt edilemez. dava açın. Kendi gerçek yaşam uygulamanızın sonuçları büyük olasılıkla farklı olacaktır.

Geçici bir çalışan içeren faktöriyel Wasm demo uygulaması. Chrome Geliştirici Araçları açık. İki blob vardır: Ağ sekmesindeki ve Console&#39;daki URL istekleri iki hesaplama zamanlaması gösterir.

Kalıcı bir Çalışanın bulunduğu faktöriyel Wasm demo uygulaması. Chrome Geliştirici Araçları açık. Yalnızca bir blob var: Ağ sekmesinde ve Console&#39;da dört hesaplama zamanlaması URL isteği gösteriliyor.

Sonuçlar

Bu gönderide, Wasm'i kullanırken yararlanabileceğiniz bazı performans kalıpları keşfediliyor.

  • Genel bir kural olarak, akış yöntemlerini tercih edin (WebAssembly.compileStreaming() ve WebAssembly.instantiateStreaming()) benzerlerine kıyasla (WebAssembly.compile() ve WebAssembly.instantiate()) tıklayın.
  • Mümkünse performans ağırlıklı işleri bir web çalışanında dış kaynak kullanarak yaptırın ve Wasm yükleme ve derleme, Web Worker'ın dışında yalnızca bir kez çalışır. Bu şekilde, Web işçisinin, yalnızca ana makineden aldığı Wasm modülünü örneklendirmesi gerekir. yükleme ve derlemenin gerçekleştiği WebAssembly.instantiate(): Bu durumda örneğin, şunları yaparsanız örnek önbelleğe alınabilir: her zaman erişilebilir olmasını sağlar.
  • Tek bir daimi Web Çalışanı bulundurmanın mantıklı olup olmadığını dikkatlice ölçün. veya ihtiyaç duyduklarında geçici Web İşçileri oluşturmak isteyebilirler. Ayrıca en uygun zamanın ne olduğunu düşünün. Dikkate alınması gereken noktalar bellek tüketimi, web çalışanı örneklendirme süresi, Ayrıca eşzamanlı isteklerle uğraşmanın karmaşıklığı da vardır.

Bu kalıpları göz önünde bulundurursanız optimum performans elde etmek için doğru yoldasınız demektir. Wasm performansı.

Teşekkür

Bu kılavuz, tarafından incelendi Andreas Haas, Jakob Kummerow, Deepti Gandluri, Alon Zakai, Furkan McCabe, François Beaufort ve Rachel Andrew.