เร็วสม่ำเสมอ แน่ๆ
ในบทความ ก่อนหน้านี้ เราได้พูดถึงวิธีที่ WebAssembly ช่วยให้คุณนําระบบนิเวศของไลบรารี C/C++ มาใช้กับเว็บได้ แอปหนึ่งที่ใช้ไลบรารี C/C++ อย่างกว้างขวางคือ squoosh ซึ่งเป็นเว็บแอปที่ช่วยให้คุณบีบอัดรูปภาพด้วยโปรแกรมเปลี่ยนไฟล์ประเภทต่างๆ ที่คอมไพล์จาก C++ เป็น WebAssembly
WebAssembly คือเครื่องเสมือนระดับล่างที่เรียกใช้ไบต์โค้ดที่จัดเก็บไว้ในไฟล์ .wasm
โค้ดไบต์นี้มีการกำหนดประเภทอย่างเข้มงวดและมีโครงสร้างในลักษณะที่คอมไพล์และเพิ่มประสิทธิภาพสำหรับระบบโฮสต์ได้เร็วกว่า JavaScript มาก WebAssembly มีสภาพแวดล้อมในการเรียกใช้โค้ดที่คำนึงถึงแซนด์บ็อกซ์และการฝังตั้งแต่เริ่มต้น
จากประสบการณ์ที่ผ่านมา ปัญหาประสิทธิภาพส่วนใหญ่ในเว็บเกิดจากการบังคับให้วางเลย์เอาต์และการระบายสีมากเกินไป แต่ในบางเวลา แอปจำเป็นต้องทำงานที่มีต้นทุนสูงในการคำนวณ ซึ่งใช้เวลามาก WebAssembly ช่วยคุณได้
เส้นทางยอดนิยม
ใน squoosh เราได้เขียนฟังก์ชัน JavaScript ที่หมุนบัฟเฟอร์รูปภาพเป็นจำนวนหลายเท่าของ 90 องศา แม้ว่า OffscreenCanvas จะเป็นตัวเลือกที่เหมาะสําหรับกรณีนี้ แต่เบราว์เซอร์ที่เรากําลังมุ่งเน้นไม่รองรับ และมีข้อบกพร่องเล็กน้อยใน Chrome
ฟังก์ชันนี้จะวนผ่านพิกเซลทุกพิกเซลของรูปภาพอินพุตและคัดลอกไปยังตําแหน่งอื่นในรูปภาพเอาต์พุตเพื่อให้เกิดการหมุน สำหรับรูปภาพขนาด 4094 x 4096 พิกเซล (16 เมกะพิกเซล) จะต้องทำการวนซ้ำบล็อกโค้ดด้านในมากกว่า 16 ล้านครั้ง ซึ่งเราเรียกว่า "เส้นทางที่ทำงานบ่อย" แม้ว่าจำนวนการวนซ้ำจะค่อนข้างมาก แต่เบราว์เซอร์ 2 ใน 3 ที่เราทดสอบทำงานเสร็จภายใน 2 วินาทีหรือน้อยกว่านั้น ระยะเวลาที่ยอมรับได้สำหรับการโต้ตอบประเภทนี้
for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
outBuffer[i] = inBuffer[in_idx];
i += 1;
}
}
แต่เบราว์เซอร์หนึ่งใช้เวลานานกว่า 8 วินาที วิธีที่เบราว์เซอร์เพิ่มประสิทธิภาพ JavaScript นั้นซับซ้อนมาก และเครื่องมือแต่ละอย่างก็เพิ่มประสิทธิภาพเพื่อสิ่งที่แตกต่างกัน บางรายการเพิ่มประสิทธิภาพเพื่อการดำเนินการแบบดิบ บางรายการเพิ่มประสิทธิภาพเพื่อโต้ตอบกับ DOM ในกรณีนี้ เราพบเส้นทางที่ไม่ได้เพิ่มประสิทธิภาพในเบราว์เซอร์หนึ่ง
ส่วน WebAssembly นั้นสร้างขึ้นเพื่อความเร็วในการดำเนินการขั้นต้นโดยสมบูรณ์ ดังนั้น หากต้องการประสิทธิภาพที่รวดเร็วและคาดการณ์ได้ในเบราว์เซอร์ต่างๆ สำหรับโค้ดเช่นนี้ WebAssembly จะช่วยคุณได้
WebAssembly เพื่อประสิทธิภาพที่คาดการณ์ได้
โดยทั่วไปแล้ว JavaScript และ WebAssembly จะให้ประสิทธิภาพสูงสุดเท่าๆ กัน อย่างไรก็ตาม สำหรับ JavaScript ประสิทธิภาพนี้จะเข้าถึงได้ใน "เส้นทางที่รวดเร็ว" เท่านั้น และการอยู่ใน "เส้นทางที่รวดเร็ว" นั้นทำได้ยาก ประโยชน์หลักอย่างหนึ่งที่ WebAssembly มีให้คือประสิทธิภาพที่คาดการณ์ได้ แม้ในเบราว์เซอร์ต่างๆ การเขียนโปรแกรมแบบเข้มงวดและสถาปัตยกรรมระดับล่างช่วยให้คอมไพเลอร์รับประกันได้มากขึ้น จึงต้องเพิ่มประสิทธิภาพโค้ด WebAssembly เพียงครั้งเดียวและจะใช้ "เส้นทางที่เร็ว" เสมอ
การเขียนสำหรับ WebAssembly
ก่อนหน้านี้เราใช้ไลบรารี C/C++ และคอมไพล์เป็น WebAssembly เพื่อใช้ฟังก์ชันการทำงานบนเว็บ เราไม่ได้แตะต้องโค้ดของไลบรารีมากนัก เพียงแค่เขียนโค้ด C/C++ เพียงเล็กน้อยเพื่อสร้างสะพานเชื่อมระหว่างเบราว์เซอร์กับไลบรารี แต่ครั้งนี้เรามีแรงจูงใจที่แตกต่างกัน เราต้องการเขียนโปรแกรมตั้งแต่ต้นโดยคำนึงถึง WebAssembly เพื่อให้ใช้ประโยชน์จากข้อได้เปรียบของ WebAssembly ได้
สถาปัตยกรรม WebAssembly
เมื่อเขียนสำหรับ WebAssembly คุณควรทำความเข้าใจเพิ่มเติมเกี่ยวกับสิ่งที่ WebAssembly เป็น
อ้างอิงจาก WebAssembly.org
เมื่อคอมไพล์โค้ด C หรือ Rust ไปยัง WebAssembly คุณจะได้รับไฟล์ .wasm
ที่มีการประกาศโมดูล การประกาศนี้ประกอบด้วยรายการ "การนําเข้า" ที่โมดูลคาดหวังจากสภาพแวดล้อม รายการการส่งออกที่โมดูลนี้ทําให้โฮสต์ใช้งานได้ (ฟังก์ชัน ค่าคงที่ ส่วนของหน่วยความจํา) และแน่นอน คำสั่งไบนารีจริงสําหรับฟังก์ชันที่อยู่ในนั้น
สิ่งที่เราไม่ทราบจนกระทั่งได้ตรวจสอบเรื่องนี้คือ กองข้อมูลที่ทําให้ WebAssembly เป็น "เครื่องเสมือนที่อิงตามกองข้อมูล" ไม่ได้จัดเก็บไว้ในส่วนของหน่วยความจําที่โมดูล WebAssembly ใช้ สแต็กนี้อยู่ภายใน VM โดยสมบูรณ์และนักพัฒนาเว็บไม่สามารถเข้าถึงได้ (ยกเว้นผ่านเครื่องมือสำหรับนักพัฒนาเว็บ) ดังนั้นคุณสามารถเขียนโมดูล WebAssembly ที่ไม่ต้องใช้หน่วยความจำเพิ่มเติมเลย และใช้เฉพาะสแต็กภายในของ VM เท่านั้น
ในกรณีของเรา เราจะต้องเพิ่มหน่วยความจำบางส่วนเพื่ออนุญาตให้เข้าถึงพิกเซลของรูปภาพแบบไม่จำกัด และสร้างรูปภาพที่หมุนแล้ว WebAssembly.Memory
มีไว้เพื่อดำเนินการนี้
การจัดการหน่วยความจำ
โดยทั่วไปแล้ว เมื่อใช้หน่วยความจำเพิ่ม คุณจะต้องจัดการหน่วยความจำนั้นด้วยวิธีใดวิธีหนึ่ง หน่วยความจําส่วนใดที่ใช้งานอยู่ รายการใดบ้างที่ให้บริการฟรี
ในตัวอย่างนี้ คุณมีฟังก์ชัน malloc(n)
ใน C ที่ค้นหาพื้นที่หน่วยความจำของไบต์ n
ติดต่อกัน ฟังก์ชันประเภทนี้เรียกอีกอย่างว่า "ตัวจัดสรร"
แน่นอนว่าการใช้งานตัวจัดสรรที่ใช้อยู่ต้องรวมอยู่ในไฟล์ WebAssembly และจะทำให้ไฟล์มีขนาดใหญ่ขึ้น ขนาดและประสิทธิภาพของฟังก์ชันการจัดการหน่วยความจำเหล่านี้อาจแตกต่างกันอย่างมากตามอัลกอริทึมที่ใช้ ด้วยเหตุนี้หลายๆ ภาษาจึงมีการใช้งานหลายแบบให้เลือก ("dmalloc", "emmalloc", "wee_alloc" ฯลฯ)
ในกรณีของเรา เราทราบขนาดของรูปภาพอินพุต (และขนาดของรูปภาพเอาต์พุต) ก่อนที่เราจะเรียกใช้โมดูล WebAssembly เราจึงมองเห็นโอกาสนี้ เดิมทีเราจะส่งบัฟเฟอร์ RGBA ของรูปภาพอินพุตเป็นพารามิเตอร์ไปยังฟังก์ชัน WebAssembly และแสดงผลรูปภาพที่หมุนแล้วเป็นค่าผลลัพธ์ หากต้องการสร้างผลลัพธ์ดังกล่าว เราต้องใช้ตัวจัดสรร แต่เนื่องจากเราทราบจํานวนหน่วยความจําทั้งหมดที่จําเป็น (2 เท่าของขนาดรูปภาพอินพุต 1 รายการสําหรับอินพุตและ 1 รายการสําหรับเอาต์พุต) เราจึงสามารถใส่รูปภาพอินพุตลงในหน่วยความจํา WebAssembly โดยใช้ JavaScript, เรียกใช้โมดูล WebAssembly เพื่อสร้างรูปภาพที่ 2 ซึ่งหมุนแล้ว จากนั้นใช้ JavaScript เพื่ออ่านผลลัพธ์กลับ เราจัดการเรื่องนี้ได้โดยไม่ต้องใช้การจัดการหน่วยความจำเลย
เลือกได้ตามใจชอบ
หากคุณดูฟังก์ชัน JavaScript เดิมที่เราต้องการแปลงเป็น WebAssembly จะเห็นว่าโค้ดนี้ใช้สำหรับการคำนวณล้วนๆ โดยไม่มี API สำหรับ JavaScript โดยเฉพาะ ดังนั้น การพอร์ตโค้ดนี้ไปยังภาษาใดๆ ก็ควรจะทำได้ง่าย เราได้ประเมิน 3 ภาษาที่ต่างกันที่คอมไพล์ไปยัง WebAssembly ได้แก่ C/C++, Rust และ AssemblyScript คำถามเดียวที่เราต้องตอบสำหรับแต่ละภาษาคือ เราจะเข้าถึงหน่วยความจำดิบได้อย่างไรโดยไม่ต้องใช้ฟังก์ชันการจัดการหน่วยความจำ
C และ Emscripten
Emscripten เป็นคอมไพเลอร์ C สําหรับเป้าหมาย WebAssembly เป้าหมายของ Emscripten คือการทำงานแทนคอมไพเลอร์ C ที่รู้จักกันดี เช่น GCC หรือ clang และส่วนใหญ่จะใช้ร่วมกันกับ Flag ได้ นี่เป็นหัวใจสําคัญของภารกิจของ Emscripten เนื่องจากต้องการทําให้การคอมไพล์โค้ด C และ C++ ที่มีอยู่เป็น WebAssembly เป็นเรื่องง่ายที่สุด
การเข้าถึงหน่วยความจำดิบเป็นลักษณะของ C และตัวชี้มีไว้เพื่อเหตุผลดังกล่าว
uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;
ในที่นี้เราจะเปลี่ยนตัวเลข 0x124
เป็นพอยน์เตอร์ไปยังจำนวนเต็ม 8 บิตแบบไม่ลงนาม (หรือไบต์) ซึ่งจะเปลี่ยนตัวแปร ptr
เป็นอาร์เรย์โดยเริ่มต้นที่ที่อยู่หน่วยความจำ 0x124
ซึ่งเราสามารถใช้เหมือนกับอาร์เรย์อื่นๆ ได้ ซึ่งจะช่วยให้เราเข้าถึงแต่ละไบต์สำหรับการอ่านและเขียน ในกรณีนี้ เรากําลังดูบัฟเฟอร์ RGBA ของรูปภาพที่ต้องการจัดเรียงใหม่เพื่อให้เกิดการหมุน หากต้องการย้ายพิกเซล เราจำเป็นต้องย้ายไบต์ 4 ตัวติดต่อกันพร้อมกัน (1 ไบต์สำหรับแต่ละช่อง ได้แก่ R, G, B และ A) เราสามารถสร้างอาร์เรย์ของจำนวนเต็มแบบไม่ลงนาม 32 บิต เพื่อให้ทำได้ง่ายขึ้น ตามธรรมเนียมแล้ว รูปภาพอินพุตจะเริ่มต้นที่ที่อยู่ 4 และรูปภาพเอาต์พุตจะเริ่มต้นทันทีหลังจากรูปภาพอินพุตสิ้นสุด
int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);
for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
outBuffer[i] = inBuffer[in_idx];
i += 1;
}
}
หลังจากพอร์ตฟังก์ชัน JavaScript ทั้งหมดไปยัง C แล้ว เราจะคอมไพล์ไฟล์ C ด้วย emcc
ได้ดังนี้
$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c
เช่นเดียวกับที่เคยเป็นมา emscripten จะสร้างไฟล์โค้ด Glue ชื่อ c.js
และโมดูล wasm ชื่อ c.wasm
โปรดทราบว่าโมดูล WASM จะได้รับการบีบอัด gzip เหลือเพียงประมาณ 260 ไบต์ ขณะที่โค้ด Glue จะเหลือประมาณ 3.5 KB หลังจากการบีบอัด gzip หลังจากลองใช้วิธีต่างๆ อยู่พักหนึ่ง เราก็สามารถเลิกใช้โค้ดกาวและสร้างอินสแตนซ์ของโมดูล WebAssembly ด้วย API เวอร์ชันปกติได้
ซึ่งมักจะเป็นไปได้ด้วย Emscripten ตราบใดที่คุณไม่ได้ใช้อะไรจากไลบรารีมาตรฐาน C
Rust
Rust เป็นภาษาโปรแกรมสมัยใหม่แบบใหม่ที่มาพร้อมกับระบบประเภทที่สมบูรณ์แบบ ไม่มีรันไทม์ และรูปแบบการเป็นเจ้าของที่รับประกันความปลอดภัยของหน่วยความจำและความปลอดภัยของเธรด Rust ยังรองรับ WebAssembly เป็นฟีเจอร์หลักด้วย และทีม Rust ยังได้มีส่วนร่วมในเครื่องมือที่ยอดเยี่ยมมากมายในระบบนิเวศ WebAssembly
เครื่องมือหนึ่งๆ เหล่านี้คือ wasm-pack
โดยกลุ่มทำงาน rustwasm wasm-pack
นำโค้ดของคุณมาเปลี่ยนให้เป็นโมดูลที่ใช้งานง่ายบนเว็บซึ่งทำงานได้ทันทีด้วย Bundler เช่น Webpack wasm-pack
เป็นประสบการณ์การใช้งานที่สะดวกมาก แต่ปัจจุบันใช้ได้กับ Rust เท่านั้น และกำลังพิจารณาที่จะเพิ่มการรองรับภาษาอื่นๆ ที่มุ่งเป้าไปยัง WebAssembly
ใน Rust สไลซ์คือสิ่งที่เทียบเท่ากับอาร์เรย์ใน C และเช่นเดียวกับใน C เราจำเป็นต้องสร้างส่วนที่ใช้ที่อยู่เริ่มต้น ซึ่งขัดต่อรูปแบบความปลอดภัยของหน่วยความจำที่ Rust บังคับใช้ ดังนั้นเราจึงต้องใช้คีย์เวิร์ด unsafe
เพื่อให้เขียนโค้ดที่ไม่เป็นไปตามรูปแบบนั้นได้
let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}
for d2 in 0..d2Limit {
for d1 in 0..d1Limit {
let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
outBuffer[i as usize] = inBuffer[in_idx as usize];
i += 1;
}
}
การคอมไพล์ไฟล์ Rust โดยใช้
$ wasm-pack build
ให้โมดูล Wasm ขนาด 7.6 KB ที่มีโค้ด Glue Code ประมาณ 100 ไบต์ (ทั้งคู่อยู่หลัง gzip)
AssemblyScript
AssemblyScript เป็นโปรเจ็กต์ที่เพิ่งเริ่มต้นขึ้นซึ่งมีเป้าหมายเพื่อเป็นคอมไพเลอร์ TypeScript เป็น WebAssembly อย่างไรก็ตาม โปรดทราบว่าเครื่องมือนี้จะไม่ใช้ TypeScript เพียงอย่างเดียว AssemblyScript ใช้ไวยากรณ์เดียวกับ TypeScript แต่เปลี่ยนไลบรารีมาตรฐานเป็นไลบรารีของตัวเอง ไลบรารีมาตรฐานจะจำลองความสามารถของ WebAssembly ซึ่งหมายความว่าคุณไม่สามารถคอมไพล์ TypeScript ที่มีอยู่เป็น WebAssembly ได้ แต่ไม่ได้หมายความว่าคุณไม่จําเป็นต้องเรียนรู้ภาษาโปรแกรมใหม่เพื่อเขียน WebAssembly
for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
i += 1;
}
}
เมื่อพิจารณาพื้นผิวประเภทเล็กๆ ที่ฟังก์ชัน rotate()
ของเรามี การย้ายโค้ดนี้ไปยัง AssemblyScript ก็ไม่ยากเลย ฟังก์ชัน load<T>(ptr:
usize)
และ store<T>(ptr: usize, value: T)
มาจาก AssemblyScript เพื่อเข้าถึงหน่วยความจําดิบ หากต้องการคอมไพล์ไฟล์ AssemblyScript เราเพียงต้องติดตั้งแพ็กเกจ npm AssemblyScript/assemblyscript
แล้วเรียกใช้
$ asc rotate.ts -b assemblyscript.wasm --validate -O3
AssemblyScript จะให้โมดูล WASM ประมาณ 300 ไบต์และไม่มีโค้ดกาว โมดูลนี้ใช้ได้กับ API ของ WebAssembly เวอร์ชันพื้นฐานเท่านั้น
การตรวจสอบ WebAssembly
ไฟล์ขนาด 7.6 KB ของ Rust นั้นใหญ่อย่างน่าตกใจเมื่อเทียบกับอีก 2 ภาษา มีเครื่องมือ 2 รายการในระบบนิเวศ WebAssembly ที่ช่วยคุณวิเคราะห์ไฟล์ WebAssembly (ไม่ว่าจะใช้ภาษาใดก็ตาม) และบอกคุณถึงสิ่งที่เกิดขึ้น ตลอดจนช่วยคุณปรับปรุงสถานการณ์
Twiggy
Twiggy เป็นเครื่องมืออีกชิ้นจากทีม WebAssembly ของ Rust ที่ดึงข้อมูลเชิงลึกจำนวนมากจากโมดูล WebAssembly เครื่องมือนี้ไม่ได้มีไว้สำหรับ Rust โดยเฉพาะ และให้คุณตรวจสอบสิ่งต่างๆ เช่น กราฟการเรียกใช้ของโมดูล ระบุส่วนที่ไม่ได้ใช้หรือไม่จำเป็น และดูว่าส่วนใดมีส่วนทำให้ไฟล์ของโมดูลมีขนาดใหญ่ขึ้น ซึ่งทำได้ด้วยคําสั่ง top
ของ Twiggy ดังนี้
$ twiggy top rotate_bg.wasm
ในกรณีนี้ เราพบว่าขนาดไฟล์ส่วนใหญ่มาจากตัวจัดสรร น่าแปลกใจเพราะโค้ดของเราไม่ได้ใช้การจัดสรรแบบไดนามิก ปัจจัยสำคัญอีกประการหนึ่งคือส่วนย่อย "ชื่อฟังก์ชัน"
wasm-strip
wasm-strip
เป็นเครื่องมือจากชุดเครื่องมือไบนารี WebAssembly หรือเรียกสั้นๆ ว่า wabt โดยจะมีเครื่องมือ 2-3 รายการที่ช่วยให้คุณตรวจสอบและจัดการโมดูล WebAssembly ได้
wasm2wat
เป็นเครื่องมือแยกชิ้นส่วนที่เปลี่ยนโมดูล Wasm แบบไบนารีให้อยู่ในรูปแบบที่มนุษย์อ่านได้ Wabt ยังมี wat2wasm
ซึ่งช่วยให้คุณเปลี่ยนรูปแบบที่มนุษย์อ่านได้กลับเป็นโมดูล Wasm แบบไบนารีได้ แม้ว่าเราจะใช้เครื่องมือเสริมทั้ง 2 อย่างนี้เพื่อตรวจสอบไฟล์ WebAssembly เราก็พบว่า wasm-strip
มีประโยชน์มากที่สุด wasm-strip
นำส่วนที่ไม่จำเป็นและข้อมูลเมตาออกจากโมดูล WebAssembly
$ wasm-strip rotate_bg.wasm
ซึ่งจะลดขนาดไฟล์ของโมดูล Rust จาก 7.5 KB เป็น 6.6 KB (หลังจาก gzip)
wasm-opt
wasm-opt
เป็นเครื่องมือจาก Binaryen
โดยจะใช้โมดูล WebAssembly และพยายามเพิ่มประสิทธิภาพทั้งขนาดและประสิทธิภาพโดยอิงตามไบต์โค้ดเท่านั้น เครื่องมือบางรายการ เช่น Emscripten ใช้งานเครื่องมือนี้ได้อยู่แล้ว แต่เครื่องมืออื่นๆ บางรายการไม่รองรับ คุณควรลองประหยัดไบต์เพิ่มเติมโดยใช้เครื่องมือเหล่านี้
wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm
เมื่อใช้ wasm-opt
เราสามารถลดจำนวนไบต์ได้อีกเล็กน้อยเพื่อให้เหลือเพียง 6.2 KB หลังจาก gzip
#![no_std]
หลังจากการให้คำปรึกษาและการวิจัยแล้ว เราเขียน Rust Code ใหม่โดยไม่ใช้ไลบรารีมาตรฐานของ Rust โดยใช้ฟีเจอร์ #![no_std]
ซึ่งจะปิดใช้การจัดสรรหน่วยความจําแบบไดนามิกโดยสิ้นเชิงด้วย โดยจะนําโค้ดตัวจัดสรรออกจากโมดูลของเรา กำลังคอมไพล์ไฟล์ Rust นี้
ด้วย
$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs
ทำให้เกิดโมดูล wasm ขนาด 1.6 KB หลังจาก wasm-opt
, wasm-strip
และ gzip แม้ว่าจะยังคงมีขนาดใหญ่กว่าโมดูลที่ C และ AssemblyScript สร้างขึ้น แต่ก็ถือว่ามีขนาดเล็ก
ประสิทธิภาพ
ก่อนจะสรุปจากขนาดไฟล์เพียงอย่างเดียว เราทํางานนี้เพื่อเพิ่มประสิทธิภาพ ไม่ใช่เพื่อลดขนาดไฟล์ เราวัดประสิทธิภาพอย่างไรและได้ผลลัพธ์อย่างไร
วิธีเปรียบเทียบ
แม้ว่า WebAssembly จะเป็นรูปแบบไบต์โค้ดระดับต่ำ แต่ก็ยังคงต้องส่งผ่านคอมไพเลอร์เพื่อสร้างโค้ดเครื่องสำหรับโฮสต์โดยเฉพาะ คอมไพเลอร์จะทำงานในหลายขั้นตอนเช่นเดียวกับ JavaScript กล่าวอย่างง่ายคือ ระยะแรกจะคอมไพล์ได้เร็วกว่ามาก แต่มักจะสร้างโค้ดที่ช้ากว่า เมื่อโมดูลเริ่มทำงาน เบราว์เซอร์จะสังเกตว่าส่วนใดมีการใช้งานบ่อย และส่งผ่านคอมไพเลอร์ที่เพิ่มประสิทธิภาพมากขึ้นแต่ทำงานช้าลง
Use Case ของเราน่าสนใจตรงที่จะใช้โค้ดหมุนภาพเพียงครั้งเดียวหรือ 2 ครั้ง ดังนั้นในกรณีส่วนใหญ่ เราจะไม่ได้รับประโยชน์จากคอมไพเลอร์แบบเพิ่มประสิทธิภาพ โปรดคำนึงถึงเรื่องนี้เมื่อทำการเปรียบเทียบ การเรียกใช้โมดูล WebAssembly 10,000 ครั้งในลูปจะให้ผลลัพธ์ที่ไม่สมจริง หากต้องการตัวเลขที่สมจริง เราควรเรียกใช้โมดูลเพียงครั้งเดียวและตัดสินใจตามตัวเลขจากการเรียกใช้ครั้งเดียวนั้น
การเปรียบเทียบประสิทธิภาพ
กราฟ 2 รูปแบบนี้เป็นมุมมองที่แตกต่างกันของข้อมูลเดียวกัน ในกราฟแรก เราจะเปรียบเทียบตามเบราว์เซอร์ ส่วนในกราฟที่ 2 เราจะเปรียบเทียบตามภาษาที่ใช้ โปรดทราบว่าเราเลือกรูปแบบเวลาแบบเชิงลําดับเลขฐานสิบ นอกจากนี้ สิ่งสำคัญคือการทดสอบประสิทธิภาพทั้งหมดใช้รูปภาพทดสอบ 16 ล้านพิกเซลเดียวกันและเครื่องโฮสต์เดียวกัน ยกเว้นเบราว์เซอร์ 1 ตัวที่ไม่สามารถทำงานบนเครื่องเดียวกันได้
โดยไม่ต้องวิเคราะห์กราฟเหล่านี้มากนัก เห็นได้ชัดว่าเราแก้ปัญหาด้านประสิทธิภาพเดิมได้แล้ว นั่นคือโมดูล WebAssembly ทั้งหมดทำงานได้ภายในเวลาประมาณ 500 มิลลิวินาทีหรือน้อยกว่า ข้อมูลนี้ยืนยันสิ่งที่เราได้กล่าวไว้ตั้งแต่ต้นว่า WebAssembly ให้ประสิทธิภาพที่คาดการณ์ได้ ไม่ว่าจะเลือกภาษาใด ความแตกต่างระหว่างเบราว์เซอร์และภาษาต่างๆ นั้นน้อยมาก กล่าวให้ชัดเจนก็คือ ค่าเบี่ยงเบนมาตรฐานของ JavaScript ในทุกเบราว์เซอร์คือประมาณ 400 มิลลิวินาที ในขณะที่ค่าเบี่ยงเบนมาตรฐานของโมดูล WebAssembly ทั้งหมดในทุกเบราว์เซอร์คือประมาณ 80 มิลลิวินาที
การใช้งาน
อีกเมตริกหนึ่งคือความพยายามที่เราต้องใช้ในการสร้างและผสานรวมข้อบังคับ WebAssembly เข้ากับ squoosh การกําหนดค่าตัวเลขให้กับความพยายามนั้นทําได้ยาก เราจึงจะไม่สร้างกราฟใดๆ แต่มีบางสิ่งที่เราอยากจะชี้ให้เห็น
AssemblyScript ทำงานได้อย่างราบรื่น เครื่องมือนี้ไม่เพียงช่วยให้คุณใช้ TypeScript เพื่อเขียน WebAssembly ทำให้การตรวจดูโค้ดเป็นเรื่องง่ายสำหรับเพื่อนร่วมงาน แต่ยังสร้างโมดูล WebAssembly แบบไร้กาวที่มีขนาดเล็กมากและมีประสิทธิภาพดีอีกด้วย เครื่องมือในระบบนิเวศ TypeScript เช่น prettier และ tslint น่าจะใช้งานได้
Rust ร่วมกับ wasm-pack
ยังสะดวกมากเช่นกัน แต่เหมาะสําหรับโปรเจ็กต์ WebAssembly ขนาดใหญ่ที่ต้องใช้การเชื่อมโยงและการจัดการหน่วยความจํามากกว่า เราต้องหันออกจากเส้นทางความสุขเล็กน้อยเพื่อให้ได้ขนาดไฟล์ที่แข่งขันได้
C และ Emscripten สร้างโมดูล WebAssembly ขนาดเล็กและมีประสิทธิภาพสูงที่พร้อมใช้งานทันที แต่ไม่มีความกล้าหาญที่จะกระโดดใช้ Glue Code และลดความจำเป็นเพียงอย่างเดียว ขนาดโดยรวม (โมดูล WebAssembly + Glue Code) ก็มีขนาดใหญ่มาก
บทสรุป
คุณควรใช้ภาษาใดหากมีเส้นทางยอดนิยมของ JS และต้องการให้ทำงานเร็วขึ้นหรือสอดคล้องกับ WebAssembly มากขึ้น คําตอบสําหรับคําถามด้านประสิทธิภาพก็ยังคงเหมือนเดิมคือ "ขึ้นอยู่กับ" เราจัดส่งอะไรไปให้
เมื่อเปรียบเทียบขนาดโมดูล / ประสิทธิภาพที่เสียไปของภาษาต่างๆ ที่เราใช้ ดูเหมือนว่า C หรือ AssemblyScript จะเป็นตัวเลือกที่ดีที่สุด เราตัดสินใจที่จะเปิดตัว Rust การตัดสินใจนี้เกิดขึ้นจากหลายสาเหตุ โค้ดทั้งหมดที่มาพร้อมกับ Squoosh จนถึงตอนนี้ได้รับการคอมไพล์โดยใช้ Emscripten เราต้องการขยายความรู้เกี่ยวกับระบบนิเวศ WebAssembly และใช้ภาษาอื่นในเวอร์ชันที่ใช้งานจริง AssemblyScript เป็นทางเลือกที่มีประสิทธิภาพ แต่โปรเจ็กต์นี้ยังค่อนข้างใหม่และคอมไพเลอร์ยังไม่สมบูรณ์เท่าคอมไพเลอร์ Rust
แม้ว่าความแตกต่างของขนาดไฟล์ระหว่าง Rust กับภาษาอื่นๆ จะดูค่อนข้างมากในผังกระจาย แต่ก็ไม่ได้เป็นปัญหาใหญ่มากนักในความเป็นจริง การโหลด 500B หรือ 1.6KB หรือแม้กระทั่งมากกว่า 2G ใช้เวลาไม่ถึง 1/10 วินาที และ Rust น่าจะช่วยอุดช่องว่างในส่วนของขนาดโมดูลได้ในเร็วๆ นี้
ในแง่ประสิทธิภาพรันไทม์ Rust มีความเร็วโดยเฉลี่ยในเบราว์เซอร์ต่างๆ เร็วกว่า AssemblyScript โดยเฉพาะอย่างยิ่งในโปรเจ็กต์ขนาดใหญ่ Rust มีแนวโน้มที่จะสร้างโค้ดที่เร็วขึ้นโดยไม่ต้องเพิ่มประสิทธิภาพโค้ดด้วยตนเอง แต่นั่นก็ไม่ได้หมายความว่าคุณจะใช้สิ่งที่คุณสะดวกที่สุดไม่ได้
อย่างไรก็ตาม AssemblyScript เป็นเครื่องมือที่ยอดเยี่ยม ซึ่งช่วยให้นักพัฒนาเว็บสร้างโมดูล WebAssembly ได้โดยไม่ต้องเรียนรู้ภาษาใหม่ ทีม AssemblyScript ตอบกลับอย่างรวดเร็วและพยายามปรับปรุงเครื่องมืออย่างสม่ำเสมอ เราจะติดตาม AssemblyScript ในอนาคตอย่างแน่นอน
อัปเดต: สนิม
หลังจากเผยแพร่บทความนี้ Nick Fitzgerald จากทีม Rust ได้ชี้แนะหนังสือ Rust Wasm ที่ยอดเยี่ยมซึ่งมีส่วนเกี่ยวกับการเพิ่มประสิทธิภาพขนาดไฟล์ การทำตามวิธีการในเว็บไซต์นั้น (สิ่งที่เห็นได้ชัดเจนที่สุดคือเปิดใช้การเพิ่มประสิทธิภาพเวลาในการลิงก์และการจัดการด้วยตนเอง) ช่วยให้เราเขียน Rust Code แบบ "ปกติ" และกลับไปใช้ Cargo
(npm
ของ Rust) ได้โดยที่ขนาดไฟล์ไม่มากเกินไป โมดูล Rust มีขนาด 370B หลังจาก gzip โปรดดูรายละเอียดใน PR ที่ฉันเปิดใน Squoosh
ขอขอบคุณเป็นพิเศษ Ashley Williams, Steve Klabnik, Nick Fitzgerald และ Max Graey ที่ให้ความช่วยเหลือตลอดเส้นทางนี้