การแทนที่เส้นทาง Hot ใน JavaScript ของแอปด้วย WebAssembly

เร็วสม่ำเสมอ แน่ๆ

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

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

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

เส้นทาง Hot Path

ใน Squash เราเขียนฟังก์ชัน 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;
    }
}

แต่เบราว์เซอร์ 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 คืออะไร

การจัดการหน่วยความจำ

โดยทั่วไป เมื่อคุณใช้หน่วยความจำเพิ่มขึ้น คุณจะต้องจัดการความทรงจำนั้น ความทรงจำส่วนใดที่ใช้งานอยู่ อันไหนที่ไม่เสียค่าใช้จ่าย เช่น ใน C คุณมีฟังก์ชัน malloc(n) ที่ค้นหาพื้นที่หน่วยความจำ n ไบต์ติดต่อกัน ฟังก์ชันประเภทนี้เรียกอีกอย่างว่า "เครื่องจัดสรรเวลา" แน่นอนว่าจะต้องมีการนำตัวจัดสรรที่ใช้งานมาใช้ในโมดูล WebAssembly และจะเพิ่มขนาดไฟล์ของคุณ ขนาดและประสิทธิภาพของฟังก์ชันการจัดการหน่วยความจำเหล่านี้อาจแตกต่างกันอย่างมากตามอัลกอริทึมที่ใช้ ด้วยเหตุนี้หลายๆ ภาษาจึงมีการใช้งานหลายแบบให้เลือก ("dmalloc", "emmalloc", "wee_alloc" ฯลฯ)

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

เลือกไม่ถูก

หากคุณดูฟังก์ชัน JavaScript เดิมที่เราต้องการ WebAssembly-fy คุณจะเห็นได้ว่าโค้ดนั้นเป็นโค้ดสำหรับการคำนวณทั้งหมดโดยไม่มี 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 Code ชื่อ c.js และโมดูล Wasm ชื่อ c.wasm โปรดทราบว่าโมดูล Wasm จะมี Gzip อยู่ที่ประมาณ 260 ไบต์เท่านั้น ในขณะที่โค้ดกาวจะอยู่ที่ประมาณ 3.5 KB หลังจาก Gzip หลังจากที่เคลียร์โค้ดแล้ว เราก็เลิกใช้โค้ดกาวและสร้างอินสแตนซ์ของโมดูล WebAssembly ด้วย Vanilla API ได้ กรณีนี้มักทำได้ด้วย Emscripten ตราบใดที่คุณไม่ได้ใช้สิ่งใดก็ตามจากไลบรารีมาตรฐาน C

Rust

Rust เป็นภาษาโปรแกรมใหม่ที่ทันสมัยซึ่งมีระบบประเภทที่สมบูรณ์ ไม่มีรันไทม์ และมีโมเดลการเป็นเจ้าของที่รับประกันความปลอดภัยของหน่วยความจำและความปลอดภัยของชุดข้อความ นอกจากนี้ Rust ยังรองรับ WebAssembly เป็นฟีเจอร์หลักและทีม Rust ได้มอบเครื่องมือที่ยอดเยี่ยมมากมายให้แก่ระบบนิเวศ WebAssembly

หนึ่งในเครื่องมือเหล่านี้คือ wasm-pack โดยคณะทำงานของ Rustwasm wasm-pack นำโค้ดของคุณมาเปลี่ยนให้เป็นโมดูลที่ใช้งานง่ายบนเว็บซึ่งทำงานได้ทันทีด้วย Bundler เช่น Webpack wasm-pack เป็นประสบการณ์การใช้งานที่สะดวกมาก แต่ปัจจุบันใช้ได้กับ Rust เท่านั้น ทางกลุ่มกำลังพิจารณาที่จะเพิ่มการรองรับภาษาอื่นๆ ที่กำหนดเป้าหมาย WebAssembly

ใน Rust ชิ้นส่วนคืออาร์เรย์ใน C และเช่นเดียวกับใน C เราต้องสร้าง Slice ที่ใช้ที่อยู่เริ่มต้นของเรา ซึ่งจะขัดแย้งกับโมเดลความปลอดภัยของหน่วยความจำที่ 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-to-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 เราเพียงต้องติดตั้งแพ็กเกจ AssemblyScript/assemblyscript npm แล้วเรียกใช้

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript จะให้โมดูล Wasm ขนาดประมาณ 300 ไบต์และไม่มีโค้ด Glue Code มาให้เรา โมดูลนี้ใช้งานได้กับ API ของ Vanilla WebAssembly

การพิสูจน์นิติของ WebAssembly

ขนาด 7.6 KB ของ Rust นั้นใหญ่เกินคาดเมื่อเทียบกับภาษาอื่นๆ อีก 2 ภาษา มีเครื่องมือ 2 อย่างในระบบนิเวศ WebAssembly ที่ช่วยคุณวิเคราะห์ไฟล์ WebAssembly (ไม่ว่าจะใช้ภาษาใดก็ตาม) และบอกคุณถึงสิ่งที่เกิดขึ้น ตลอดจนช่วยคุณปรับปรุงสถานการณ์

ทวิกกี้

Twiggy เป็นอีกเครื่องมือหนึ่งจากทีม WebAssembly ของ Rust ที่แยกข้อมูลเชิงลึกจำนวนมากจากโมดูล WebAssembly เครื่องมือนี้ไม่ได้ใช้สำหรับ Rust โดยเฉพาะและช่วยให้คุณตรวจสอบสิ่งต่างๆ เช่น การเรียกใช้กราฟของโมดูล ระบุส่วนที่ไม่ได้ใช้หรือไม่จำเป็น และดูว่าส่วนใดส่งผลต่อขนาดไฟล์ทั้งหมดของโมดูล ใช้คำสั่ง top ของ Twiggy ดังนี้

$ twiggy top rotate_bg.wasm
ภาพหน้าจอการติดตั้ง Twiggy

ในกรณีนี้ เราจะเห็นว่าขนาดไฟล์ส่วนใหญ่ของเรามาจากตัวจัดสรร ซึ่งน่าประหลาดใจเนื่องจากโค้ดของเราไม่ได้ใช้การจัดสรรแบบไดนามิก ปัจจัยสำคัญอีกประการหนึ่งคือส่วนย่อย "ชื่อฟังก์ชัน"

แถบ Wasm

wasm-strip เป็นเครื่องมือจากชุดเครื่องมือไบนารี WebAssembly หรือเรียกสั้นๆ ว่า wabt โดยจะมีเครื่องมือ 2 อย่างที่ช่วยให้คุณตรวจสอบและจัดการโมดูล WebAssembly wasm2wat เป็นเครื่องมือแยกชิ้นส่วนที่เปลี่ยนโมดูล Wasm แบบไบนารีให้อยู่ในรูปแบบที่มนุษย์อ่านได้ Wabt ยังมี wat2wasm ที่ให้คุณเปลี่ยนรูปแบบที่มนุษย์อ่านได้กลับไปเป็นโมดูล Wabt แบบไบนารี แม้ว่าเราจะใช้เครื่องมือเสริมทั้ง 2 รายการนี้เพื่อตรวจสอบไฟล์ WebAssembly เราก็พบว่า wasm-strip มีประโยชน์มากที่สุด wasm-strip จะนำส่วนและข้อมูลเมตาที่ไม่จำเป็นออกจากโมดูล WebAssembly

$ wasm-strip rotate_bg.wasm

เพื่อลดขนาดไฟล์ของโมดูล Rust จาก 7.5 KB เป็น 6.6 KB (หลังเป็น gzip)

wasm-opt

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

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

การเปรียบเทียบประสิทธิภาพ

การเปรียบเทียบความเร็วต่อภาษา
การเปรียบเทียบความเร็วตามเบราว์เซอร์

กราฟทั้งสองเป็นมุมมองที่ต่างกันสำหรับข้อมูลเดียวกัน ในกราฟแรกเราจะเปรียบเทียบตามเบราว์เซอร์ ในกราฟที่สองเราจะเปรียบเทียบตามภาษาที่ใช้ โปรดทราบว่าผมเลือกสเกลเวลาลอการิทึม สิ่งสำคัญอีกอย่างคือ การเปรียบเทียบทั้งหมดต้องใช้รูปภาพทดสอบ 16 เมกะพิกเซลและเครื่องโฮสต์เดียวกัน ยกเว้นเบราว์เซอร์หนึ่งที่ใช้งานในเครื่องเดียวกันไม่ได้

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

การใช้งาน

อีกเมตริกหนึ่งคือความพยายามอย่างมากในการสร้างและผสานรวมโมดูล WebAssembly ของเราลงในสคูช การกำหนดค่าตัวเลขให้กับความพยายามนั้นเป็นเรื่องยาก ดังนั้นผมจึงไม่สร้างกราฟใดๆ เลย แต่มี 2-3 สิ่งที่ผมอยากชี้แจง ดังนี้

AssemblyScript ทำงานได้อย่างราบรื่น เครื่องมือนี้ไม่เพียงช่วยให้คุณใช้ TypeScript เพื่อเขียน WebAssembly ทำให้การตรวจดูโค้ดเป็นเรื่องง่ายสำหรับเพื่อนร่วมงาน แต่ยังสร้างโมดูล WebAssembly แบบไร้กาวที่มีขนาดเล็กมากและมีประสิทธิภาพสูงด้วย เครื่องมือในระบบนิเวศ TypeScript เช่น Prettier และ Tlint น่าจะทำงานได้

การใช้สนิมร่วมกับ 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 โปรดดูรายละเอียดที่การประชาสัมพันธ์ที่ฉันเปิดใน Squoosh

ขอขอบคุณ Ashley Williams, Steve Klabnik, Nick Fitzgerald และ Max Graey ที่คอยช่วยเหลือตลอดเส้นทางครั้งนี้