كيف تعرف أن بطء الكود سببه الخوارزمية لا اللغة؟
عالم البرمجة
تكتب دالة تبدو بسيطة، تختبرها على مئة عنصر فتعمل بسرعة، ثم تضعها أمام مئة ألف عنصر فتتحول الثواني إلى انتظار طويل.
في هذه اللحظة يكون من السهل أن تقول إن اللغة بطيئة، أو أن المترجم سيئ، أو أن الحل يحتاج
إلى إعادة كتابة بلغة أسرع.
وقد يكون هذا صحيحًا أحيانًا، لكنه ليس أول استنتاج ينبغي القفز إليه.
فالبطء قد يأتي من خوارزمية تتضاعف خطواتها بسرعة مع زيادة البيانات، أو من بنية بيانات غير مناسبة،
أو من تخصيصات ذاكرة كثيرة، أو من قاعدة بيانات وشبكة، أو من Runtime ومكتبة لا تنفذ العملية بالكفاءة التي تتوقعها.
الفرق بين هذه الأسباب لا يظهر من قراءة الكود وحدها.
تحتاج إلى قياس: أين يذهب الوقت؟ ماذا يحدث عندما تضاعف حجم المدخلات؟ هل وحدة المعالجة مشغولة فعلًا، أم أن البرنامج ينتظر قرصًا أو شبكة أو قاعدة بيانات؟ وهل يتكرر البطء في تنفيذ
آخر
للخوارزمية نفسها؟
السؤال
الصحيح ليس: أي لغة أسرع؟ بل: أي طبقة من النظام هي التي تفرض الكلفة التي أراها
الآن؟
لا تبدأ بإدانة الخوارزمية أو اللغة قبل القياس
أول خطأ في مشاكل الأداء هو أن تختار المتهم قبل جمع البيانات.
قد ترى حلقة متداخلة فتقرر أنها السبب، ثم يكشف القياس أن معظم الزمن يضيع في استعلام قاعدة بيانات داخل الحلقة.
وقد تلوم اللغة، بينما المشكلة في تحويل بيانات متكرر يمكن حذفه.
ابدأ بزمن واضح لعملية محددة.
لا تقل إن "التطبيق بطيء" فقط؛ حدد المهمة: البحث في القائمة، إنشاء التقرير، معالجة ملف، حساب نتيجة، أو الاستجابة لطلب معين.
كلما كان القياس محددًا، أصبح التشخيص أقل عرضة للانطباعات.
بعد ذلك استخدم مدخلات تمثل الواقع.
اختبار دالة على عشرة عناصر لا يخبرك كيف ستتصرف عند مئة ألف، واختبارها على بيانات اصطناعية منتظمة قد يخفي سلوكًا يظهر في البيانات
الفعلية.
ولا تغيّر أكثر من شيء في التجربة الواحدة.
إذا بدلت اللغة والخوارزمية والمكتبة
وطريقة التخزين معًا ثم تحسن الأداء، فلن تعرف أي تغيير صنع الفرق.
الأداء مشكلة قياس قبل أن يكون مشكلة رأي.
لذلك اجعل خط البداية رقمًا، لا وصفًا عامًا.
وسجّل النتائج في جدول بسيط؛ لأن الذاكرة البشرية تميل إلى تضخيم تشغيل سريع أو بطيء شاهدته
مرة واحدة.
راقب كيف يتغير الزمن مع حجم المدخلات
أقوى علامة على وجود مشكلة خوارزمية هي طريقة نمو الزمن عندما تكبر البيانات.
إذا ضاعفت حجم المدخلات تقريبًا وبقي الزمن قريبًا من الضعف، فالسلوك مختلف عن حالة يصبح
فيها الزمن أربعة أضعاف أو أكثر.
هذا لا يعني أنك تستطيع استخراج Big O بدقة من تجربتين، لكنه يعطيك إشارة عملية.
خوارزمية تفحص كل عنصر مقابل كل العناصر الأخرى
قد تصبح مكلفة بسرعة مع زيادة الحجم، حتى لو كانت سريعة جدًا على عينة صغيرة.
جرب أحجامًا متدرجة تحت الظروف نفسها، مثل ألف وعشرة آلاف ومئة ألف عنصر، وسجل الزمن والذاكرة.
لا تكتفِ بالرقم النهائي؛ انظر إلى شكل النمو.
إذا كانت الكلفة تتسارع
مع الحجم، فابدأ بمراجعة عدد العمليات وبنية البيانات.
في المقابل، إذا بقي شكل النمو معقولًا لكن كل تشغيل أبطأ بثابت كبير، فقد تكون
المشكلة أقرب إلى كلفة التنفيذ أو المكتبة أو Runtime أو عملية خارجية بطيئة.
التعقيد الخوارزمي يشرح كيف تتغير الكلفة مع الحجم، بينما سرعة اللغة أو التنفيذ تؤثر كثيرًا في مقدار الكلفة لكل خطوة.
الفارق بين السؤالين مهم قبل اتخاذ قرار إعادة
الكتابة.
ومن المفيد أن تفصل بين اتجاه النمو والرقم المطلق.
قد تكون خوارزمية ذات تعقيد جيد أبطأ على ألف عنصر بسبب كلفة ثابتة، ثم تتفوق عند مليون عنصر.
والعكس صحيح: حل سريع على البيانات الصغيرة قد ينهار حين يكبر الحجم لأن عدد عملياته ينمو أسرع.
لذلك لا تجعل اختبارًا صغيرًا يحسم قرارًا
معماريًا كبيرًا.
بنية البيانات قد تغيّر المشكلة أكثر من اللغة
أحيانًا لا تحتاج خوارزمية "أذكى" بقدر ما تحتاج بنية بيانات تناسب العملية التي تكررها.
إذا كنت تبحث عن عنصر مرارًا داخل قائمة خطية، فقد تدفع تكلفة البحث على القائمة
في كل مرة، بينما بنية تعتمد على الفهرسة أو التجزئة قد تغير نمط الوصول
جذريًا.
المثال البسيط هو بناء مجموعة أو خريطة للعناصر التي تحتاج إلى اختبار وجودها مرارًا،
بدل إعادة فحص القائمة من بدايتها لكل عنصر جديد.
هنا لم تتغير لغة البرمجة، لكن عدد
الخطوات التي تنفذها قد ينخفض بصورة كبيرة.
وهذا لا يعني أن القواميس أو الجداول المجزأة هي الحل لكل شيء.
لها كلفة ذاكرة، وقد تتغير خصائص الأداء بحسب التوزيع وحجم البيانات وطريقة الاستخدام.
الأشجار والصفوف
والأكوام والمصفوفات لكل منها حالات تناسبها.
اقرأ ايضا : كيف تفهم مشروعًا برمجيًا قديمًا لم تشارك في كتابته؟
اسأل عن العملية المسيطرة: هل أبحث كثيرًا؟ أضيف وأحذف؟ أحتاج ترتيبًا؟ أريد أقل عنصر باستمرار؟
أكرر الوصول بحسب مفتاح؟ اختيار البنية يأتي من نمط العمل، لا من كونها
"أسرع" بصورة مطلقة.
إذا غيّرت بنية البيانات فتحسن شكل النمو مع زيادة المدخلات، فهذه قرينة قوية على أن
المشكلة كانت في تصميم الحل أكثر من اسم اللغة.
افصل CPU عن I/O والذاكرة قبل الحكم
ليس كل بطء خوارزمية حسابية.
قد يكون المعالج ينتظر قاعدة بيانات، أو ملفًا على القرص، أو استجابة شبكة.
في هذه الحالات تغيير حلقة حسابية أو الانتقال إلى لغة أسرع قد لا
يغير الزمن الذي يقضيه البرنامج منتظرًا المورد الخارجي.
راقب استخدام CPU أثناء العملية.
إذا كانت النواة أو الأنوية مشغولة بقوة لفترة طويلة، فالتنفيذ الحسابي يستحق الفحص.
أما إذا كان CPU منخفضًا
والطلب ينتظر، فابحث في I/O والاستعلامات
والأقفال والتزامن والاتصالات.
الذاكرة طبقة أخرى.
إذا كان الاستهلاك يرتفع باستمرار، لا تفترض أن السبب "تعقيد ذاكرة" خوارزمي فقط.
قد توجد كائنات لا تتحرر، أو تخصيصات مؤقتة كثيفة، أو نسخ متكرر للبيانات، أو Cache ينمو بلا
حد.
وهنا يصبح تتبع التخصيصات ولقطات الذاكرة مهمًا مثل قياس زمن الدوال.
أدوات الأداء
الحديثة تستطيع فصل وقت CPU عن الانتظار، وتكشف مواقع التخصيص وأنماط جمع القمامة في البيئات المدارة.
قبل أن تقول "اللغة بطيئة"، حدد أولًا المورد الذي نفد: وقت CPU، ذاكرة، شبكة، قرص، قاعدة بيانات،
أم تنافس بين الخيوط.
متى تكون المشكلة في التنفيذ أو الـRuntime فعلًا؟
لا يصح الانتقال إلى الطرف الآخر والقول إن اللغة لا تهم أبدًا.
بعد تثبيت الخوارزمية،
يمكن لطريقة تنفيذ اللغة وRuntime والمترجم
والمكتبات أن تحدث فرقًا واضحًا، خصوصًا في الأعمال كثيفة الحساب أو الحساسة للزمن.
قد تظهر الكلفة من تخصيص عدد هائل من الكائنات، أو ضغط جامع القمامة، أو التحويل المتكرر بين أنواع البيانات، أو استدعاءات ديناميكية كثيرة، أو حدود بين كود مُدار ومكتبة أصلية.
وفي بيئات JIT قد يتغير
الأداء بعد الإحماء والتحسين.
كذلك قد تستفيد مكتبة من كود أصلي أو تعليمات متجهية، بينما ينفذ حل آخر العملية
في طبقة تفسير أبطأ.
هنا المقارنة ليست بين "لغتين" مجردتين فقط، بل بين مسارين
تنفيذيين كاملين.
وتوجد حالات تكون فيها اللغة أو Runtime غير مناسبين لمتطلبات صارمة جدًا في التأخير أو استهلاك الذاكرة أو التحكم في الموارد.
لكن القرار يحتاج قياسًا يثبت أن الكلفة باقية بعد تحسين الخوارزمية وأنها تقع
فعلًا في طبقة التنفيذ.
السؤال
العملي: لو نفذت الخوارزمية نفسها ببنية البيانات نفسها ومدخلات مماثلة، هل يبقى
فرق الأداء كبيرًا ومستقرًا؟ إذا نعم، تبدأ حجة تغيير التنفيذ بالتحول من انطباع
إلى دليل.
كذلك لا تنس أن عبارة «مشكلة اللغة» غالبًا ما تكون أوسع مما تبدو.
قد يكون الاختناق في نسخة Runtime محددة، أو إعداد Compiler، أو مكتبة تستخدم تنفيذًا أبطأ، أو نمط برمجة لا يناسب البيئة.
تحديد هذه الطبقة بدقة
مهم لأن تحديث مكتبة أو تغيير طريقة الاستدعاء قد يعطيك المكسب المطلوب من دون نقل
النظام إلى لغة جديدة بالكامل.
استخدم Profiler لتعرف أين يذهب الوقت
قراءة الكود مهمة، لكنها ليست بديلًا عن Profiler.
أداة
تحليل الأداء تخبرك أي الدوال استهلكت CPU، وكم
مرة استُدعيت، وأين تتراكم التخصيصات أو الانتظار بحسب البيئة التي تعمل عليها.
قد تعتقد أن دالة الفرز هي المشكلة، ثم تكتشف أن 70% من الزمن داخل تحويل نصوص أو استعلام متكرر.
وقد ترى دالة صغيرة جدًا، لكنها تُستدعى ملايين المرات فتتحول إلى الاختناق
الحقيقي.
ابدأ على نسخة قريبة من إعدادات التشغيل الفعلية، لا على وضع Debug إذا كان يغير الأداء بصورة كبيرة.
واستخدم حملًا يمثل الاستخدام الحقيقي
قدر الإمكان، لأن الـProfiler على سيناريو غير ممثل سيقودك إلى تحسين شيء لا يهم المستخدم.
بعد العثور على البؤرة الساخنة، انزل طبقة واحدة: هل كثرة الاستدعاءات ضرورية؟
هل يمكنك نقل الحساب خارج الحلقة؟ هل تحتاج Cache؟ هل يوجد استعلام N+1؟
هل تنسخ البيانات بلا حاجة؟
الـProfiler لا يخبرك تلقائيًا أن "الخوارزمية خاطئة"، لكنه يحدد المكان الذي يستحق التفكير.
وهذا أفضل كثيرًا من إعادة كتابة مشروع كامل لأنك شعرت أن اللغة
بطيئة.
اختبر التحسين بBenchmark عادل لا بانطباع
بعد تعديل الخوارزمية أو التنفيذ، تحتاج إلى Benchmark يثبت أن التحسين حقيقي.
شغّل الحالة القديمة والجديدة على المدخلات نفسها والبيئة نفسها، وكرر القياس
أكثر من مرة بدل الاعتماد
على تشغيل واحد.
انتبه إلى الإحماء في البيئات التي تستخدم JIT، وإلى Cache النظام، والعمليات الأخرى على الجهاز،
وإلى أن بعض اختبارات الأداء الصغيرة
يمكن أن تقيس أشياء مختلفة عما تعتقد إذا صمم الاختبار بطريقة سيئة.
ولهذا توجد أدوات متخصصة مثل JMH في عالم JVM لتقليل أخطاء الـmicrobenchmark الشائعة.
وفي Python توجد
أدوات قياس مثل timeit إلى جانب
cProfile، ولكل أداة وظيفة مختلفة: القياس الصغير ليس
بديلًا عن Profiling تطبيق
كامل.
ولا تقارن لغتين بكود "متشابه شكليًا" فقط.
قارن حلين متكافئين وظيفيًا ويستخدمان هياكل وخوارزميات مناسبة لكل بيئة.
وإلا فقد تكون تقيس جودة التنفيذ
الذي كتبته لا خصائص اللغة.
إذا كان
التحسن لا يظهر إلا على عينة صغيرة ثم يختفي مع البيانات الحقيقية، فالمشكلة لم
تُحل بعد.
لا تعيد كتابة المشروع قبل تجربة أصغر تغيير
إعادة كتابة النظام بلغة أخرى من أغلى حلول الأداء.
قد تنقل أخطاء التصميم نفسها، وتضيف أخطاء جديدة، وتستهلك وقتًا كان يمكن أن يكشف
أن دالة واحدة أو استعلامًا واحدًا
هو الاختناق.
ابدأ بأصغر تغيير يختبر فرضيتك.
إذا كنت تظن أن البحث الخطي هو المشكلة، استبدله ببنية فهرسة في جزء معزول وقِس.
إذا كنت تشك في Runtime، ابنِ Prototype
صغيرًا للخوارزمية نفسها في التنفيذ البديل وقارن.
إذا كان الاختناق في قاعدة البيانات، حسّن الاستعلام أو الفهرس قبل تغيير لغة التطبيق.
وإذا كان في نقل البيانات، راجع حجم الحمولة وعدد الطلبات.
وإذا كان في تخصيصات
الذاكرة، خفف النسخ والكائنات المؤقتة قبل إعادة بناء النظام.
قرار إعادة الكتابة يصبح معقولًا عندما تعرف مقدار الكسب المتوقع، وتثبت أن الطبقة الحالية
هي القيد، وتوازن الكسب مع تكلفة النقل والصيانة والمخاطر.
الهدف ليس الدفاع عن اللغة التي اخترتها، ولا الانبهار بلغة أخرى.
الهدف إزالة أكبر كلفة
مؤكدة بأقل تغيير معقول.
استخدم مصفاة تشخيص الأداء قبل أن تتهم اللغة أو الخوارزمية
مرر المشكلة عبر خمس طبقات.
الأولى النمو: ماذا يحدث للزمن والذاكرة عندما تكبر المدخلات؟ النمو المتسارع يوجهك نحو الخوارزمية وبنية البيانات.
الثانية الموضع: أين يذهب الوقت فعلًا بحسب الـProfiler؟ لا تحسن الكود الذي يبدو قبيحًا
إذا لم يكن في المسار الساخن.
الثالثة المورد: هل الاختناق CPU أم ذاكرة أم I/O أم قاعدة بيانات أم شبكة أم أقفال؟
كل مورد يقود إلى نوع مختلف من الحل.
الرابعة الثبات: بعد تحسين الخوارزمية، هل يبقى فرق كبير مرتبط بـRuntime أو المكتبة أو طريقة التنفيذ؟ هنا تصبح مقارنة التنفيذات أكثر معنى.
الخامسة الكلفة: ما أصغر تغيير يختبر الفرضية؟ غيّر بنية، أو استعلامًا، أو دالة، أو أنشئ Prototype
قبل أن تقرر نقل المشروع كله.
إذا وجدت أن الزمن يتدهور بسرعة مع حجم البيانات، وأن الـProfiler يشير إلى عمليات تتكرر
بعدد كبير، وأن تغيير بنية البيانات يصلح شكل النمو، فالمشكلة خوارزمية على الأرجح.
اقرأ ايضا : لماذا تبطؤ قاعدة البيانات مع زيادة عدد السجلات؟
وإذا بقي الشكل نفسه لكن التنفيذ ما زال أبطأ بثابت
كبير وتظهر الكلفة داخل Runtime أو تخصيصات أو مكتبة بعينها، فطبقة اللغة والتنفيذ تستحق التحقيق.
المبرمج الأقوى ليس من يعرف أي لغة "أسرع" في النقاش، بل من يستطيع تحويل البطء إلى فرضية قابلة للقياس، ثم يحدد الطبقة المسؤولة قبل أن يدفع ثمن حل أكبر من المشكلة.
