การโหลดโมดูล 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 แบบซิงโครนัส ซึ่งหมายความว่าเทรดหลักจะบล็อกเทรดหลักจนกว่าจะเสร็จสมบูรณ์ หากไม่ต้องการให้เป็นเช่นนั้น 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.compatibleStreaming() จะทำให้การคอมไพล์เสร็จสิ้นเกือบจะทันทีหลังจากที่ดาวน์โหลดไบต์สุดท้ายแล้ว

หากต้องการเปิดใช้การเพิ่มประสิทธิภาพนี้ ให้ใช้ 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 แบบอะซิงโครนัสเพื่อหลีกเลี่ยงการบล็อกเทรดหลัก
  • ใช้ API สตรีมมิงเพื่อคอมไพล์และสร้างอินสแตนซ์ของโมดูล WebAssembly ได้เร็วขึ้น
  • อย่าเขียนโค้ดที่ไม่ต้องการ

สนุกกับ WebAssembly