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

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

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

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

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

เส้นทาง Hot Path

เราเขียนฟังก์ชัน JavaScript ใน Squash ซึ่งจะหมุนบัฟเฟอร์รูปภาพหลายองศา 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 ประสิทธิภาพนี้จะเข้าถึงได้ใน "Funnel Path" เท่านั้น แต่การจะอยู่บน "เส้นทางที่รวดเร็ว" นั้นไม่ใช่เรื่องง่าย ประโยชน์สำคัญอย่างหนึ่งที่ ข้อเสนอของ 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 เท่า รูปภาพสำหรับอินพุตและอีกครั้งสำหรับเอาต์พุต) เราสามารถวางรูปภาพอินพุตลงใน หน่วยความจำ 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 แบบไม่มีรันไทม์ และรูปแบบการเป็นเจ้าของที่รับประกันความปลอดภัยของหน่วยความจำและความปลอดภัยของชุดข้อความ สนิม และยังรองรับ 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-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 ภาษา มี เป็นเครื่องมือคู่ในระบบนิเวศ WebAssembly ที่ช่วยคุณวิเคราะห์ ไฟล์ WebAssembly ของคุณ (โดยไม่คำนึงถึงภาษาที่ใช้ในการสร้าง) และ บอกให้ทราบถึงสิ่งที่กำลังเกิดขึ้นและ ช่วยปรับปรุงสถานการณ์ของคุณ

ทวิกกี้

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

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

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

แถบ Wasm

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

กรณีการใช้งานของเราน่าสนใจตรงที่โค้ดสำหรับหมุนภาพจะนำไปใช้ 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.6 KB แม้ผ่าน 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 ที่คอยช่วยเหลือตลอดเส้นทางครั้งนี้