المستوى: مبتدئ. هذا المقال موجَّه لمن يعرف كتابة دالة بسيطة ويريد أن يتعلّم أول تقنية تحسين حقيقية في حياته البرمجية. لا يحتاج أي خبرة سابقة في الخوارزميات أو في تحليل التعقيد.
Memoization: تخزين نتيجة الدالة بدل إعادة حسابها
دالة fib(35) المكتوبة بالطريقة العادية تأخذ 2.1 ثانية وتنفّذ 29,860,703 استدعاء. نفس الدالة بعد إضافة Memoization تأخذ 0.03 مللي ثانية و69 استدعاء فقط. هذا المقال يشرح سبب هذا الفرق الضخم، وكيف تطبّق التقنية بنفسك في دقائق.
المشكلة باختصار
دوال كثيرة تحسب نفس القيمة عشرات أو ملايين المرات بدون أن تنتبه. كل مرة تعيد عملًا تم إنجازه من قبل. ما دامت البيانات صغيرة لن تلاحظ شيئًا. لكن عندما تكبر، يتحول هذا الهدر من مللي ثانية إلى ثوانٍ، ثم إلى دقائق. Memoization يحل هذا بفكرة واحدة: إذا حسبت نتيجة من قبل، خزّنها وأعدها بدل أن تحسبها مرة أخرى.
الفكرة بمثال بسيط: موظف الاستقبال وكرّاسة الأرقام
تخيّل موظف استقبال في شركة كبيرة. كل قليل يسأله أحدهم: "رقم تحويلة قسم المبيعات كام؟". في المرة الأولى يقوم، يبحث في الدليل، يأخذ دقيقة كاملة، ثم يعود بالرقم. لو سأله 50 شخصًا نفس السؤال، سيبحث 50 مرة ويهدر 50 دقيقة على إجابة واحدة.
الموظف الذكي يفعل شيئًا بسيطًا. في المرة الأولى يبحث، لكنه يكتب الرقم في كرّاسة صغيرة على مكتبه. في المرة التالية ينظر في الكرّاسة ويجيب في ثانية. هذه الكرّاسة هي Memoization بالضبط: ذاكرة جانبية صغيرة تحفظ الإجابات الجاهزة بدل تكرار البحث.
تعريف Memoization بدقة
Memoization تقنية تحسين تُخزّن نتيجة استدعاء دالة، وتُرجع النتيجة المخزّنة فورًا عندما تتكرر نفس المدخلات. المصطلح صاغه الباحث Donald Michie سنة 1968 في ورقته عن دوال المذكرة وتعلّم الآلة. الكلمة مشتقة من "memo" بمعنى مذكرة، وليست خطأ إملائيًا لكلمة memorization.
لها شرط أساسي واحد: الدالة يجب أن تكون دالة نقية (Pure Function). الدالة النقية تعطي نفس المخرج لنفس المدخل دائمًا، وليس لها أي تأثير جانبي. مثال بسيط: دالة تجمع رقمين دالة نقية، فـ add(2, 3) ترجع 5 في كل مرة. لكن دالة ترجع الوقت الحالي ليست نقية، لأن مخرجها يتغيّر في كل استدعاء. الافتراض الذي يقوم عليه هذا الشرح: الدالة التي تخزّنها نقية، ومدخلاتها تتكرر فعلًا.
الكود: من 2.1 ثانية إلى جزء من المللي ثانية
لنأخذ متتالية فيبوناتشي. الطريقة المباشرة تبدو نظيفة لكنها فخ:
# الطريقة العادية: تعيد نفس الحساب ملايين المرات
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
# fib(35) = 9227465
# القياس الفعلي: ~2.1 ثانية، 29,860,703 استدعاء
الآن نفس الدالة مع كرّاسة (قاموس) تحفظ كل نتيجة محسوبة: