سرعة فائقة بشكلٍ منتظم
في مقالات السابقة، تحدّثت عن كيفية استخدام WebAssembly لإدخال منظومة مكتبة C/C++ المتكاملة إلى الويب. من التطبيقات التي تستخدم مكتبات C/C++ بشكل مكثّف هو squoosh، وهو تطبيق الويب الذي يتيح لك ضغط الصور باستخدام مجموعة متنوعة من برامج الترميز التي تم compilingها من C++ إلى WebAssembly.
WebAssembly هو جهاز افتراضي منخفض المستوى يشغّل الرمز الثانوي المخزّن
في ملفات .wasm
. هذا الرمز الثنائي مصنّف بشكلٍ دقيق ومُنظَّم بطريقة تسمح بتحويله إلى رمز آلي وتحسينه للنظام المضيف بشكلٍ أسرع بكثير مما يمكن أن توفّره JavaScript. توفّر WebAssembly بيئة لتشغيل الرموز البرمجية التي تم وضع ميزة
وضع الحماية في مساحة معيّنة والاقتران في الاعتبار منذ البداية.
من واقع خبرتي، تعود معظم مشاكل الأداء على الويب إلى التنسيق العميق والرسم المفرط، ولكن من حين لآخر يحتاج التطبيق إلى تنفيذ مهمة مكلّفة من الناحية الحسابية وتستغرق الكثير من الوقت. يمكن أن يساعدك WebAssembly في حلّ هذه المشكلة.
المسار الرائج
في أداة squoosh، كتبنا دالة JavaScript تؤدي إلى تدوير ذاكرة التخزين المؤقت للصورة بمضاعفات 90 درجة. على الرغم من أنّ OffscreenCanvas سيكون مثاليًا للقيام بهذه المهمة، إلا أنّه غير متوافق مع جميع المتصفّحات التي كنا نستهدفها، ويتضمّن بعض الأخطاء في Chrome.
تكرّر هذه الدالة كل بكسل من صورة الإدخال وتنسخه إلى موضع مختلف في صورة الإخراج لتحقيق الدوران. بالنسبة إلى صورة بحجم 4094 بكسل x 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 ثوانٍ. إنّ طريقة المتصفّحات في تحسين 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 لا تحتاج إلى أي ذاكرة إضافية على الإطلاق و تستخدم فقط الحزمة الداخلية لنظام التشغيل الافتراضي.
في هذه الحالة، سنحتاج إلى استخدام بعض الذاكرة الإضافية للسماح بالوصول بشكل عشوائي
إلى وحدات بكسل الصورة وإنشاء نسخة مُحوَّلة من تلك الصورة. وهذا هو
الغرض من WebAssembly.Memory
.
إدارة الذاكرة
بعد استخدام ذاكرة إضافية، ستحتاج عادةً إلى تدبير هذه الذاكرة بطريقة ما. ما هي أجزاء الذاكرة المستخدَمة؟ ما هي الأدوات المجانية؟
في C، على سبيل المثال، لديك الدالة malloc(n)
التي تعثر على مساحة ذاكرة
تتألف من n
بايت متتاليين. وتُعرف الدوالّ من هذا النوع أيضًا باسم "الموزّعين".
بالطبع، يجب تضمين عملية تنفيذ أداة التوزيع المستخدَمة في ملف
وحدة WebAssembly، وسيؤدي ذلك إلى زيادة حجم ملفك. يمكن أن يختلف حجم وأداء
وظائف إدارة الذاكرة هذه بشكل كبير حسب
الخوارزمية المستخدَمة، ولهذا السبب توفّر العديد من اللغات عمليات تنفيذ متعددة
للاختيار من بينها ("dmalloc" و"emmalloc" و"wee_alloc" وما إلى ذلك).
في هذه الحالة، نعرف أبعاد الصورة المُدخلة (وبالتالي أبعاد الصورة الناتجة) قبل تشغيل وحدة WebAssembly. لقد لاحظنا في هذه الحالة فرصة: عادةً ما نُرسل ذاكرة التخزين المؤقت RGBA للصورة المُدخلة كأحد المَعلمات إلى دالة WebAssembly ونعرض الصورة المُديرة كأحد القِيم المعروضة. لإنشاء قيمة الإرجاع هذه، علينا استخدام الموزّع. ولكن بما أنّنا نعرف إجمالي سعة الذاكرة المطلوبة (ضعف حجم الصورة المُدخلة، مرة واحدة للصورة المُدخلة ومرة أخرى للصورة الناتجة)، يمكننا وضع الصورة المُدخلة في ذاكرة WebAssembly باستخدام JavaScript، وتشغيل وحدة WebAssembly لإنشاء صورة ثانية تم تدويرها، ثم استخدام JavaScript لإعادة قراءة النتيجة. يمكننا التخلص من هذه المشكلة بدون استخدام أي إدارة للذاكرة على الإطلاق.
خيارات متعدّدة
إذا اطّلعت على وظيفة JavaScript الأصلية التي نريد تحويلها إلى WebAssembly، يمكنك ملاحظة أنّها رمز حسابي بحت بدون واجهات برمجة تطبيقات خاصة بـ 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 بايت تقريبًا فقط، في حين أنّ رمز
glue يبلغ حجمه حوالي 3.5 كيلوبايت بعد ضغطه باستخدام gzip. بعد بعض التنقّل، تمكّنا من التخلص من
الرمز البرمجي المُجمّع وإنشاء مثيل وحدات WebAssembly باستخدام واجهات برمجة التطبيقات العادية.
يكون ذلك ممكنًا في أغلب الأحيان باستخدام Emscripten ما دامت لا تستخدم أيًا مما يلي:
من المكتبة العادية لـ C.
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 إلى 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. توفّر AssemblyScript الدالتَين load<T>(ptr:
usize)
وstore<T>(ptr: usize, value: T)
لمحاولة
الوصول إلى الذاكرة الأوّلية. لتجميع ملف AssemblyScript،
ما عليك سوى تثبيت حزمة npm AssemblyScript/assemblyscript
وتشغيل
$ asc rotate.ts -b assemblyscript.wasm --validate -O3
سيقدّم لنا AssemblyScript وحدة wasm تبلغ سعتها 300 بايت تقريبًا وبدون رمز ربط. تعمل الوحدة فقط مع واجهات برمجة تطبيقات WebAssembly العادية.
تحليل WebAssembly الجنائي
إنّ حجم Rust الذي يبلغ 7.6 كيلوبايت كبير بشكلٍ مفاجئ مقارنةً باللغتَين الأخرتَين. هناك أداتان في منظومة WebAssembly المتكاملة يمكن أن تساعدك في تحليل ملفات WebAssembly (بغض النظر عن اللغة التي تم إنشاؤها بها) واطلاعك على ما يحدث ومساعدتك أيضًا في تحسين الأداء.
Twiggy
Twiggy هي أداة أخرى من فريق WebAssembly في Rust لاستخراج مجموعة من البيانات الإحصائية من ملف WebAssembly. هذه الأداة ليست خاصة بلغة Rust، وتتيح لك فحص عناصر مثل
مخطّط استدعاء الوحدة، وتحديد الأقسام غير المستخدَمة أو الزائدة، ومعرفة
الأقسام التي تساهم في إجمالي حجم ملف وحدتك. يمكن تنفيذ
الخطوة الأخيرة باستخدام الأمر top
في Twiggy:
$ twiggy top rotate_bg.wasm
في هذه الحالة، يمكننا أن نرى أنّ معظم حجم الملف يرجع إلى المخصّص. كان هذا الأمر مفاجئًا لأنّ الرمز البرمجي الخاص بنا لا يستخدم عمليات التوزيع الديناميكية. ومن العوامل المؤثرة الأخرى أيضًا القسم الفرعي "أسماء الدوالّ".
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
، يمكننا إزالة عدد آخر من البايتات ليصبح إجمالي حجم الملف هو
6.2 كيلوبايت بعد استخدام gzip.
#![no_std]
بعد بعض الاستشارات والأبحاث، أعدنا كتابة رمز Rust بدون استخدام
المكتبة العادية في Rust، وذلك باستخدام ميزة
#![no_std]
. يؤدي ذلك أيضًا إلى إيقاف عمليات تخصيص الذاكرة الديناميكية تمامًا، ما يؤدي إلى إزالة رمز العبارة
allocator من وحدتنا. تجميع ملف 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، يعمل المُجمِّع في مراحل متعدّدة. بعبارة بسيطة، المرحلة الأولى أسرع في compiling، ولكنّها تميل إلى إنشاء رمز أبطأ. بعد بدء تنفيذ الوحدة، يرصد المتصفّح الأجزاء التي يتم استخدامها بشكل متكرّر ويرسلها من خلال مترجم أكثر تحسينًا ولكن أبطأ.
إنّ حالة الاستخدام التي نتناولها مثيرة للاهتمام لأنّ رمز تدوير الصورة سيتم استخدامه مرة واحدة أو مرتين. وبالتالي، في الغالبية العظمى من الحالات، لن نحصل أبدًا على مزايا المُجمِّع المحسِّن. من المهم أن تضع ذلك في الاعتبار عند مقارنة الأداء. سيؤدي تشغيل وحدات WebAssembly 10,000 مرة في حلقة إلى تقديم نتائج غير واقعية. للحصول على أرقام واقعية، يجب تشغيل الوحدة مرّة واحدة و اتخاذ قرارات استنادًا إلى الأرقام الواردة من هذه العملية الواحدة.
مقارنة الأداء
هذان الرسمان البيانيان هما عرضان مختلفان للبيانات نفسها. في الرسم البياني الأول، تتم المقارنة لكل متصفّح، وفي الرسم البياني الثاني، تتم المقارنة لكل لغة مستخدَمة. يُرجى العِلم أنّني اخترت مقياسًا زمنيًا لوغاريتميًا. من المهم أيضًا أنّ جميع اختبارات الأداء كانت تستخدم صورة الاختبار نفسها التي تبلغ دقتها 16 ميغابكسل والجهاز المضيف نفسه، باستثناء متصفّح واحد لم يكن بالإمكان تشغيله على الجهاز نفسه.
بدون تحليل هذه الرسوم البيانية كثيرًا، من الواضح أنّنا حللنا مشكلة الأداء الأصلية: يتم تشغيل جميع وحدات WebAssembly في غضون 500 ملي ثانية تقريبًا أو أقل. يؤكد ذلك ما أوضحناه في البداية: يمنحك WebAssembly أداءً يمكن توقّعه. بغض النظر عن اللغة التي نختارها، يكون التباين بين المتصفّحات واللغات ضئيلًا. على وجه التحديد: يبلغ التباين المعياري لـ JavaScript في جميع المتصفّحات حوالي 400 ملي ثانية، في حين يبلغ التباين المعياري لجميع وحدات WebAssembly في جميع المتصفّحات حوالي 80 ملي ثانية.
الجهد
ومن المقاييس الأخرى مقدار الجهد الذي بذلناه لإنشاء وحدة WebAssembly ودمجها في أداة squoosh. من الصعب تحديد قيمة عددية لمحاولة التحسين، لذا لن أُنشئ أيّ رسوم بيانية، ولكن هناك بعض النقاط التي أودّ لفت انتباهك إليها:
كان استخدام AssemblyScript سلسًا. لا يسمح لك هذا الإصدار فقط باستخدام TypeScript لكتابة WebAssembly، ما يسهّل على زملائي مراجعة الرموز البرمجية، بل يسمح أيضًا بإنشاء وحدات WebAssembly صغيرة جدًا وذات أداء جيد بدون استخدام أي أدوات مساعدة. من المرجّح أن تعمل الأدوات في منظومة TypeScript المتكاملة، مثل prettier وtslint، ببساطة.
إنّ Rust مع wasm-pack
ملائم للغاية أيضًا، ولكنه ينجح بشكلٍ أفضل في مشروعات WebAssembly الأكبر حجمًا التي تتطلّب ربطًا وإدارة ذاكرة. كان علينا الابتعاد قليلاً عن المسار الصحيح لتحقيق حجم ملف competitiv.
أنشأت C وEmscripten وحدة WebAssembly صغيرة جدًا وذات أداء عالٍ بشكل تلقائي، ولكن بدون الجرأة على الانتقال إلى رمز التجميع وتقليله إلى الحد الأدنى من الاحتياجات، ينتهي الأمر بحجم إجمالي (وحدة 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 إلى كتاب Rust Wasm الرائع الذي يحتوي على
قسم حول تحسين حجم الملف. من خلال اتّباع
التعليمات الواردة هناك (أبرزها تفعيل التحسينات في وقت الربط ومعالجة الصعوبات المتعلّقة بالبرمجة بشكل يدوي)، تمكّنا من كتابة رمز Rust "عادي" والعودة إلى استخدام
Cargo
(npm
من Rust) بدون زيادة حجم الملف. تنتهي وحدة Rust
بحجم 370 بايت بعد استخدام gzip. لمعرفة التفاصيل، يُرجى الاطّلاع على طلب الإصدار الذي فتحته على Squoosh.
"شكر خاص لأشلي ويليامز، وستيف كلابنيك، ونيك فيتزجيرالد، وماكس جراي على كل مساعدتهم في هذه الرحلة".