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

รวดเร็วเสมอ

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

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

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

เส้นทางยอดนิยม

ใน squoosh เราได้เขียนฟังก์ชัน JavaScript ที่หมุนบัฟเฟอร์รูปภาพเป็นจำนวนหลายเท่าของ 90 องศา ขณะที่ OffscreenCanvas เหมาะสำหรับ ไม่ได้รับการสนับสนุนในทุกเบราว์เซอร์ ที่เรากำหนดเป้าหมาย ข้อบกพร่องใน Chrome

ฟังก์ชันนี้จะวนผ่านพิกเซลทุกพิกเซลของรูปภาพอินพุตและคัดลอกไปยังตําแหน่งอื่นในรูปภาพเอาต์พุตเพื่อให้เกิดการหมุน ขนาด 4094 พิกเซล x รูปภาพ 4096px (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 นำไปใช้งาน คุณจะเห็นได้ว่างานดังกล่าวเป็นการคำนวณ ที่ไม่มี API สำหรับ JavaScript โดยเฉพาะ ดังนั้นชื่อควรจะเป็นแนวตรง เพื่อโอนโค้ดนี้ไปเป็นภาษาใดก็ได้ เราประเมินภาษา 3 ภาษาที่แตกต่างกันซึ่งคอมไพล์เป็น WebAssembly ได้แก่ C/C++, Rust และ AssemblyScript คำถามเดียว ที่เราต้องตอบให้ได้แต่ละภาษาคือ เราจะเข้าถึงหน่วยความจำดิบได้อย่างไร โดยไม่ต้องใช้ฟังก์ชันการจัดการหน่วยความจำ

C และ Emscripten

Emscripten เป็นคอมไพเลอร์ C สำหรับเป้าหมาย WebAssembly เป้าหมายของ Emscripten คือการ ทำหน้าที่เป็นการแทนที่แบบดร็อปอินสำหรับคอมไพเลอร์ C ที่รู้จักกันดี เช่น GCC หรือคำแลง และส่วนใหญ่เข้ากันได้ด้วย สิ่งนี้เป็นส่วนสำคัญในพันธกิจของ Emscripten เนื่องจากจะทำให้การคอมไพล์โค้ด C และ C++ ที่มีอยู่ไปยัง WebAssembly ได้ง่ายเหมือน เท่าที่จะเป็นไปได้

การเข้าถึงหน่วยความจำดิบเป็นลักษณะของ C และตัวชี้ก็อยู่ด้วยเหตุผลนี้

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

ในที่นี้เราจะเปลี่ยนตัวเลข 0x124 เป็นพอยน์เตอร์ไปยังจำนวนเต็ม 8 บิตแบบไม่ลงนาม (หรือไบต์) ซึ่งจะเปลี่ยนตัวแปร ptr เป็นอาร์เรย์โดยเริ่มต้นที่ที่อยู่หน่วยความจำ 0x124 ซึ่งเราสามารถใช้เหมือนกับอาร์เรย์อื่นๆ ได้ ซึ่งจะช่วยให้เราเข้าถึงแต่ละไบต์สำหรับการอ่านและเขียน ในกรณีนี้ เรากําลังดูบัฟเฟอร์ RGBA ของรูปภาพที่ต้องการจัดเรียงใหม่เพื่อให้เกิดการหมุน ในการย้ายพิกเซล เราต้องย้าย 4 ไบต์ติดต่อกันพร้อมกัน (หนึ่งไบต์สำหรับแต่ละแชแนล: 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 จะแปลงเป็น ~260 ไบต์เท่านั้นในขณะที่ Glue Code จะอยู่ที่ประมาณ 3.5KB หลังจาก gzip หลังจากทำมือลุยๆ แล้ว เราก็ปล่อยมือได้ Glue Code และสร้างอินสแตนซ์โมดูล WebAssembly ด้วย Vanilla API ซึ่งมักจะเป็นไปได้ด้วย Emscripten ตราบใดที่คุณไม่ได้ใช้อะไรจากไลบรารีมาตรฐาน C

Rust

Rust เป็นภาษาโปรแกรมสมัยใหม่ที่มีระบบ Rich Type แบบไม่มีรันไทม์ และรูปแบบการเป็นเจ้าของที่รับประกันความปลอดภัยของหน่วยความจำและความปลอดภัยของชุดข้อความ 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-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-3 อย่างในระบบนิเวศ WebAssembly จะช่วยคุณวิเคราะห์ไฟล์ WebAssembly (ไม่ว่าจะสร้างด้วยภาษาใดก็ตาม) และบอกสิ่งที่เกิดขึ้น รวมถึงช่วยปรับปรุงสถานการณ์ของคุณได้ด้วย

ทวิกกี้

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

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

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

wasm-strip

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

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

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

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

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

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

การใช้งาน

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

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

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

C และ Emscripten สร้างโมดูล WebAssembly ที่มีขนาดเล็กมากและมีประสิทธิภาพสูง ให้แกะออกจากกล่องเลย แต่หากไม่มีความกล้าที่จะเข้าไปใน Glue Code และลดเหลือ ไม่มีความจำเป็น ขนาดโดยรวม (โมดูล 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 ในอนาคตอย่างแน่นอน

อัปเดต: สนิม

หลังจากเผยแพร่บทความนี้ Nick Fitzgerald ทีม Rust พาเราไปยังหนังสือ Rust Wasm ที่ยอดเยี่ยมของพวกเขา ส่วนในการเพิ่มประสิทธิภาพของไฟล์ การปฏิบัติตาม คำแนะนำด้วยตนเอง (เห็นได้ชัดเจนที่สุดคือทำให้การเพิ่มประสิทธิภาพเวลาของลิงก์และการด้วยตนเอง การจัดการความตื่นตระหนก) ทำให้เราสามารถเขียนโค้ด Rust "ปกติ" และกลับไปใช้ Cargo (npm ของ Rust) โดยไม่ทำให้ขนาดไฟล์ขยายออก โมดูล Rust มีขนาด 370B หลังจาก gzip โปรดดูรายละเอียดที่การประชาสัมพันธ์ที่ฉันเปิดใน Squoosh

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