รวดเร็วเสมอ
ในบทความ ก่อนหน้านี้ เราได้พูดถึงวิธีที่ 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 ไบต์ ขณะที่โค้ดกาวจะเหลือประมาณ 3.5 KB หลังจากการบีบอัด gzip หลังจากลองใช้วิธีต่างๆ อยู่พักหนึ่ง เราก็สามารถเลิกใช้โค้ดกาวและสร้างอินสแตนซ์ของโมดูล WebAssembly ด้วย API เวอร์ชันปกติได้
ซึ่งมักจะเป็นไปได้ด้วย Emscripten ตราบใดที่คุณไม่ได้ใช้อะไรจากไลบรารีมาตรฐาน C
Rust
Rust เป็นภาษาโปรแกรมสมัยใหม่แบบใหม่ที่มาพร้อมกับระบบประเภทที่สมบูรณ์แบบ ไม่มีรันไทม์ และรูปแบบการเป็นเจ้าของที่รับประกันความปลอดภัยของหน่วยความจำและความปลอดภัยของเธรด Rust ยังรองรับ WebAssembly เป็นฟีเจอร์หลักด้วย และทีม Rust ยังได้มีส่วนร่วมในเครื่องมือที่ยอดเยี่ยมมากมายในระบบนิเวศ WebAssembly
เครื่องมืออย่างใดอย่างหนึ่งคือ wasm-pack
โดยกลุ่มทำงาน rustwasm wasm-pack
นำโค้ดของคุณไปเปลี่ยนเป็นโมดูลที่เหมาะกับเว็บซึ่งใช้งานได้ทันทีกับเครื่องมือรวมไฟล์อย่าง 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 พร้อมโค้ดกาวประมาณ 100 ไบต์ (ทั้ง 2 รายการหลังจาก 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-3 อย่างในระบบนิเวศ WebAssembly จะช่วยคุณวิเคราะห์ไฟล์ WebAssembly (ไม่ว่าจะสร้างด้วยภาษาใดก็ตาม) และบอกสิ่งที่เกิดขึ้น รวมถึงช่วยปรับปรุงสถานการณ์ของคุณได้ด้วย
Twiggy
Twiggy เป็นเครื่องมืออีกชิ้นจากทีม WebAssembly ของ Rust ที่ดึงข้อมูลเชิงลึกจำนวนมากจากโมดูล WebAssembly เครื่องมือนี้ไม่ได้มีไว้สำหรับ Rust โดยเฉพาะ และให้คุณตรวจสอบสิ่งต่างๆ เช่น กราฟการเรียกใช้ของโมดูล ระบุส่วนที่ไม่ได้ใช้หรือไม่จำเป็น และดูว่าส่วนใดมีส่วนทำให้ไฟล์ของโมดูลมีขนาดใหญ่ขึ้น ซึ่งทำได้ด้วยคำสั่ง top
ของ Twiggy ดังนี้
$ twiggy top rotate_bg.wasm
ในกรณีนี้ เราพบว่าขนาดไฟล์ส่วนใหญ่มาจากตัวจัดสรร น่าแปลกใจเพราะโค้ดของเราไม่ได้ใช้การจัดสรรแบบไดนามิก ปัจจัยสำคัญอีกประการหนึ่งคือส่วนย่อย "ชื่อฟังก์ชัน"
wasm-strip
wasm-strip
เป็นเครื่องมือจาก WebAssembly Binary Toolkit หรือเรียกสั้นๆ ว่า 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 ขึ้นมาใหม่โดยไม่ใช้บรรณานุกรมมาตรฐานของ 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 ที่เล็กมากและมีประสิทธิภาพสูงได้ทันที แต่หากไม่กล้าที่จะลองใช้โค้ดกาวและลดขนาดให้เหลือเฉพาะสิ่งที่จำเป็น ขนาดโดยรวม (โมดูล WebAssembly + โค้ดกาว) ก็จะค่อนข้างใหญ่
บทสรุป
คุณจึงควรใช้ภาษาใดหากมีเส้นทางที่ใช้งานบ่อยของ 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 ในอนาคตอย่างแน่นอน
อัปเดต: Rust
หลังจากเผยแพร่บทความนี้ Nick Fitzgerald จากทีม Rust ได้ชี้แนะหนังสือ Rust Wasm ที่ยอดเยี่ยมซึ่งมีส่วนเกี่ยวกับการเพิ่มประสิทธิภาพขนาดไฟล์ การทำตามวิธีการในนั้น (โดยเฉพาะอย่างยิ่งการเปิดใช้การเพิ่มประสิทธิภาพเวลาลิงก์และการจัดการข้อผิดพลาดด้วยตนเอง) ทำให้เราสามารถเขียนโค้ด Rust "ปกติ" และกลับไปใช้ Cargo
(npm
ของ Rust) ได้โดยไม่ทำให้ไฟล์มีขนาดใหญ่ขึ้น โมดูล Rust มีขนาด 370B หลังจาก gzip โปรดดูรายละเอียดใน PR ที่ฉันเปิดใน Squoosh
ขอขอบคุณเป็นพิเศษ Ashley Williams, Steve Klabnik, Nick Fitzgerald และ Max Graey ที่ให้ความช่วยเหลือตลอดเส้นทางนี้