استبدال مسار سريع في رمز JavaScript لتطبيقك باستخدام WebAssembly

إنه سريع دائمًا، أليس كذلك

تحدثت في السابقة مقالات عن كيفية إتاحة WebAssembly لك النظام الشامل للمكتبة بتنسيق C/C++ على الويب. أحد التطبيقات التي تستخدم مكتبات C/C++ بشكل كبير هو squoosh، وهو تطبيق الويب الذي يتيح لك ضغط الصور باستخدام مجموعة متنوعة من برامج الترميز التي تم تجميعها من C++ إلى WebAssembly.

WebAssembly هو جهاز افتراضي منخفض المستوى تشغّل رمز البايت الذي يتم تخزينه في ملفات .wasm. وتتم كتابة رمز البايت هذا وتنظيمه بشدّة بطريقة تتيح تجميعه وتحسينه للتوافق مع النظام المضيف بشكل أسرع بكثير من JavaScript. توفّر WebAssembly بيئة لتشغيل الرموز البرمجية التي كانت تضع وضع الحماية والتضمين في الاعتبار منذ البداية.

من واقع خبرتي، ترجع معظم مشكلات الأداء على الويب إلى التخطيط الإجباري والتحميل المفرط ولكن من حين لآخر يحتاج أحد التطبيقات إلى تنفيذ مهمة مكلفة من الناحية الحسابية وتستغرق وقتًا طويلاً. يمكن أن تساعدك WebAssembly هنا.

أغنية The Hot Path

في squoosh، كتبنا دالة جافا سكريبت التي تعمل على تدوير المخزن المؤقت للصور بمقدار مضاعفات بمقدار 90 درجة. وعلى الرغم من أنّ OffscreenCanvas قد تكون مثالية لهذا الغرض، إلا أنّ المنصة غير متوافقة مع المتصفّحات التي استهدفناها، كما أنّها تتضمّن بعض الأخطاء في Chrome.

تتكرر هذه الدالة على كل بكسل من صورة المدخلة وتنسخها إلى موضع مختلف في صورة الإخراج لتحقيق التدوير. للحصول على صورة بحجم 4094 x 4096 بكسل (16 ميغابكسل)، ستحتاج إلى أكثر من 16 مليون تكرار لكتلة التعليمات البرمجية الداخلية، وهو ما نسميه "المسار السريع". وعلى الرغم من هذا العدد الكبير من التكرارات، يُنهي اثنان من كل ثلاثة متصفحات اختبرناها المهمة في ثانيتين أو أقل. مدة مقبولة لهذا النوع من التفاعل.

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 هي معقدة حقًا، وتعمل محرّكات مختلفة على إجراء تحسينات مختلفة. وبعضها يُحسن التنفيذ الأوّلي، بينما يُحسِّن البعض الآخر للتفاعل مع نموذج العناصر في المستند. في هذه الحالة، لدينا مسار غير محسّن في متصفح واحد.

من ناحية أخرى، تستند 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. تكون الحزمة داخلية بالكامل عبر الجهاز الافتراضي ولا يمكن لمطوّري البرامج على الويب الوصول إليها (إلا من خلال "أدوات مطوري البرامج"). وبالتالي، يمكن كتابة وحدات WebAssembly التي لا تحتاج إلى أيّ ذاكرة إضافية على الإطلاق ولا تستخدم سوى حزم VM-internal فقط.

في هذه الحالة، سنحتاج إلى استخدام ذاكرة إضافية للسماح بالوصول العشوائي إلى وحدات البكسل في الصورة وإنشاء نسخة مستديرة من تلك الصورة. لهذا السبب، الهدف من "WebAssembly.Memory".

إدارة الذاكرة

وعادةً، بمجرد استخدام ذاكرة إضافية، ستجد الحاجة إلى إدارة تلك الذاكرة بطريقة ما. ما هي أجزاء الذاكرة المستخدَمة؟ أي منها مجانية؟ في لغة C، على سبيل المثال، تكون لديك الدالة malloc(n) التي تبحث عن مساحة ذاكرة تبلغ n بايت متتاليًا. تُعرف الدوال من هذا النوع أيضًا باسم "أدوات تخصيص الإعلانات". وبالطبع، يجب تضمين عملية تنفيذ أداة التخصيص المستخدمة في وحدة WebAssembly التي ستؤدي إلى زيادة حجم الملف. قد يختلف هذا الحجم والأداء لوظائف إدارة الذاكرة هذه بشكل كبير اعتمادًا على الخوارزمية المستخدمة، ولهذا السبب توفّر العديد من اللغات عمليات تنفيذ متعددة للاختيار من بينها ("dmalloc" و"emmalloc" و"wee_alloc"، وما إلى ذلك).

في حالتنا، نعرف أبعاد الصورة المدخلة (ومن ثم أبعاد صورة الإخراج) قبل تشغيل وحدة WebAssembly. وهنا صادفنا فرصة: عادةً ما كنا نمرر المخزن المؤقت RGBA الخاص بالصورة المُدخلة كمعلمة إلى دالة WebAssembly ونعيد الصورة التي يتم تدويرها كقيمة عرض. لإنشاء تلك القيمة المعروضة، يتعين علينا الاستفادة من مخصص. وبما أنّنا نعرف المساحة الإجمالية المطلوبة للذاكرة (ضعف حجم الصورة المُدخلة، مرة للإدخال ومرة للمخرجات)، يمكننا وضع الصورة المُدخلة في ذاكرة WebAssembly باستخدام JavaScript وتشغيل وحدة WebAssembly لإنشاء صورة ثانية تم تدويرها ثم استخدام JavaScript لقراءة النتيجة. يمكننا الهروب دون استخدام أي إدارة للذاكرة على الإطلاق!

استكشِفها للاختيار

إذا اطّلعت على دالة JavaScript الأصلية التي نريد استخدامها في WebAssembly-fy، ستلاحظ أنها عبارة عن رمز حسابي بحت بدون أي واجهات برمجة تطبيقات خاصة بـ JavaScript. لذلك يجب أن يكون نقل هذا الرمز إلى أي لغة مباشرة إلى حد ما. قمنا بتقييم 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). لتسهيل ذلك، يمكننا إنشاء مصفوفة من الأعداد الصحيحة 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 يتم ضغطها بتنسيق gzip فقط لحوالي 260 بايت فقط، في حين يبلغ حجم رمز الغراء حوالي 3.5 كيلوبايت بعد gzip. وبعد مرور بعض الوقت، تمكّنا من إزالة الرمز الملتصق وإنشاء مثيل لوحدات WebAssembly باستخدام واجهات برمجة التطبيقات vanilla API. غالبًا ما يكون ذلك ممكنًا مع Emscripten طالما أنك لا تستخدم أي شيء من مكتبة C standard.

Rust

Rust هي لغة برمجة جديدة وحديثة مع نظام من النوع المفيد، وبدون وقت تشغيل، ونموذج ملكية يضمن الحفاظ على أمان الذاكرة وسلسلة المحادثات. يدعم 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 كيلوبايت وبها حوالي 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، نحتاج فقط إلى تثبيت حزمة npm AssemblyScript/assemblyscript وتشغيل

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

ستزوّدنا شركة AssemblyScript بوحدة Wasm بحجم 300 بايت تقريبًا وبدون رموز غراء. تعمل الوحدة فقط مع واجهات برمجة تطبيقات vanilla WebAssembly.

تحليل WebAssembly

إنّ حجم 7.6 كيلوبايت في Rust كبير بدرجة مدهشة مقارنةً باللغتَين الأخريَين. هناك أداتان في منظومة WebAssembly المتكاملة يمكن أن تساعدك في تحليل ملفات WebAssembly (بصرف النظر عن اللغة المستخدَمة لإنشاء المحتوى)، وإخبارك بما يحدث، وتحسين حالتك.

تويغي

Twiggy هي أداة أخرى من فريق WebAssembly تابع لـ Rust تستخرج مجموعة من البيانات المفيدة من وحدة WebAssembly. ولا تقتصر الأداة على Rust وتتيح لك فحص عناصر مثل الرسم البياني للاتصالات بالوحدة، وتحديد الأقسام غير المستخدَمة أو الزائدة، ومعرفة الأقسام التي تساهم في إجمالي حجم الملف للوحدة. يمكن تنفيذ الطريقة الثانية باستخدام أمر top في Twiggy:

$ twiggy top rotate_bg.wasm
لقطة شاشة لتثبيت Twiggy

وفي هذه الحالة، يمكننا أن نرى أن غالبية حجم الملفات يأتي من المخصص. كان ذلك مفاجئًا لنا لأنّ الرمز البرمجي لا يستخدم تخصيصات ديناميكية. هناك عامل مساهم كبير آخر هو القسم الفرعي "أسماء الدوال".

شريط Wasm

wasm-strip هي أداة من WebAssembly Binary Toolkit، أو wabt باختصار. ويحتوي على أداتَين تتيحان لك فحص وحدات WebAssembly ومعالجتها. wasm2wat هي أداة تفكيك يحوّل وحدة Wasm الثنائية إلى تنسيق يمكن للمستخدمين قراءته. يحتوي Wabt أيضًا على wat2wasm، ما يتيح لك إعادة تحويل ذلك التنسيق الذي يمكن للإنسان قراءته إلى وحدة Wab ثنائية. لقد استخدمنا هاتَين الأداتَين المكمّلتَين لفحص ملفات 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، يمكننا تقليص عدد قليل من وحدات البايت لترك إجمالي 6.2 كيلوبايت بعد 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 كيلوبايت بعد wasm-opt وwasm-strip وgzip. في حين أنه لا يزال أكبر من الوحدات التي تم إنشاؤها بواسطة C وAssemblyScript، إلا أنه صغير بما يكفي لاعتباره خفيفًا.

عروض أداء

قبل أن ننتقل إلى الاستنتاجات بناءً على حجم الملف فقط - ذهبنا في هذه الرحلة لتحسين الأداء وليس حجم الملف. إذن كيف قمنا بقياس الأداء وماذا كانت النتائج؟

كيفية قياس الأداء

على الرغم من أنّ WebAssembly تنسيق منخفض المستوى لرمز البايت، لا يزال يجب إرسالها من خلال برنامج تجميع لإنشاء رمز جهاز خاص بالمضيف. تمامًا مثل JavaScript، يعمل المحول البرمجي في مراحل متعددة. وباختصار: المرحلة الأولى أسرع بكثير في التجميع ولكنها تميل إلى إنشاء تعليمات برمجية أبطأ. فور بدء تشغيل الوحدة، يلاحظ المتصفح الأجزاء التي يتم استخدامها بشكل متكرر ويرسلها من خلال برنامج تجميع أبطأ وأكثر بطئًا.

من المثير للاهتمام في حالة الاستخدام لدينا أن رمز تدوير الصورة سيُستخدم مرة واحدة، وربما مرتين. لذا في الغالبية العظمى من الحالات لن نحصل أبدًا على فوائد من التجميع المحسّن. من المهم أن تضع هذا في اعتبارك عند قياس الأداء. إنّ تشغيل وحدات WebAssembly 10000 مرة بشكل متكرّر قد يؤدي إلى الحصول على نتائج غير واقعية. للحصول على أرقام واقعية، يجب علينا تشغيل الوحدة مرة واحدة واتخاذ القرارات بناءً على الأرقام من تلك الجولة الفردية.

مقارنة الأداء

مقارنة السرعة لكل لغة
مقارنة السرعة لكل متصفح

هذان الرسمان البيانيان يمثلان وجهات نظر مختلفة لنفس البيانات. نقارن في الرسم البياني الأول حسب كل متصفح، وفي الرسم البياني الثاني نقارن حسب اللغة المستخدمة. يُرجى ملاحظة أنني اخترت مقياسًا زمنيًا لوغاريتميًا. ومن المهم أيضًا أن تكون جميع مقاييس الأداء تستخدم نفس صورة الاختبار التي تبلغ دقتها 16 ميغابكسل ونفس الجهاز المضيف، باستثناء متصفح واحد لا يمكن تشغيله على الجهاز نفسه.

بدون تحليل هذه الرسومات البيانية أكثر من اللازم، يتضح لنا أننا أصلحنا مشكلة الأداء الأصلية: تعمل جميع وحدات WebAssembly في غضون 500 ملي ثانية أو أقل تقريبًا. وهذا يؤكد ما وضعناه في البداية: تمنحك WebAssembly أداءً متوقّعًا. وبغض النظر عن اللغة التي نختارها، يكون الاختلاف بين المتصفحات واللغات ضئيلاً. على وجه الدقة، يبلغ الانحراف المعياري لـ JavaScript في جميع المتصفحات 400 ملي ثانية تقريبًا، في حين يبلغ الانحراف المعياري لجميع وحدات WebAssembly في جميع المتصفحات حوالي 80 ملي ثانية.

الجهد

ويتوفر مقياس آخر وهو مقدار الجهد الذي اضطررنا إلى بذله لإنشاء ودمج وحدة WebAssembly في squoosh. من الصعب تعيين قيمة رقمية للجهد، لذلك لن أقوم بإنشاء أي رسوم بيانية ولكن هناك بعض الأشياء التي أود الإشارة إليها:

كانت AssemblyScript سلسة للغاية. فهو لا يتيح لك استخدام TypeScript لكتابة WebAssembly، ما يسهّل مراجعة الرموز البرمجية على زملائي، كما يتيح لك أيضًا إنشاء وحدات WebAssembly خالية من الصمغ صغيرة الحجم وذات أداء جيد. من المحتمل أن تنجح الأدوات في منظومة TypeScript المتكاملة، مثل الأجمل والأخطار tslint.

بالإضافة إلى ذلك، يعتبر الصدأ عند استخدام wasm-pack مناسبًا جدًا، لكنّه يتفوق كثيرًا في مشاريع WebAssembly الأكبر حجمًا التي كانت تحتاج إلى الربط وإدارة الذاكرة. كان علينا الابتعاد قليلاً عن المسار الصحيح لتحقيق حجم ملف تنافسي.

أنشأ كل من C وEmscripten وحدة WebAssembly صغيرة وعالية الأداء جدًا من العلبة، ولكن بدون الشجاعة لاستخدام التعليمات البرمجية الملتصقة وتقليلها إلى الضروريات.

الخلاصة

إذًا، ما هي اللغة التي يجب استخدامها إذا كان لديك مسار JavaScript سريع وتريد أن تجعله أسرع أو أكثر اتّساقًا مع WebAssembly. كما هو الحال دائمًا مع أسئلة الأداء، الإجابة هي: الأمر على حسب. إذًا ماذا شحننا؟

الرسم البياني للمقارنة

بالمقارنة بين حجم الوحدة وأدائها للغات المختلفة التي استخدمناها، فإن الخيار الأفضل هو إما C أو AssemblyScript. قرّرنا شحن Rust. هناك أسباب متعددة لهذا القرار: جميع برامج الترميز التي تم شحنها في Squoosh حتى الآن تم تجميعها باستخدام Emscripten. أردنا توسيع نطاق معرفتنا بمنظومة WebAssembly المتكاملة واستخدام لغة مختلفة في الإنتاج. تُعد AssemblyScript بديلاً قويًا، ولكن المشروع صغير نسبيًا وبرنامج التحويل البرمجي ليس ناضجًا مثل محول Rust.

بينما يبدو الفرق في حجم الملف بين Rust واللغات الأخرى كبيرًا إلى حد كبير في الرسم البياني للنقاط المبعثرة، إلا أن الأمر ليس كبيرًا على أرض الواقع: فإن تحميل 500 مليار أو 1.6 كيلوبايت حتى أكثر من 2 جيجا يستغرق أقل من 1/10 من الثانية. ونأمل أن يسد Rust الفجوة في ما يتعلق بحجم الوحدة قريبًا.

في ما يتعلق بأداء وقت التشغيل، تحقّق Rust متوسطًا أسرع في المتصفحات مقارنةً بـ AssemblyScript. وبشكل خاص في المشاريع الكبيرة، من المرجح أن ينتج Rust على شكل رموز أسرع بدون الحاجة إلى تحسينات يدوية للرموز. لكن هذا لا ينبغي أن يمنعك من استخدام ما تشعر بالراحة تجاهه.

ويُرجى العِلم أنّ AssemblyScript كانت اكتشافًا عظيمًا. فهي تتيح لمطوّري البرامج على الويب إنشاء وحدات WebAssembly بدون الحاجة إلى تعلُّم لغة جديدة. كان فريق AssemblyScript مستجيبًا للغاية ويعمل بنشاط على تحسين سلسلة أدواتهم. وبالتأكيد سنراقب AssemblyScript في المستقبل.

تحديث: Rust

بعد نشر هذه المقالة، أشار Nick Fitzgerald من فريق Rust إلى كتابه الممتاز Rust Wasm الذي يحتوي على قسم حول تحسين حجم الملف. ومن خلال اتّباع التعليمات الواردة هناك (لا سيّما تفعيل تحسينات وقت الربط والمعالجة اليدوية للهلع) أتاح لنا كتابة رمز Rust "العادي" والرجوع إلى استخدام Cargo (npm من Rust) بدون المبالغة في حجم الملف. تنتهي وحدة Rust بحجم 370B بعد gzip. للاطّلاع على التفاصيل، يُرجى إلقاء نظرة على المعلومات العامة التي فتحتها على Squoosh.

نشكر آشلي ويليامز وستيف كلابنيك ونك فيتزجيرالد وماكس غراي على كل مساعدتهم في هذه الرحلة.