ในคู่มือนี้มีจุดมุ่งหมายไปยังนักพัฒนาเว็บที่ต้องการได้รับประโยชน์จาก WebAssembly คุณจะได้เรียนรู้วิธีใช้ Wasm เพื่อจัดจ้างงานที่ใช้ CPU มากด้วย ความช่วยเหลือสำหรับตัวอย่างที่ทำงานอยู่ คู่มือนี้ครอบคลุมทุกเรื่อง ตั้งแต่แนวทางปฏิบัติแนะนำสำหรับ การโหลดโมดูล Wasm เพื่อเพิ่มประสิทธิภาพการคอมไพล์และการสร้างอินสแตนซ์ ทั้งนี้ อธิบายเพิ่มเติมถึงการเปลี่ยนงานที่ต้องใช้ CPU ให้กับ Web Workers และพิจารณาในเรื่อง ในการตัดสินใจเกี่ยวกับการติดตั้งใช้งาน คุณจะต้องเผชิญ เช่น เมื่อใดที่ควรสร้าง ผู้ปฏิบัติงานและเลือกว่าจะคงอยู่อย่างถาวรหรือหมุนเมื่อจำเป็น พัฒนาแนวทางอย่างสม่ำเสมอและแนะนำรูปแบบประสิทธิภาพ 1 รูปแบบ จนกว่าจะแนะนำวิธีแก้ปัญหาที่ดีที่สุด
สมมติฐาน
สมมติว่าคุณมีงานที่ใช้ CPU มากและต้องการจ้างบุคคลภายนอกให้
WebAssembly (Wasm) สำหรับประสิทธิภาพการทำงานแบบท้องถิ่น งานที่ใช้ CPU มาก
จะใช้เป็นตัวอย่างในคู่มือนี้ในการคำนวณแฟกทอเรียลของจำนวน
แฟกทอเรียลคือผลคูณของจำนวนเต็มและจำนวนเต็มทั้งหมดที่อยู่ใต้ สำหรับ
เช่น แฟกทอเรียลของ 4 (เขียนเป็น 4!
) เท่ากับ 24
(นั่นคือ
4 * 3 * 2 * 1
) ตัวเลขจะเพิ่มขึ้นอย่างรวดเร็ว ตัวอย่างเช่น 16!
คือ
2,004,189,184
ตัวอย่างที่สมจริงยิ่งขึ้นของงานที่ใช้ CPU มากอาจจะเป็น
การสแกนบาร์โค้ด หรือ
การติดตามรูปภาพแรสเตอร์
การติดตั้งใช้งานซ้ำเพื่อประสิทธิภาพ (แทนที่จะเป็นการเกิดซ้ำ) ของ factorial()
จะแสดงในตัวอย่างโค้ดต่อไปนี้ที่เขียนด้วย C++
#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;
}
}
สำหรับส่วนที่เหลือของบทความ ให้สมมติว่ามีโมดูล Wasm ตามการคอมไพล์
ฟังก์ชัน factorial()
นี้ที่มี Emscripten ในไฟล์ชื่อ factorial.wasm
โดยใช้ทั้งหมด
แนวทางปฏิบัติแนะนำในการเพิ่มประสิทธิภาพโค้ด
หากต้องการทบทวนวิธีดำเนินการนี้ โปรดอ่าน
การเรียกใช้ฟังก์ชัน C ที่คอมไพล์จาก JavaScript โดยใช้ ccall/cwrap
มีการใช้คำสั่งต่อไปนี้เพื่อคอมไพล์ factorial.wasm
เป็น
Wasm แบบสแตนด์อโลน
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
ใน HTML จะมี form
ที่มี input
ที่จับคู่กับ output
และการส่ง
button
โดยองค์ประกอบเหล่านี้จะอ้างอิงจาก JavaScript ตามชื่อองค์ประกอบ
<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');
การโหลด การคอมไพล์ และการสร้างอินสแตนซ์ของโมดูล
คุณต้องโหลดโมดูล Wasm ก่อนจึงจะใช้ได้ บนเว็บ
ผ่านทาง
fetch()
API ดังที่คุณทราบแล้วว่าเว็บแอปของคุณขึ้นอยู่กับโมดูล Wasm สำหรับ
งานที่ใช้ CPU มาก คุณควรโหลดไฟล์ Wasm ล่วงหน้าโดยเร็วที่สุด คุณ
ให้ทำสิ่งต่างๆ ด้วย
การดึงข้อมูลที่พร้อมใช้งาน CORS
ในส่วน <head>
ของแอป
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
ในความเป็นจริง fetch()
API เป็นแบบไม่พร้อมกันและคุณต้องawait
ผลลัพธ์
fetch('factorial.wasm');
จากนั้น คอมไพล์และสร้างอินสแตนซ์โมดูล Wasm มีชื่อที่น่าสนใจ
ฟังก์ชันที่เรียกว่า
WebAssembly.compile()
(เครื่องหมายบวก
WebAssembly.compileStreaming()
)
และ
WebAssembly.instantiate()
สำหรับงานเหล่านี้ แต่จะมี
WebAssembly.instantiateStreaming()
คอมไพล์ และ สร้างอินสแตนซ์โมดูล Wasm โดยตรงจากสตรีม
แหล่งที่มาที่สำคัญ เช่น fetch()
ไม่จำเป็นต้องมี await
ตัวเลือกนี้มีประสิทธิภาพมากที่สุด
และวิธีที่มีประสิทธิภาพในการโหลดโค้ด Wasm เช่นกัน สมมติว่าโมดูล Wasm ส่งออก
factorial()
คุณก็สามารถใช้ได้ทันที
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));
});
เปลี่ยนงานเป็น Web Worker
หากคุณดำเนินการนี้ในเทรดหลักโดยมีงานที่ใช้ CPU มากจริงๆ คุณจะมีความเสี่ยง บล็อกทั้งแอป แนวทางปฏิบัติทั่วไปคือการย้ายงานดังกล่าวไปยังเว็บ ผู้ปฏิบัติงาน
โครงสร้างใหม่ของเทรดหลัก
ในการย้ายงานที่ใช้ CPU มากไปยัง Web Worker ขั้นตอนแรกคือการปรับโครงสร้าง
แอปพลิเคชัน ตอนนี้ชุดข้อความหลักจะสร้าง Worker
และนอกเหนือจากนั้น
ดีลกับการส่งข้อมูลที่ป้อนไปยัง Web Worker เท่านั้น และจากนั้นจึงได้รับ
และแสดง
/* 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) });
});
ไม่ถูกต้อง: งานทำงานใน Web Worker แต่โค้ดเป็นโค้ดสำหรับผู้ใหญ่
Web Worker จะสร้างอินสแตนซ์ของโมดูล Wasm และเมื่อได้รับข้อความ
จะทำงานที่ใช้ CPU มากและส่งผลลัพธ์กลับไปยังเทรดหลัก
ปัญหาของวิธีนี้คือการสร้างอินสแตนซ์โมดูล Wasm ด้วย
WebAssembly.instantiateStreaming()
เป็นการดำเนินการที่ไม่พร้อมกัน ซึ่งหมายความว่า
โค้ดนี้เป็นโค้ดสำหรับผู้ใหญ่ ในกรณีที่แย่ที่สุด เทรดหลักจะส่งข้อมูลเมื่อ
Web Worker ยังไม่พร้อม และ Web Worker จะไม่ได้รับข้อความ
/* 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) });
});
ดีกว่า: งานทำงานใน Web Worker แต่อาจมีการโหลดและคอมไพล์ที่ซ้ำซ้อน
วิธีแก้ไขวิธีหนึ่งในการแก้ปัญหาการสร้างอินสแตนซ์โมดูล Wasm แบบไม่พร้อมกันคือ ย้ายการโหลดโมดูล Wasm การคอมไพล์ และการสร้างอินสแตนซ์ทั้งหมดไปยังเหตุการณ์ Listener ของคุณ แต่การดำเนินการนี้หมายความว่า การดำเนินการนี้จะต้องเกิดขึ้นในทุกๆ ได้รับข้อความแล้ว ด้วยการแคช HTTP และแคช HTTP สามารถแคช ไบต์โค้ด Wasm ที่คอมไพล์แล้ว นี่ไม่ใช่วิธีแก้ไขที่แย่ที่สุด แต่มีวิธีที่แย่กว่า
โดยการย้ายโค้ดอะซิงโครนัสไปยังจุดเริ่มต้นของ Web Worker ไม่ใช่ กำลังรอให้คำสัญญาที่ให้ไว้ แต่เลือกที่จะเก็บคำสัญญาไว้ใน โปรแกรมจะย้ายไปที่ส่วน Listener เหตุการณ์ของ ได้โดยจะไม่มีข้อความจากชุดข้อความหลักสูญหาย ภายในงาน ผู้ฟัง คำมั่นสัญญานั้นจะรอคุณอยู่
/* 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 });
});
ดี: งานทำงานใน Web Worker และโหลดและคอมไพล์เพียงครั้งเดียว
ผลลัพธ์ของสถิติ
WebAssembly.compileStreaming()
เป็นสัญญาที่แก้ไข
WebAssembly.Module
ฟีเจอร์ที่ดีอย่างหนึ่งของออบเจ็กต์นี้คือสามารถโอนโดยใช้
postMessage()
ซึ่งหมายความว่าคุณสามารถโหลดและคอมไพล์โมดูล Wasm ได้เพียงครั้งเดียวใน
เทรด (หรือแม้แต่โปรแกรมทำงานบนเว็บอื่นที่เกี่ยวข้องโดยตรงกับการโหลดและคอมไพล์)
จากนั้นจะโอนไปยัง Web Worker ที่รับผิดชอบค่า CPU ที่ใช้ CPU มาก
งาน โค้ดต่อไปนี้จะแสดงขั้นตอนนี้
/* 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 Worker ที่เหลือก็คือการดึงข้อมูล WebAssembly.Module
แล้วสร้างอินสแตนซ์ เนื่องจากข้อความกับ WebAssembly.Module
ไม่
สตรีมแล้ว ขณะนี้รหัสใน Web Worker จะใช้
WebAssembly.instantiate()
แทนที่จะเป็นตัวแปร instantiateStreaming()
จากก่อนหน้านี้ สร้างอินสแตนซ์แล้ว
มีการแคชโมดูลในตัวแปร ดังนั้นงานอินสแตนซ์จึงจำเป็นต้องเกิดขึ้นเท่านั้น
ครั้งหนึ่งหลังจากที่สร้าง Web Worker
/* 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 });
});
สมบูรณ์แบบ: งานทำงานใน Web Worker แบบอินไลน์ และโหลดและคอมไพล์เพียงครั้งเดียว
แม้แต่การแคช HTTP การรับโค้ด Web Worker ที่แคชไว้ (ควรจะเป็น) และ
เครือข่ายที่มีโอกาสเข้าถึงจะมีค่าใช้จ่ายสูง เคล็ดลับด้านประสิทธิภาพที่พบบ่อยคือ
ในบรรทัด Web Worker และโหลดเป็น URL blob:
ซึ่งยังคงต้องมี
คอมไพล์โมดูล Wasm เพื่อส่งไปยัง Web Worker สำหรับการเริ่มต้นอินสแตนซ์ เช่น
บริบทของ Web Worker และเทรดหลักแตกต่างกัน
จากไฟล์ต้นฉบับ JavaScript เดียวกัน
/* 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,
});
});
การสร้าง Web Worker แบบ Lazy Loading หรือมีความกระตือรือร้น
ที่ผ่านมา ตัวอย่างโค้ดทั้งหมดสร้าง Web Worker แบบ Lazy Loading ตามคำขอ ซึ่งก็คือ เวลาที่กดปุ่ม สิ่งที่ควรทำคือ ทั้งนี้ขึ้นอยู่กับแอปพลิเคชันของคุณ สร้าง Web Worker อย่างกระตือรือร้นมากขึ้น ตัวอย่างเช่น เมื่อแอปไม่มีการใช้งานหรือ ของกระบวนการเปิดเครื่องของแอปได้ ดังนั้น ให้ย้ายการสร้าง Web Worker นอก Listener เหตุการณ์ของปุ่ม
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
ให้ Web Worker ทำงานได้ตลอดเวลา
คำถามหนึ่งที่คุณอาจถามตัวเองก็คือ คุณควรเก็บ Web Worker ไว้หรือไม่ อย่างถาวร หรือสร้างขึ้นใหม่ได้ทุกเมื่อที่ต้องการ ทั้ง 2 แนวทาง ที่เป็นไปได้ทั้งในด้านนี้ ทั้งข้อดีและข้อเสีย ตัวอย่างเช่น การทำให้เว็บ ผู้ปฏิบัติงานที่อยู่ถาวรอาจเพิ่มพื้นที่หน่วยความจำของแอปและทำให้ และต้องจัดการกับงานที่เกิดขึ้นพร้อมกันให้ยากขึ้น เนื่องจาก คุณต้องแมปผลลัพธ์ ที่มาจาก Web Worker กลับไปยังคำขอ ในทางกลับกัน เว็บ โค้ด Bootstrapping ของผู้ปฏิบัติงานอาจค่อนข้างซับซ้อน จึงอาจมีข้อมูลจำนวนมาก ค่าใช้จ่าย หากคุณสร้างใหม่ในแต่ละครั้ง โชคดีที่นี่คือสิ่งที่คุณสามารถ วัดด้วยฟังก์ชัน User Timing API
จนถึงขณะนี้ตัวอย่างโค้ดได้เก็บ Web Worker แบบถาวรไว้ 1 ราย ดังต่อไปนี้ ตัวอย่างโค้ดจะสร้างงานเฉพาะกิจรายการใหม่ของ Web Worker ทุกครั้งที่จำเป็น โปรดทราบว่าคุณต้องมี ในการติดตาม การสิ้นสุด Web Worker ตัวคุณเอง (ข้อมูลโค้ดจะข้ามการจัดการข้อผิดพลาด แต่หากเกิดข้อผิดพลาดขึ้น มีข้อผิดพลาด โปรดตรวจดูว่าสิ้นสุดการดำเนินงานในทุกกรณี ไม่ว่าจะสำเร็จหรือล้มเหลว)
/* 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,
});
});
เดโม
มีการสาธิต 2 แบบให้คุณได้เล่น รายการที่มี
ผู้ปฏิบัติงานบนเว็บเฉพาะกิจ
(ซอร์สโค้ด)
และอีกรายการที่มี
Web Worker อย่างถาวร
(ซอร์สโค้ด)
หากคุณเปิดเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome และตรวจสอบคอนโซล คุณจะเห็น
บันทึก Timing API ที่วัดระยะเวลาที่ใช้ตั้งแต่การคลิกปุ่มไปจนถึง
ผลลัพธ์ที่แสดงบนหน้าจอ แท็บเครือข่ายจะแสดง URL ของ blob:
คำขอ ในตัวอย่างนี้ ความแตกต่างของเวลาระหว่างแบบเฉพาะกิจและถาวร
ประมาณ 3× ในทางปฏิบัติแล้ว ในสายตามนุษย์แล้ว ทั้งสองสิ่งนี้ไม่สามารถแยกความแตกต่างได้ในเรื่องนี้
ผลลัพธ์สําหรับแอปในชีวิตจริงของคุณเองมักจะแตกต่างกัน
บทสรุป
โพสต์นี้มีการสำรวจรูปแบบประสิทธิภาพบางอย่างในการจัดการกับ Wasm
- ตามกฎทั่วไป เราขอแนะนำให้ใช้วิธีการสตรีมมิง
(
WebAssembly.compileStreaming()
และWebAssembly.instantiateStreaming()
) โฆษณาที่ไม่ใช่สตรีมมิง (WebAssembly.compile()
และWebAssembly.instantiate()
) - หากทำได้ ให้จ้าง Web Worker ที่ทำงานหนักๆ จากภายนอกมาเป็นงาน Wasm
การโหลดและคอมไพล์จะทำงานนอก Web Worker เพียงครั้งเดียว วิธีนี้ทำให้
Web Worker เพียงต้องสร้างอินสแตนซ์โมดูล Wasm ที่ได้รับการตั้งค่าจาก
เทรดที่เกิดการโหลดและการคอมไพล์ด้วย
WebAssembly.instantiate()
ซึ่งหมายความว่าระบบแคชอินสแตนซ์ได้หากคุณ ให้ Web Worker ทำงานอย่างถาวร - วัดอย่างรอบคอบว่าการเก็บ Web Worker ไว้อย่างถาวรเป็นความคิดที่ดีหรือไม่ ตลอดไป หรือจะสร้าง Web Worker เฉพาะกิจก็ได้เช่นกัน และ คิดว่าช่วงเวลาใดที่ควรสร้าง Web Worker ได้ดีที่สุด สิ่งที่ต้องพิจารณา ได้แก่ การใช้หน่วยความจำ ระยะเวลาการสร้างอินสแตนซ์ Web Worker แต่ยังมีความซับซ้อนของการต้องจัดการกับคำขอที่เกิดขึ้นพร้อมกัน
หากคุณนำรูปแบบเหล่านี้มาพิจารณา คุณก็มาถูกทางแล้วที่จะเหมาะสมที่สุด ประสิทธิภาพ Wasm
กิตติกรรมประกาศ
คู่มือนี้ได้รับการตรวจสอบโดย Andreas Haas Jakob Kummerow Deepti Gandluri Alon Zakai Francis McCabe François Beaufort และ Rachel Andrew