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

إنه سريع باستمرار،

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

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

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

المسار الساخن

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

وتكرر هذه الدالة كل بكسل في الصورة المدخلة وتنسخها إلى موضع مختلف في الصورة الناتجة لتحقيق الدوران. للحصول على صورة بمقاس 4094 × 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 معقدة حقًا، وتعمل المحرّكات المختلفة على التحسين من أجل تنفيذ مهام مختلفة. البعض يحسّن التنفيذ الأولي، والبعض الآخر يحسن التفاعل مع نموذج العناصر في المستند (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. تكون الحزمة داخلية بالكامل ولا يمكن لمطوّري الويب الوصول إليها (إلا من خلال "أدوات مطوري البرامج"). وبالتالي، من الممكن كتابة وحدات 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. لذلك يجب أن يكون ذلك مباشرة إلى حد ما لنقل هذه التعليمة البرمجية إلى أي لغة. وقد قيّمنا ثلاث لغات مختلفة يتم استخدامها في التجميع على WebAssembly: C/C++، وRst و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. غالبًا ما يكون هذا ممكنًا مع Emscripten طالما أنك لا تستخدم أي شيء من مكتبة C القياسية.

Rust

Rust هي لغة برمجة جديدة وحديثة ذات نظام كتابة فعّال، بدون وقت تشغيل ونموذج ملكية يضمن أمان الذاكرة وأمان سلسلة التعليمات. يدعم Rust أيضًا WebAssembly كميزة أساسية، وقد ساهم فريق Rust في الكثير من الأدوات الممتازة في منظومة WebAssembly المتكاملة.

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

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

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

التحليل الجنائي من WebAssembly

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

تويغي

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

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

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

شريط Wasm

wasm-strip هي أداة من WebAssembly Binary Toolkit أو wabt للاختصار wabt. وهو يحتوي على أداتَين تتيحان لك فحص وحدات WebAssembly ومعالجتها. wasm2wat هي أداة تفكيك تُحوِّل وحدة Wasm الثنائية إلى تنسيق يمكن للإنسان قراءته. يحتوي Wabt أيضًا على wat2wasm الذي يتيح لك إعادة تحويل التنسيق الذي يمكن للمستخدمين قراءته إلى وحدة Wabt الثنائية. لقد استخدمنا هاتَين الأداتَين المكمّلتَين لفحص ملفات WebAssembly، ولكنّنا وجدنا أنّ السمة wasm-strip هي الأكثر فائدة. تزيل wasm-strip الأقسام والبيانات الوصفية غير الضرورية من وحدة WebAssembly:

$ wasm-strip rotate_bg.wasm

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

$ 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 المتكاملة، مثل الأدوات الأكثر جمالاً وقابلية التدخين، على الفور.

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

ابتكر كل من C وEmscripten وحدة WebAssembly

الخلاصة

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

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

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

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

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

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

تعديل: قديم

بعد نشر هذه المقالة، وجّهنا {0}Nick Fitzgerald{/1}{101}من فريق Rust إلى كتابهم الرائع Rust Wasm، والذي يحتوي على{101}{2}قسم حول تحسين حجم الملف{/3}. وقد سمح لنا اتّباع التعليمات الواردة فيها (وأبرزها تفعيل عمليات تحسين وقت الربط والتعامل مع حالة القلق اليدوي) بكتابة رمز Rust "عادي" والرجوع إلى استخدام Cargo (أي npm الخاص بالصدأ) بدون تضخم حجم الملف. ينتهي الأمر بوحدة Rust إلى 370B بعد gzip. للحصول على التفاصيل، يُرجى الاطّلاع على PR الذي فتحته على Squoosh.

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