การโหลดโมดูล WebAssembly อย่างมีประสิทธิภาพ

เมื่อทํางานกับ WebAssembly คุณมักจะต้องดาวน์โหลดโมดูล คอมไพล์ อินสแตนซ์ แล้วใช้สิ่งที่ส่งออกใน JavaScript โพสต์นี้จะอธิบายแนวทางที่เราแนะนำเพื่อประสิทธิภาพสูงสุด

เมื่อทํางานกับ WebAssembly คุณมักจะต้องดาวน์โหลดโมดูล คอมไพล์ อินสแตนซ์ และจากนั้นใช้สิ่งที่ส่งออกใน JavaScript โพสต์นี้เริ่มต้นด้วยข้อมูลโค้ดทั่วไปแต่มีประสิทธิภาพต่ำกว่ามาตรฐาน ซึ่งจะกล่าวถึงการเพิ่มประสิทธิภาพที่เป็นไปได้หลายแบบ และสุดท้ายก็จะแสดงวิธีที่ง่ายและมีประสิทธิภาพมากที่สุดในการเรียกใช้ WebAssembly จาก JavaScript

ข้อมูลโค้ดนี้จะดาวน์โหลด คอมไพล์ และสร้างอินสแตนซ์อย่างสมบูรณ์ แม้ว่าจะไม่ใช่วิธีที่มีประสิทธิภาพสูงสุดก็ตาม

อย่าใช้

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

อย่าลืมสังเกตวิธีที่เราใช้ new WebAssembly.Module(buffer) เพื่อเปลี่ยนบัฟเฟอร์คำตอบเป็นโมดูล ซึ่งเป็น API แบบซิงโครนัส ซึ่งหมายความว่า API นี้จะบล็อกเทรดหลักจนกว่าจะเสร็จสมบูรณ์ Chrome จะปิดใช้ WebAssembly.Module สำหรับบัฟเฟอร์ที่มีขนาดใหญ่กว่า 4 KB เพื่อไม่แนะนำให้ใช้ หากต้องการหลีกเลี่ยงขีดจำกัดด้านขนาด เราอาจใช้ await WebAssembly.compile(buffer) แทน

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

await WebAssembly.compile(buffer) ยังคงไม่ใช่แนวทางที่เหมาะสมที่สุด แต่เราจะพูดถึงเรื่องนี้ในอีกสักครู่

ปัจจุบันการดำเนินการเกือบทุกอย่างในข้อมูลโค้ดที่แก้ไขจะเป็นแบบไม่พร้อมกัน เนื่องจากการใช้ await ทำให้เห็นได้อย่างชัดเจน ข้อยกเว้นเพียงอย่างเดียวคือ new WebAssembly.Instance(module) ซึ่งมีขีดจำกัดขนาดบัฟเฟอร์ 4 KB เช่นเดียวกับใน Chrome เราสามารถใช้แบบไม่พร้อมกัน WebAssembly.instantiate(module) เพื่อรักษาความสม่ำเสมอและทำให้เธรดหลักว่าง

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

กลับมาที่การเพิ่มประสิทธิภาพ compile ที่เราบอกใบ้ไปก่อนหน้านี้ เมื่อใช้การคอมไพล์สตรีมมิง เบราว์เซอร์จะเริ่มคอมไพล์โมดูล WebAssembly แล้วขณะที่ดาวน์โหลดไบต์ของโมดูลอยู่ เนื่องจากการดาวน์โหลดและการคอมไพล์เกิดขึ้นพร้อมกัน วิธีนี้จึงเร็วกว่า โดยเฉพาะสำหรับเพย์โหลดขนาดใหญ่

เมื่อเวลาดาวน์โหลดนานกว่าเวลาคอมไพล์ของโมดูล WebAssembly นั้น WebAssembly.compileStreaming() จะคอมไพล์เสร็จเกือบทันทีหลังจากดาวน์โหลดไบต์สุดท้าย

หากต้องการเปิดใช้การเพิ่มประสิทธิภาพนี้ ให้ใช้ WebAssembly.compileStreaming แทน WebAssembly.compile การเปลี่ยนแปลงนี้ยังช่วยให้เรากำจัดบัฟเฟอร์อาร์เรย์กลางได้ด้วย เนื่องจากตอนนี้เราสามารถส่งผ่านอินสแตนซ์ Response ที่ await fetch(url) แสดงผลได้โดยตรง

(async () => {
  const response = await fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(response);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

นอกจากนี้ WebAssembly.compileStreaming API ยังยอมรับสัญญาที่แก้ไขเป็นอินสแตนซ์ Response ได้อีกด้วย หากคุณไม่ใช้ response ในตำแหน่งอื่นในโค้ด คุณจะส่งสัญญาที่ fetch ส่งคืนมาได้โดยตรงโดยไม่ต้องawaitระบุผลลัพธ์อย่างชัดแจ้ง

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(fetchPromise);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

หากไม่ต้องการผลลัพธ์ fetch ในส่วนอื่นๆ ด้วย คุณก็ส่งผลลัพธ์นั้นโดยตรงได้ ดังนี้

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

แต่โดยส่วนตัวแล้ว การแบ่งออกเป็นอีกบรรทัดหนึ่งอ่านได้ง่ายกว่า

มาดูกันว่าเราคอมไพล์คำตอบเป็นโมดูลและสร้างอินสแตนซ์ทันทีได้อย่างไร ปรากฏว่า WebAssembly.instantiate สามารถคอมไพล์และสร้างอินสแตนซ์ได้ในครั้งเดียว โดย WebAssembly.instantiateStreaming API จะทําดังนี้

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // To create a new instance later:
  const otherInstance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

หากต้องการอินสแตนซ์เดียว ก็ไม่จำเป็นต้องเก็บออบเจ็กต์ module ไว้ โค้ดจะง่ายขึ้นอีก

// This is our recommended way of loading WebAssembly.
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

สรุปการเพิ่มประสิทธิภาพที่เราใช้ได้มีดังนี้

  • ใช้ API แบบไม่พร้อมกันเพื่อหลีกเลี่ยงการบล็อกเทรดหลัก
  • ใช้ Streaming API เพื่อคอมไพล์และสร้างอินสแตนซ์ของโมดูล WebAssembly ได้เร็วขึ้น
  • อย่าเขียนโค้ดที่ไม่จําเป็น

ขอให้สนุกกับ WebAssembly