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

รวดเร็วอย่างต่อเนื่อง

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

Rust

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

หนึ่งในเครื่องมือเหล่านี้คือ wasm-pack โดยกลุ่มทำงานรัสเซีย 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 ที่มีโค้ดกาวประมาณ 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 ไบต์และโค้ดกาวไม่มีให้กับเรา โมดูลดังกล่าวจะทำงานร่วมกับ API ของ Vanilla WebAssembly ได้

การพิสูจน์หลักฐานของ WebAssembly

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

ทวิตกี

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

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

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

แถบ Wasm

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

$ wasm-strip rotate_bg.wasm

ซึ่งจะลดขนาดไฟล์ของโมดูลสนิมจาก 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 สร้างขึ้น แต่ก็มีขนาดเล็กพอที่จะถือว่ามีขนาดเล็ก

การแสดง

ก่อนที่จะดูข้อสรุปจากขนาดไฟล์เพียงอย่างเดียว เราได้ดำเนินการเพื่อเพิ่มประสิทธิภาพ ไม่ใช่ขนาดไฟล์ แล้วเราวัดประสิทธิภาพอย่างไร และผลลัพธ์ที่ได้คืออะไร

วิธีเปรียบเทียบ

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

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

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

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

กราฟทั้งสองนี้เป็นมุมมองที่แตกต่างกันสำหรับข้อมูลเดียวกัน ในกราฟแรก เราเปรียบเทียบ ตามเบราว์เซอร์แต่ละเบราว์เซอร์ ส่วนกราฟที่ 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.6 KB แม้จะสูงกว่า 2G นั้นใช้เวลาไม่ถึง 1/10 วินาที และหวังว่า Ruust จะช่วยอุดช่องว่างในเรื่องขนาดโมดูลได้ในเร็วๆ นี้

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

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

ข้อมูลอัปเดต: Rust

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

ขอขอบคุณเป็นพิเศษจาก Ashley Williams, Steve Klabnik, Nick Fitzgerald และ Max Graey ที่ให้ความช่วยเหลือในการเดินทางในครั้งนี้