وسام البهنسي

مدونة مبرمج معماري

عندما نجعل الجيجا أبطأ من الميجا

السلام عليكم ورحمة الله،

طن طن طن طن طن... لو قرأتَ الطنّات الخمس السابقة بأسرع ما تستطيع، ربما تصل لسرعة تقارب ٥ طنات في الثانية، أو بعبارة أخرى تستطيع أن تطن بسرعة ٥ هيرتز.

والآن ماذا لو طلبت منك ١٠٠٠ طنة في الثانية، أو واحد كيلو هيرتز؟ ما رأيك بمليون طنة، أو ميجا هيرتز؟ سريع جداً أليس كذلك؟ لقد ابتعدنا كثيراً عن إمكانية تمييز كل طنة بشكل مستقل. فلنرفع الرقم قليلاً مرة أخيرة ونرصد سرعة 3.58 ميجا هيرتز.

القارئ الهرِم منكم الذي يتكئ على العكاز قد يتعرف هذا الرقم لو كان مهووساً بالكمبيوترات منذ ثلاثة عقود من الزمن. إنه تردد عمل معالجات أجهزة صخر MSX.

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

بل وفوق كل ذلك، لم تعد تكتفي الكمبيوترات الشخصية المعاصرة بمعالج واحد، بل أصبحنا نتحدث عن معالجات عديدة الأنوية (٢، ٤، ٦، ٨، ٦٤ وحتى ما شاء الله). هل تستطيع تخيل كل هذه الطاقة الحوسبية؟

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

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

عندما نتأمل هذا الوضع، ثم ننظر إلى لعبة كمبيوتر بسيطة بالكاد تعمل بمعدل ٣٠ لقطة في الثانية على مثل هذه الأجهزة... عندها يجب أن نتساءل؟ من الحمار الذي استطاع أن يكتب برنامجاً يعمل بهذا البطء على جهاز بهذه السرعة؟!

لا تأخذ العبارة السابقة على محمل شخصي لو سمحت. فلا عيب في ذلك، لأننا جميعاً مررنا بهذه المرحلة في وقت من الأوقات في حياتنا البرمجية. والمتذاكين منكم قد يرفعون أنوفهم ويقولون نحن لا دخل لنا بهذا لأننا فقط نستعمل محركات الألعاب ولا نبرمجها، اذهب واتهم من بنى هذه المحركات (ملعوبة، لكن اقرؤوا معي للنهاية لتعرفوا ما تقدمه محركاتكم هذه). لكن عموماً، نستطيع أن نستدل من سهولة جعل البرنامج يعمل ببطء على مدى حساسية هذه المعالجات رغم قوتها.

نعم، فالمبرمج الغير واعي بتبعات الكود الذي يكتبه، سيجد أنه يدمّر أداء برنامجه تدميراً، وأن كل هذه الجيجاهرتزات لا تفيده بشيء.. بل ربما ألعاب جهاز صخر تعمل بأداء أفضل على معالجها ذو ال 3.58 ميجاهيرتز!

بالفعل، هناك جوانب عديدة تقتل الأداء في المعالجات، لا سيما الحديثة منها. مثلاً برنامج يخيب تنبؤات التفرع (branch misprediction) في المعالج باستمرار، أو حتى كثير التفرع وكفى. تسألني كيف؟ خذ مثلاً كود يقوم بمعالجة كل بيكسل في الشاشة وبناء على قيمة لون البيكسل، ينفذ إحدى عدة فروع من كتلة شرطية (كتعليمة switch مثلاً).

هناك أيضاً القفز المستمر عبر المؤشرات، ويسمى أيضاً تتبع المؤشرات (pointer chasing). والمثال على هذا سهل جداً: القوائم المترابطة (linked lists) عدوّة المعالجات رقم واحد، وبدرجة قريبة أيضاً النداءات الافتراضية (virtual calls).

وأخيراً (وليس آخِراً) نذكر نمط الوصول إلى الذاكرة (memory access pattern) وتلويث الكاش (cache thrashing)، والذي يشكل عامل أداء هام جداً في المعالجات الحديثة. فكلما كان الوصول للذاكرة أكثر عشوائية وفوضوية، كلما قلّت فعالية نظم التخزين السريع الموجودة على المعالج، بحيث يصبح الأداء مرهوناً بتأخر وصول البيانات من الذاكرة الأساسية (system RAM).

ولكن قد يُعتبر كل ما سبق مجرد نماذج مجهرية من مشاكل الأداء. فالكثير من البرامج قد لا تحوي حلقات تكرار عالية التردد بحيث تتركز المشكلة في مكان معين. وإنما يتبدد أو يتسرب الأداء بتجانس بين مئات أو آلاف الأماكن في الكود ليبني في المجمل مشكلة أداء كبيرة. وهذه الحالة تعرف بالموت بألف جرح، وهي من أصعب مشاكل الأداء.

لو قرأنا الصفحة الأولى من كتاب "ألِف باء تحسين الأداء" لعرفنا أن أهم خطوة لحل مشاكل الأداء في البرامج هي التشخيص (profiling). وببساطة هي معرفة سبب المشكلة قبل أن تحاول حلها. وستفاجأ أيها القارئ الكريم بعدد ورتبة المبرمجين الذين يقعون في هذا الفخ. فلنسجل هذه المعلومة القيمة في عقلنا الباطن، علها تطفو في الوقت المناسب.

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

بدايةً، المحرك العام الذي يقدّم نفسه كفؤاً لإنجاز كافة أنواع الألعاب، هذا المحرك في أغلب الأحيان يقوم بتشغيل الكثير من النظم التي يدعمها حتى وإن لم تستخدمها اللعبة!

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

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

السبب الثاني قد يفاجئ البعض، وهو ببساطة تعاسة أو سذاجة الكود في هذه المحركات. فكون هذه المحركات وراءها شركة كبيرة للأسف لا يعتبر ضماناً أن مبرمجي هذه المحركات قد وضعوا فيها الكود الأفضل.

أذكر حوالي عام ٢٠٠٩ أنني كنت أعمل في EA على لعبة تصويب على محرك أنريل ٣. وبما أنها لعبة تصويب فمن الطبيعي أن تظهر للاعب آثار طلقاته على الجدران إن أصابها. قلنا بسيطة، المحرك يدعم ميزة اللصاقات (decals) والتي تسمح لنا بوضع صورة أو إكساء على أي سطح في المشهد (مثلاً صورة ثقب رصاصة). تفضلوا استخدموها.. فقام أحد المبرمجين بكتابة كود يبتعث لصاقة عند كل ارتطام بين رصاصة وجدار في اللعبة. جميل... الآن يدخل اللاعب المرحلة والأداء عند ٣٠ لقطة في الثانية، يسحب مسدسه ويبدأ بإطلاق الرصاص... رصاصة، اثنين، ثلاثة، لا مشكلة.. وبدءاً من الرابعة أخذ الأداء بالتناقص وصولاً لعاشر طلقة كان الأداء قد نزل من ٣٠ إلى ٣ لقطات في الثانية فقط! بهذه النتيجة "المشجعة" لك أن تتخيل ما سيحصل للعبة عند استخدام السلاح الرسمي فيها، وهو المدفع الرشاش. لن أطيل عليكم بالتفاصيل هنا لكن المحصلة أنني اضطررت لإعادة كتابة كود الرسوميات لميزة اللصاقات في المحرك للنهوض بالأداء من ٣ لقطات في الثانية عند العشر لصاقات في المشهد، ليعود لمعدله الطبيعي عند الثلاثين لقطة في الثانية حتى مع ١٠٠ لصاقة في المشهد. وبصراحة، هذا التحسين في الأداء لم يكن نتاج أبحاث عميقة أو أفكار خلابة أتيتُ بها، وإنما فقط إعادة كتابة الميزة مع مراعاة بديهيات الأداء في رسوميات الحاسوب... هذه فقط واحدة من قصص المعاناة مع محرك أنريل ٣ الذي كانت كلفة ترخيصه للمشروع الواحد لا تقل عن المليون دولار.

قد تكون القصة السابقة محطّ سخرية يستغلها عشاق محرك يونيتي في حربهم الأزلية ضد عشاق محرك أنريل، لكني أعتذر منكم عندما أقول أن يونيتي ليس بأفضل حالاً... أبداً!

محرك يونيتي مليء بمثل هذه السذاجات لدرجة أنها تصيبك بنزيف دماغي عند رؤية الكود الداخلي للمحرك. الكثير من أنظمة المحرك تزداد كلفتها بشكل فوق-خطي مع ازدياد حجم العمل. الحالة المقبولة في عُرف هندسة البرمجيات هي نمو خطي للكلفة مع حجم المدخلات كحد أقصى. مثلاً ٥٠ شخصية تكلف ٥ ميللي ثانية وبالتالي ١٠٠ شخصية تكلف ١٠ ميللي ثانية. هذا ما يسمى بالمقايسة (scalability). وعادة يعتبر الكود فاشلاً في المقايسة إن كان أداؤه يتباطأ بشكل أكبر من خطي مع ازدياد حجم المدخلات. فما رأيك بأداء يُبطئ بشكل أسّي مع ازدياد حجم المدخلات؟

أضف إلى ذلك الكثير من النظم المفروضة داخلياً والتي لا يمكنك تعطيلها حتى (لي قصة مع أحد هذه النظم في يونيتي ذكروني أن أتحدث عنها في تدوينة لاحقة!).

أحد أصدقائي المخضرمين في أمور تحسين الأداء كان سيء الحظ ليعمل في مشروع لعبة كبيرة مبنية بمحرك يونيتي لتعمل على أجهزة الموبايل. طبعاً الأخ لا يكتفي بالطفو على سطح المحرك، وإنما قرر ترك المسخرة كلها والدخول فوراً إلى أعماق المحرك لرؤية ما يحدث وراء الكواليس دون حتى أن يحصل على الكود الداخلي للمحرك من يونيتي. ذكر لي كم هاله عدد السطور التي فقط تتحقق من أن مؤشراً ما ليس صفرياً قبل استخدام ذاك المؤشر (null pointer check). تقريباً كل عملية تبدأ بالكشف عن مؤشر صفري! لذلك قام بتجربة طريفة، حيث أزال كل تلك السطور بطريقة مؤتمتة، ليجعل الكود ينساب دون الاحتكاك بكشوف المؤشرات الصفرية. والنتيجة؟ تحسن أداء ٣٠% فوري في كل أداء اللعبة! طبعاً لم تستمر اللعبة بالعمل طويلاً قبل أن تنهار بسبب محاولة الوصول لمؤشر صفري. لكن ٣٠% ؟!  على اختبارات ربما أغلبها لا طائل منه؟

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

هيييييه.. اليوم سألني أحد زملائي في إنفيديا عن لعبتي هايبر فويد. من عَمِل بها؟ كم استغرقَتْ من الوقت؟ لماذا تمّ إصدارها على عدة منصات بشكل متدرج بدلاً من دفعة واحدة؟ وأخيراً، لماذا لم نستخدم يونيتي؟

عندما طرح عليّ هذا السؤال الأخير استعدتُ قطار ذكرياتي مع يونيتي وأنريل وتخيلتُ كيف سيكون أداء اللعبة مع كل ذلك الحمل العديم الفائدة.

اللعبة عمِلَتْ بأداء ١٨٠ لقطة في الثانية على دقة 1440p مع 2xMSAA على ال PSVR (أي أن المشهد يُرسم مرتين، مرة لكل عين على سطح رسم بحجم 2880 بيكسل ارتفاعاً) على جهاز PS4 الأول.

هذا ما حققَتْه هايبر فويد بمحرك مكتوب من الصفر. وللشهادة فإن المحرك لم يستخدم حتى تعدد المسارات إلا لتشغيل الموسيقى والأفلام! بل وهذا دون حتى أن أبذل أي جهد حقيقي لتحسين أداء اللعبة، أي أننا نستطيع أن نصل لأعلى من ١٨٠ لقطة في الثانية لو خصصنا بعض الوقت لتحسين الأداء...

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

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

للحديث بقية، لكننا نكتفي بهذا القدر اليوم...

والسلام عليكم ورحمة الله!

التعليقات (2) -

  • سرمد خالد عبد الله

    29/03/2021 01:27:01 م | الرد

    شكراً على المعلومات وعلى المقال الرائع. لم أتخيل أن الكشوف الصفرية يمكن أن تكلفك 30% من الأداء.

    أحسنت في الطرح تقنيا، وأبدعت لغويًا. ليت كل الكتاب العرب يتعلمون المصطلحات الصحيحة كما تفعل، ويتوقفون عن استخدام العبارة التي ترفع لي ضغطي، عبارة "إطار في الثانية".

  • وسام البهنسي

    30/03/2021 08:49:29 ص | الرد

    شكراً لإطرائك أخي سرمد. في حالة يونيتي، هذه الكشوف مكلفة بسبب كثرتها. توجد في المعالجات الحديثة خوارزميات تنبؤ بالكود الذي سيتم تنفيذه قريباً فيتم جلبه من الذاكرة استعداداً لنفيذه. لكن وجود شرط في الكود قد يتسبب بخيبة التنبؤ، وبالتالي ضياع في الأداء. الكلفة صغيرة لكن عند كثرة التفرعات الشرطية تتراكم الكلفة لتصبح ظاهرة... في حالة يونيتي يبدو أنها وصلت ل ٣٠% من الأداء، وهذا رقم مجنون صراحة!

أضف تعليقاً

Loading