جایگزین کردن یک مسیر داغ در جاوا اسکریپت برنامه خود با WebAssembly

به طور مداوم سریع است، بله

در مقاله‌های قبلی‌ام درباره اینکه چگونه WebAssembly به شما اجازه می‌دهد اکوسیستم کتابخانه C/C++ را به وب بیاورید صحبت کردم. یکی از برنامه‌هایی که از کتابخانه‌های C/C++ استفاده زیادی می‌کند squoosh است، برنامه وب ما که به شما امکان می‌دهد تصاویر را با انواع کدک‌هایی که از C++ به WebAssembly کامپایل شده‌اند، فشرده کنید.

WebAssembly یک ماشین مجازی سطح پایین است که بایت کد ذخیره شده در فایل های .wasm را اجرا می کند. این کد بایتی به شدت تایپ و ساختار یافته است به گونه ای که می توان آن را برای سیستم میزبان بسیار سریعتر از جاوا اسکریپت کامپایل و بهینه کرد. WebAssembly محیطی را برای اجرای کدهایی فراهم می کند که از همان ابتدا سندباکس و جاسازی را در ذهن داشتند.

در تجربه من، اکثر مشکلات عملکرد در وب به دلیل چیدمان اجباری و رنگ بیش از حد ایجاد می شود، اما هر از چند گاهی یک برنامه باید یک کار محاسباتی پرهزینه را انجام دهد که زمان زیادی می برد. WebAssembly می تواند در اینجا کمک کند.

مسیر داغ

در squoosh ما یک تابع جاوا اسکریپت نوشتیم که بافر تصویر را مضربی 90 درجه می چرخاند. اگرچه OffscreenCanvas برای این کار ایده‌آل خواهد بود، اما در مرورگرهایی که ما هدف آن بودیم پشتیبانی نمی‌شود و در کروم کمی باگ است.

این تابع روی هر پیکسل از یک تصویر ورودی تکرار می‌شود و آن را در موقعیت دیگری در تصویر خروجی کپی می‌کند تا به چرخش برسد. برای یک تصویر 4094 در 4096 پیکسل (16 مگاپیکسل) به بیش از 16 میلیون تکرار از بلوک کد داخلی نیاز دارد، که ما آن را "مسیر داغ" می نامیم. با وجود این تعداد نسبتاً زیاد تکرار، دو مرورگر از هر سه مرورگر مورد آزمایش ما کار را در 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 ثانیه طول می کشد. روشی که مرورگرها جاوا اسکریپت را بهینه می کنند واقعاً پیچیده است و موتورهای مختلف برای موارد مختلف بهینه سازی می کنند. برخی برای اجرای خام، برخی برای تعامل با DOM بهینه سازی می کنند. در این مورد، ما یک مسیر بهینه نشده را در یک مرورگر زده ایم.

از طرف دیگر WebAssembly کاملاً بر اساس سرعت اجرای خام ساخته شده است. بنابراین اگر عملکرد سریع و قابل پیش‌بینی در مرورگرها برای کدهایی مانند این می‌خواهیم، ​​WebAssembly می‌تواند کمک کند.

WebAssembly برای عملکرد قابل پیش بینی

به طور کلی، جاوا اسکریپت و WebAssembly می توانند به حداکثر عملکرد یکسانی دست یابند. با این حال، برای جاوا اسکریپت، این عملکرد فقط در "مسیر سریع" قابل دستیابی است و اغلب ماندن در آن "مسیر سریع" دشوار است. یکی از مزایای کلیدی که WebAssembly ارائه می دهد عملکرد قابل پیش بینی است، حتی در بین مرورگرها. تایپ دقیق و معماری سطح پایین به کامپایلر این امکان را می دهد که تضمین های قوی تری ایجاد کند به طوری که کد WebAssembly فقط یک بار باید بهینه شود و همیشه از "مسیر سریع" استفاده می کند.

نوشتن برای WebAssembly

قبلاً کتابخانه‌های C/C++ را گرفتیم و آنها را در WebAssembly کامپایل کردیم تا از عملکرد آنها در وب استفاده کنیم. ما واقعاً به کد کتابخانه‌ها دست نزدیم، فقط مقادیر کمی کد C/C++ نوشتیم تا پل بین مرورگر و کتابخانه ایجاد شود. این بار انگیزه ما متفاوت است: می خواهیم چیزی را از ابتدا با در نظر گرفتن WebAssembly بنویسیم تا بتوانیم از مزایای WebAssembly استفاده کنیم.

معماری WebAssembly

هنگامی که برای WebAssembly می نویسید، بهتر است کمی بیشتر در مورد اینکه WebAssembly واقعاً چیست بدانید.

برای نقل قول از WebAssembly.org :

هنگامی که یک قطعه از کد C یا Rust را در WebAssembly کامپایل می کنید، یک فایل .wasm دریافت می کنید که حاوی یک اعلان ماژول است. این اعلان شامل لیستی از "واردات" است که ماژول از محیط خود انتظار دارد، لیستی از صادراتی که این ماژول در اختیار میزبان قرار می دهد (توابع، ثابت ها، تکه های حافظه) و البته دستورالعمل های باینری واقعی برای توابع موجود در داخل .

چیزی که من تا زمانی که به این موضوع نگاه نکردم متوجه نشدم: پشته ای که WebAssembly را به یک "ماشین مجازی مبتنی بر پشته" تبدیل می کند در حافظه ای که ماژول های WebAssembly استفاده می کنند ذخیره نمی شود. پشته کاملاً VM داخلی است و برای توسعه دهندگان وب غیرقابل دسترسی است (به جز از طریق DevTools). به این ترتیب می توان ماژول های WebAssembly را نوشت که اصلاً به حافظه اضافی نیاز ندارند و فقط از پشته داخلی VM استفاده می کنند.

در مورد ما باید از مقداری حافظه اضافی استفاده کنیم تا اجازه دسترسی دلخواه به پیکسل های تصویرمان را بدهیم و یک نسخه چرخشی از آن تصویر تولید کنیم. این همان چیزی است که WebAssembly.Memory برای آن است.

مدیریت حافظه

معمولاً، هنگامی که از حافظه اضافی استفاده می کنید، نیاز به مدیریت آن حافظه را خواهید یافت. کدام بخش از حافظه در حال استفاده است؟ کدام یک رایگان هستند؟ به عنوان مثال، در C، تابع malloc(n) دارید که فضای حافظه n بایت متوالی را پیدا می کند. توابع از این نوع "تخصیص دهنده" نیز نامیده می شوند. البته اجرای تخصیص دهنده در حال استفاده باید در ماژول WebAssembly شما گنجانده شود و حجم فایل شما را افزایش دهد. این اندازه و عملکرد این توابع مدیریت حافظه می تواند بسته به الگوریتم مورد استفاده کاملاً متفاوت باشد، به همین دلیل است که بسیاری از زبان ها پیاده سازی های متعددی را برای انتخاب ارائه می دهند ("dmalloc"، "emmalloc"، "wee_alloc"، و غیره).

در مورد ما، قبل از اجرای ماژول WebAssembly، ابعاد تصویر ورودی (و در نتیجه ابعاد تصویر خروجی) را می دانیم. در اینجا ما یک فرصت را دیدیم: به طور سنتی، بافر RGBA تصویر ورودی را به عنوان یک پارامتر به یک تابع WebAssembly می‌فرستیم و تصویر چرخانده شده را به عنوان مقدار بازگشتی برمی‌گردانیم. برای تولید آن مقدار بازگشتی، باید از تخصیص دهنده استفاده کنیم. اما از آنجایی که ما مقدار کل حافظه مورد نیاز را می دانیم (دو برابر اندازه تصویر ورودی، یک بار برای ورودی و یک بار برای خروجی)، می توانیم تصویر ورودی را با استفاده از جاوا اسکریپت در حافظه WebAssembly قرار دهیم، ماژول WebAssembly را اجرا کنیم تا دومی تولید شود. تصویر را چرخانده و سپس از جاوا اسکریپت برای بازخوانی نتیجه استفاده کنید. ما می توانیم بدون استفاده از هیچ گونه مدیریت حافظه دور شویم!

برای انتخاب خراب شده است

اگر به تابع اصلی جاوا اسکریپت که می‌خواهیم WebAssembly-fy کنیم نگاه کنید، می‌بینید که یک کد محاسباتی محاسباتی بدون APIهای خاص جاوا اسکریپت است. بنابراین، انتقال این کد به هر زبانی باید نسبتاً ساده باشد. ما 3 زبان مختلف را که به WebAssembly کامپایل می‌شوند، ارزیابی کردیم: C/C++، Rust و AssemblyScript. تنها سوالی که برای هر یک از زبان ها باید پاسخ دهیم این است: چگونه بدون استفاده از توابع مدیریت حافظه به حافظه خام دسترسی پیدا کنیم؟

C و Emscripten

Emscripten یک کامپایلر C برای هدف WebAssembly است. هدف Emscripten این است که به عنوان جایگزینی برای کامپایلرهای معروف C مانند GCC یا clang عمل کند و عمدتاً با پرچم سازگار است. این بخش اصلی ماموریت Emscripten است زیرا می‌خواهد کامپایل کدهای C و C++ موجود در WebAssembly را تا حد امکان آسان کند.

دسترسی به حافظه خام در ماهیت C است و اشاره گرها به همین دلیل وجود دارند:

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

در اینجا ما عدد 0x124 را به یک اشاره گر به اعداد صحیح 8 بیتی (یا بایت) بدون علامت تبدیل می کنیم. این به طور موثر متغیر ptr را به آرایه‌ای تبدیل می‌کند که از آدرس حافظه 0x124 شروع می‌شود، که می‌توانیم مانند هر آرایه دیگری از آن استفاده کنیم و به ما امکان می‌دهد به بایت‌های جداگانه برای خواندن و نوشتن دسترسی داشته باشیم. در مورد ما، ما به یک بافر RGBA از یک تصویر نگاه می کنیم که می خواهیم برای دستیابی به چرخش دوباره آن را مرتب کنیم. برای جابجایی یک پیکسل در واقع باید 4 بایت متوالی را به طور همزمان جابه جا کنیم (یک بایت برای هر کانال: R، G، B و A). برای آسان‌تر کردن این کار، می‌توانیم آرایه‌ای از اعداد صحیح ۳۲ بیتی بدون علامت ایجاد کنیم. طبق قرارداد، تصویر ورودی ما از آدرس 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;
    }
}

پس از انتقال کل تابع جاوا اسکریپت به C، می توانیم فایل C را با emcc کامپایل کنیم:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

مثل همیشه، emscripten یک فایل کد چسب به نام c.js و یک ماژول wasm به نام c.wasm تولید می کند. توجه داشته باشید که ماژول wam gzip فقط به 260 بایت می رسد، در حالی که کد چسب پس از gzip حدود 3.5 کیلوبایت است. پس از کمی تکان دادن، ما توانستیم کد چسب را حذف کنیم و ماژول های WebAssembly را با API های وانیلی نمونه سازی کنیم. این اغلب با Emscripten امکان پذیر است تا زمانی که از چیزی از کتابخانه استاندارد C استفاده نکنید.

زنگ

Rust یک زبان برنامه نویسی جدید و مدرن با سیستم نوع غنی، بدون زمان اجرا و مدل مالکیت است که ایمنی حافظه و thread-safety را تضمین می کند. Rust همچنین از WebAssembly به عنوان یک ویژگی اصلی پشتیبانی می کند و تیم Rust ابزارهای بسیار خوبی را به اکوسیستم WebAssembly ارائه کرده است.

یکی از این ابزارها wasm-pack توسط کارگروه rustwasm است. wasm-pack کد شما را می گیرد و آن را به یک ماژول وب پسند تبدیل می کند که با بسته هایی مانند وب پک به صورت خارج از جعبه کار می کند. 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 کیلوبایتی با حدود 100 بایت کد چسب (هر دو بعد از gzip) ایجاد می کند.

AssemblyScript

AssemblyScript یک پروژه نسبتاً جوان است که هدف آن کامپایلر TypeScript-to-WebAssembly است. مهم است که توجه داشته باشید، با این حال، آن را صرفا هیچ نوع TypeScript مصرف نمی کند. AssemblyScript از همان نحوی استفاده می کند که TypeScript است، اما کتابخانه استاندارد را برای خود تغییر می دهد. کتابخانه استاندارد آنها قابلیت های WebAssembly را مدل می کند. این بدان معناست که شما نمی توانید هر نوع تایپ اسکریپتی را که در اطراف خود دارید در 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 Bytes و بدون کد چسب در اختیار ما قرار می دهد. ماژول فقط با API های وانیلی WebAssembly کار می کند.

WebAssembly Forensics

7.6 کیلوبایت Rust در مقایسه با 2 زبان دیگر به طرز شگفت انگیزی بزرگ است. چند ابزار در اکوسیستم WebAssembly وجود دارد که می تواند به شما کمک کند فایل های WebAssembly خود را تجزیه و تحلیل کنید (صرف نظر از زبانی که با آن ساخته شده است) و به شما بگوید که چه اتفاقی می افتد و همچنین به شما کمک می کند وضعیت خود را بهبود بخشید.

توئیگی

Twiggy ابزار دیگری از تیم Rust's WebAssembly است که مجموعه ای از داده های روشنگر را از یک ماژول WebAssembly استخراج می کند. این ابزار مخصوص Rust نیست و به شما امکان می‌دهد مواردی مانند نمودار فراخوانی ماژول را بررسی کنید، بخش‌های استفاده نشده یا اضافی را تعیین کنید و بفهمید کدام بخش‌ها در اندازه کل فایل ماژول شما نقش دارند. دومی را می توان با دستور top Twiggy انجام داد:

$ twiggy top rotate_bg.wasm
اسکرین شات نصب Twiggy

در این مورد می‌توانیم ببینیم که اکثر حجم فایل ما از تخصیص‌دهنده نشات می‌گیرد. این تعجب آور بود زیرا کد ما از تخصیص پویا استفاده نمی کند. یکی دیگر از عوامل موثر در فرعی "نام توابع" است.

wasm-strip

wasm-strip ابزاری از WebAssembly Binary Toolkit یا به اختصار wabt است. این شامل چند ابزار است که به شما امکان می دهد ماژول های WebAssembly را بازرسی و دستکاری کنید. wasm2wat جداسازی‌کننده‌ای است که یک ماژول wasm باینری را به یک قالب قابل خواندن برای انسان تبدیل می‌کند. Wabt همچنین حاوی wat2wasm است که به شما امکان می دهد آن قالب قابل خواندن توسط انسان را به یک ماژول wasm باینری تبدیل کنید. در حالی که ما از این دو ابزار مکمل برای بازرسی فایل های WebAssembly خود استفاده کردیم، متوجه شدیم که wasm-strip مفیدترین است. wasm-strip بخش‌ها و ابرداده‌های غیرضروری را از ماژول WebAssembly حذف می‌کند:

$ wasm-strip rotate_bg.wasm

این باعث کاهش حجم فایل ماژول rust از 7.5 کیلوبایت به 6.6 کیلوبایت (بعد از gzip) می شود.

wasm-opt

wasm-opt ابزاری از Binaryen است. این ماژول WebAssembly را می گیرد و سعی می کند آن را هم از نظر اندازه و هم از نظر عملکرد فقط بر اساس بایت کد بهینه کند. برخی از ابزارها مانند Emscripten قبلاً این ابزار را اجرا می کنند، برخی دیگر نه. معمولاً ایده خوبی است که سعی کنید با استفاده از این ابزارها مقداری بایت اضافی ذخیره کنید.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

با wasm-opt می‌توانیم تعداد انگشت‌شماری بایت دیگر را حذف کنیم تا در مجموع ۶.۲ کیلوبایت پس از gzip باقی بماند.

#![no_std]

پس از کمی مشورت و تحقیق، کد Rust خود را بدون استفاده از کتابخانه استاندارد Rust با استفاده از ویژگی #![no_std] دوباره نوشتیم. این همچنین تخصیص حافظه پویا را به طور کلی غیرفعال می کند و کد تخصیص دهنده را از ماژول ما حذف می کند. کامپایل این فایل Rust با

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

پس از wasm-opt ، wasm-strip و gzip یک ماژول wasm 1.6KB تولید کرد. در حالی که هنوز بزرگتر از ماژول های تولید شده توسط C و AssemblyScript است، به اندازه ای کوچک است که بتوان آن را سبک وزن دانست.

کارایی

قبل از اینکه ما فقط بر اساس اندازه فایل نتیجه گیری کنیم - ما این سفر را برای بهینه سازی عملکرد و نه اندازه فایل انجام دادیم. بنابراین چگونه عملکرد را اندازه گیری کردیم و چه نتایجی داشتیم؟

نحوه محک زدن

علیرغم اینکه WebAssembly یک قالب بایت کد سطح پایین است، اما همچنان باید از طریق یک کامپایلر ارسال شود تا کد ماشین مخصوص میزبان تولید شود. درست مانند جاوا اسکریپت، کامپایلر در چند مرحله کار می کند. به سادگی گفت: مرحله اول در کامپایل بسیار سریعتر است اما تمایل به تولید کد کندتر دارد. هنگامی که ماژول شروع به اجرا می کند، مرورگر مشاهده می کند که کدام قسمت ها اغلب استفاده می شوند و آنها را از طریق یک کامپایلر بهینه تر اما کندتر ارسال می کند.

مورد استفاده ما از این جهت جالب است که کد چرخاندن یک تصویر یک بار، شاید دو بار استفاده می شود. بنابراین در اکثریت قریب به اتفاق موارد ما هرگز از مزایای کامپایلر بهینه سازی بهره نخواهیم برد. این مهم است که در هنگام بنچمارک به خاطر داشته باشید. اجرای ماژول های WebAssembly ما 10000 بار در یک حلقه نتایج غیر واقعی می دهد. برای به دست آوردن اعداد واقعی، باید ماژول را یک بار اجرا کنیم و بر اساس اعداد از آن اجرا تصمیم گیری کنیم.

مقایسه عملکرد

مقایسه سرعت در هر زبان
مقایسه سرعت در هر مرورگر

این دو نمودار نماهای متفاوتی از یک داده هستند. در نمودار اول به ازای هر مرورگر مقایسه می کنیم، در نمودار دوم به ازای هر زبان استفاده شده مقایسه می کنیم. لطفاً توجه داشته باشید که من مقیاس زمانی لگاریتمی را انتخاب کردم. همچنین مهم است که همه بنچمارک‌ها از یک تصویر آزمایشی 16 مگاپیکسلی و یک دستگاه میزبان استفاده می‌کنند، به جز یک مرورگر که نمی‌توان آن را روی یک دستگاه اجرا کرد.

بدون تجزیه و تحلیل بیش از حد این نمودارها، واضح است که ما مشکل اصلی عملکرد خود را حل کردیم: همه ماژول های WebAssembly در ~ 500 میلی ثانیه یا کمتر اجرا می شوند. این امر آنچه را که در ابتدا ارائه کردیم تأیید می کند: WebAssembly عملکرد قابل پیش بینی را به شما ارائه می دهد. مهم نیست که کدام زبان را انتخاب می کنیم، اختلاف بین مرورگرها و زبان ها حداقل است. به طور دقیق: انحراف استاندارد جاوا اسکریپت در همه مرورگرها 400 میلی‌ثانیه است، در حالی که انحراف استاندارد همه ماژول‌های WebAssembly ما در همه مرورگرها 80 میلی‌ثانیه است.

تلاش

معیار دیگر میزان تلاشی است که برای ایجاد و ادغام ماژول WebAssembly در squoosh باید انجام می دادیم. تخصیص یک مقدار عددی به تلاش دشوار است، بنابراین من هیچ نموداری ایجاد نمی‌کنم اما چند نکته وجود دارد که می‌خواهم به آن اشاره کنم:

AssemblyScript بدون اصطکاک بود. نه تنها به شما اجازه می دهد تا از TypeScript برای نوشتن WebAssembly استفاده کنید و بررسی کد را برای همکارانم بسیار آسان می کند، بلکه ماژول های WebAssembly بدون چسب را نیز تولید می کند که بسیار کوچک با عملکرد مناسب هستند. ابزارهای موجود در اکوسیستم TypeScript، مانند زیباتر و tslint، احتمالاً کار خواهند کرد.

Rust در ترکیب با wasm-pack نیز بسیار راحت است، اما در پروژه‌های WebAssembly بزرگ‌تر که اتصالات و مدیریت حافظه مورد نیاز است، برتری بیشتری دارد. برای دستیابی به اندازه فایل رقابتی، مجبور شدیم کمی از مسیر شاد فاصله بگیریم.

C و Emscripten یک ماژول WebAssembly بسیار کوچک و بسیار کارآمد از جعبه ایجاد کردند، اما بدون شهامت پرش به کد چسب و کاهش آن تا حد نیازها، اندازه کل (ماژول WebAssembly + کد چسب) در نهایت بسیار بزرگ است.

نتیجه

بنابراین اگر یک مسیر داغ JS دارید و می خواهید آن را سریعتر یا سازگارتر با WebAssembly کنید، باید از چه زبانی استفاده کنید. مثل همیشه در مورد سوالات عملکرد، پاسخ این است: بستگی دارد. پس چه چیزی ارسال کردیم؟

نمودار مقایسه

در مقایسه با اندازه ماژول / عملکرد زبان های مختلفی که استفاده می کنیم، بهترین انتخاب C یا AssemblyScript است. تصمیم گرفتیم Rust را ارسال کنیم . دلایل متعددی برای این تصمیم وجود دارد: تمام کدک های ارسال شده در Squoosh تاکنون با استفاده از Emscripten کامپایل شده اند. ما می خواستیم دانش خود را در مورد اکوسیستم WebAssembly گسترش دهیم و از زبانی متفاوت در تولید استفاده کنیم. AssemblyScript یک جایگزین قوی است، اما پروژه نسبتاً جوان است و کامپایلر به اندازه کامپایلر Rust بالغ نیست.

در حالی که تفاوت در اندازه فایل بین Rust و اندازه زبان های دیگر در نمودار پراکندگی کاملاً شدید به نظر می رسد، در واقعیت آنقدرها هم بزرگ نیست: بارگیری 500 بایت یا 1.6 کیلوبایت حتی در 2G کمتر از 1/10 ثانیه طول می کشد. و Rust امیدوار است به زودی شکاف را از نظر اندازه ماژول برطرف کند.

از نظر عملکرد زمان اجرا، Rust میانگین سریع‌تری در بین مرورگرها نسبت به AssemblyScript دارد. به خصوص در پروژه‌های بزرگ‌تر، Rust بدون نیاز به بهینه‌سازی کد دستی، کد سریع‌تری تولید می‌کند. اما این نباید شما را از استفاده از چیزی که بیشتر با آن راحت هستید باز دارد.

همه آنچه گفته شد: AssemblyScript یک کشف بزرگ بوده است. این به توسعه دهندگان وب اجازه می دهد تا ماژول های WebAssembly را بدون نیاز به یادگیری زبان جدید تولید کنند. تیم AssemblyScript بسیار پاسخگو بوده و فعالانه روی بهبود زنجیره ابزار خود کار می کند. مطمئناً در آینده به AssemblyScript توجه خواهیم کرد.

به روز رسانی: زنگ

پس از انتشار این مقاله، نیک فیتزجرالد از تیم Rust به ما کتاب بسیار عالی Rust Wasm را اشاره کرد که شامل بخشی در بهینه سازی اندازه فایل است. پیروی از دستورالعمل‌های موجود در آنجا (به ویژه فعال کردن بهینه‌سازی زمان پیوند و کنترل دستی پانیک) به ما اجازه می‌دهد تا کد Rust «عادی» را بنویسیم و بدون افزایش اندازه فایل به استفاده از Cargo ( npm Rust) برگردیم. ماژول Rust پس از gzip به 370B می رسد. برای جزئیات، لطفاً به روابط عمومی که در Squoosh باز کردم نگاهی بیندازید.

تشکر ویژه از اشلی ویلیامز ، استیو کلابنیک ، نیک فیتزجرالد و مکس گری برای همه کمک هایشان در این سفر.