مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 21: قياس زمن البرامج وعدّ العمليات 23 / 30 السابق التالي

النص الكامل للمحاضرة 21: توقيت البرامج وعدّ العمليات

النص الكامل للمحاضرة 21: توقيت البرامج وعدّ العمليات

المصدر الأصلي: النص الرسمي الإنجليزي، PDF، التعليقات النصية الرسمية.

النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة وتكييف عربي غير رسمي للاستخدام غير التجاري، وليست معتمدة من MIT؛ تخضع المواد الأصلية المشمولة والترجمة لرخصة CC BY-NC-SA 4.0. لا تشمل الرخصة الافتراضية مواد الأطراف الثالثة المستثناة.

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

صفحة المصدر 1

MITOCW | المحاضرة 21 من 6.100L، الإصدار 2.

[صرير]

[حفيف]

[نقرات]

آنا بيل (ANA BELL):

لنبدأ. ستكون محاضرة اليوم قصيرة جدًّا. لدينا اختبار قصير مدته 45 دقيقة عن البرمجة كائنية التوجه (Object-Oriented Programming)، والأصناف (Classes)، وما شابه. لذلك أردت أن أتيح لكم وقتًا إضافيًّا لحل ثلاث مسائل برمجية. لكن في جزء المحاضرة نفسه سنغيّر الاتجاه قليلًا. سنبدأ الحديث عن شيء أكثر نظرية: كيف نعرف إن كانت البرامج التي نكتبها كفؤة، وما مدى كفاءتها؟ حسنًا. سنفعل ذلك اليوم باستخدام فكرة توقيت برامجنا، ثم عدّ عدد العمليات، كما سأشرح بعد قليل.

لكن أولًا، قليل من الدافع. لماذا نهتم فعلًا بهذا الموضوع؟ إنه من المجالات البحثية المهمة في علوم الحاسوب. ومع ذلك، ركّزنا في هذا المقرر حتى الآن على صحة النتائج (Correctness)، أليس كذلك؟ في مجموعات المسائل، تفحص اختبارات الوحدة (Unit Tests) ما إذا كانت البرامج التي كتبتموها صحيحة. وفي الاختبارات القصيرة ننظر أساسًا إلى عدد حالات الاختبار التي تجتازونها، أليس كذلك، لتحديد الدرجة.

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

ولهذا سنركّز خلال المحاضرات الثلاث أو الأربع القادمة — لا أتذكر عددها بالضبط — لكن في الجزء القصير التالي من هذا المقرر، على فكرة تحديد كفاءة برامجنا. وعندما نتحدث عن الكفاءة (Efficiency)، يمكننا الحديث عن الكفاءة الزمنية (Time Efficiency) للبرامج، وكذلك الكفاءة المكانية (Space Efficiency) للبرامج.

وعادةً ستكون هناك مفاضلة (Tradeoff) بين الاثنتين. فمن النادر جدًّا هذه الأيام أن تتوصل إلى خوارزمية كفؤة في الزمن والمساحة معًا بالمقارنة مع الخوارزميات الموجودة بالفعل.

إذًا، عادةً توجد مفاضلة. وأفضل مثال هو ما رأيناه مع فيبوناتشي (Fibonacci). رأينا كودًا عوديًّا (Recursive) لحساب فيبوناتشي. كانت فيبوناتشي لـn تساوي فيبوناتشي لـn-1 زائد فيبوناتشي لـn-2، أليس كذلك؟ كانت هذه خطوتنا العودية. احتاج هذا البرنامج العودي إلى نحو 30 مليون خطوة لحساب فيبوناتشي لعدد في الثلاثينيات، أليس كذلك؟ 30 مليون استدعاء عودي، وهذا بطيء إلى حدٍّ ما. استغرق تشغيله بضع ثوانٍ.

لكننا رأينا بعد ذلك نسخة تستخدم حفظ النتائج (Memoization). لا، لا يوجد حرف R ناقص في الكلمة. إنها فقط Memoization، أي عملية الاحتفاظ بمذكرة (Memo)، باستخدام قاموس (Dictionary) في تلك الحالة بالتحديد. وكانت فكرة حفظ النتائج أن نأخذ بعض القيم التي نحسبها، ونخزنها في المذكرة أثناء حسابها. ففي مثال حفظ النتائج، ضحّينا بجزء من ذاكرتنا، أليس كذلك، لتخزين هذه القيم حتى لا نضطر إلى حسابها.

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

صفحة المصدر 2

إذًا، ما سنفعله في هذه المحاضرة هو أن أريكم طريقة بسيطة جدًّا لمعرفة مدى كفاءة برامجنا: سنوقّتها ببساطة. وبعد ذلك سنعدّ عدد العمليات التي تستغرقها هذه البرامج. لكننا سنفعل هذا وفي خلفية تفكيرنا فكرة أن هناك طريقة أفضل لمعرفة كفاءة هذه البرامج. وفي النهاية، لا نريد حقًّا معرفة كفاءة تنفيذ بعينه (Implementation).

معنى التنفيذ أنك تنفّذ برنامجًا يجد مجموعًا باستخدام حلقة while، وأنني أنفّذ برنامجًا يجد مجموعًا باستخدام حلقة for. هذان تنفيذان مختلفان. لكن في جوهرهما ستكون الخوارزميات التي تعمل وراء الكواليس هي نفسها. وما نريد فعله إذًا هو محاولة معرفة كيف نقيّم الخوارزميات (Algorithms)، بدلًا من هذه التنفيذات المختلفة؛ لأن كل واحد منكم سيتوصل إلى تنفيذ مختلف تمامًا في اختبار اليوم. لكنني لا أريد تقييم هذا. أود تقييم الخوارزميات التي تعمل وراء الكواليس.

حسنًا. سنفعل، كما ذكرت، أننا سننظر اليوم في قياس المدة التي يستغرقها برنامجنا باستخدام مؤقّت فعلي (Timer). وسنعدّ كذلك عدد العمليات التي يستغرقها برنامجنا. ولن ننظر في هذا المفهوم المجرد الآخر؛ سننظر فيه في المحاضرة القادمة.

في محاضرة اليوم سنستخدم وحدة أخرى (Module). كنّا ننظر بالفعل في الوحدات خلال المحاضرتين الماضيتين. رأينا وحدة random، التي تساعدنا على التعامل مع الأعداد العشوائية. ورأينا وحدة datetime، التي تساعدنا على التعامل مع — أم كانت dateutil، شيء من هذا القبيل — التي تساعدنا على التعامل مع كائنات التاريخ والوقت وتحويل التواريخ إلى كائنات سهلة الاستخدام. واليوم سنستخدم وحدة time — هنا — التي ستساعدنا على التعامل مع ساعة النظام (System Clock).

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

فإذا أردت استخدام دالة الجيب من وحدة math، فسأقول ببساطة math.sine. وعندئذ يصبح بإمكاني الوصول إلى دالة الجيب هذه. حسنًا. لنبدأ النظر في توقيت برنامج؛ أبسط طريقة لمعرفة سرعة تشغيله. سنستخدم وحدة time. إذًا أنا أستوردها هنا. وعندما أفعل ذلك، تجلب Python كل هذه الدوال المرتبطة بالوقت.

ملاحظة المترجم: الاسم المنطوق في النص الرسمي هو math.sine؛ الاسم الصحيح في Python، كما يظهر في الشرائح، هو math.sin.

سننظر في هذه المحاضرة بالتحديد في ثلاث دوال مختلفة. وسنوقّت كل واحدة منها. وفي المحاضرة القادمة سننظر في دوال أكثر بكثير، كي تحصلوا على تدريب إضافي قليل في التوقيت وعدّ العمليات. ثم سنقدم مفهومًا أكثر تجريدًا لرتبة النمو (Order of Growth). إذًا الدوال الثلاث التي سننظر فيها هي هذه: التحويل من سيلسيوس إلى فهرنهايت، وmysum، وsquare.

التحويل من سيلسيوس إلى فهرنهايت واضح إلى حد كبير. يأخذ معاملًا واحدًا (Parameter)، وهو عدد يمثل درجة حرارة بالسيلسيوس، ويحوّله إلى فهرنهايت. فعلنا هذا في المحاضرة الأولى، باستخدام الصيغة فقط. وهذه الدالة، mysum، تأخذ عددًا x — مثل 7 أو 10 أو 100 أو أيًّا يكن. وتستخدم حلقة، أي تستخدم حسابيًّا هذه الحلقة التي تمرّ على كل عدد من 0 وحتى x شاملًا، وتحتفظ بمجموع تراكمي. إذًا تضيف i إلى المجموع وتُرجعه.

صفحة المصدر 3

بالطبع كان يمكننا إعادة كتابة هذا بطريقة أكثر كفاءة باستخدام صيغة حساب المجموع: ما هي؟ n مضروبًا في n+1 مقسومًا على 2. لكننا هنا نفعله باستخدام حلقة for هذه. وأخيرًا، هذه الدالة التي اسمها square. وهذه ستكون أقل كفاءة أيضًا. سنحسب مربع n. إذًا سيُربّع المعامل n هنا.

لكننا لا نفعل return n*n ولا return n**2. لا نفعل أيًّا من ذلك. سنستخدم فعلًا حلقتين متداخلتين (Nested Loops). لدي حلقة for خارجية تمرّ على الأعداد من 0 إلى n دون شمول n. وحلقة for داخلية تمرّ على الأعداد من 0 إلى n دون شمول n. ويضيف مجموع المربع هنا 1 إلى نفسه في كل مرة. إذًا نحن، فعليًّا، نمرّ ونضيف 1 إلى ذلك المجموع n تربيع مرة. طريقة غير كفؤة إطلاقًا، لكن هذا هو المثال الذي نستخدمه.

وكيف نوقّت هذه الدوال فعلًا؟ هنا — هذه أساسًا بعض أسطر الكود في ملف. استوردت وحدة time هنا. ولدي الدالة هنا. سأستدعي وحدة time ودالة الوقت داخل وحدة datetime. وهذا يخبرني بعدد الثواني التي انقضت منذ 1 يناير 1970. وهذا يُسمى الحقبة (Epoch)، أي بداية الزمن بلغة الحاسوب.

ملاحظة المترجم: تذكر المتحدثة datetime في هذه الجملة، لكن الاستدعاء الذي تشرحه هو time.time() من وحدة time.

فإذا التقطت عدد الثواني التي مرّت منذ ذلك الوقت، فإن tstart يخزن هذا العدد من الثواني. ثم أشغّل دالتي: التحويل من سيلسيوس إلى فهرنهايت للقيمة 37. وبعد ذلك آخذ الوقت مجددًا هنا في الأسفل، وأطرح الوقت الحالي، بعد انتهاء الدالة، ناقص الوقت الذي كان قبل بدء الدالة مباشرة. وهذا يعطيني dt. ثم أطبعه فقط.

يمكننا تشغيله معًا. وسأشغّله بإجراء قليل من التقسيم إلى وحدات (Modularization) لهذا الكود. لدي هذه الدالة، وهذه هي الدالة الوحيدة التي سأشغّلها فعلًا هنا في الأسفل. إنها ما أسميه غلاف التوقيت (Time Wrapper). إنها دالة غلاف (Wrapper Function)، وتأخذ معاملين. الأول هو الدالة الفعلية التي أريد تشغيلها. وسأريكم هنا في الأسفل: ترون أنني أشغّل غلاف التوقيت مع الاسم، اسم الدالة التي أريد تشغيلها حرفيًّا. هذا ليس استدعاء دالة. إنه فقط اسم دالتي. وهذا هو المعامل الأول.

والمعامل الثاني مجموعة كبيرة من المدخلات المختلفة التي أريد تشغيل الدالة بها. إذًا تُنشأ L_N هنا في الأعلى، وتصنع لي ببساطة قائمة بكل هذه المدخلات. سأشغّل التحويل من سيلسيوس إلى فهرنهايت مع العدد 1، ثم مع العدد 10، ثم مع 100، وهكذا. ستكون هذه جميع مدخلات دالتي.

وعندما أستدعي هذا الغلاف، تستبدل Python ببساطة f بالدالة التي أشغّلها فعلًا، أي التحويل من سيلسيوس إلى فهرنهايت، أو mysum، أو square. وترون هنا أنه لكل واحد من المدخلات المختلفة، آخذ الوقت، وأشغّل الدالة، وآخذ الوقت مجددًا لأحصل على dt، ثم أطبع المدة التي استغرقتها. سأريكم كيف يبدو هذا. هنا شغّلت التحويل من سيلسيوس إلى فهرنهايت مع المدخلات 1 و10 و100 و1,000 و10,000، وهكذا.

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

حسنًا، ماذا عن الدالة التالية؟ لنجرّب mysum. لا تنفّذ mysum حسابات فقط، بل فيها حلقة تعتمد على المدخل. إذًا يتغيّر مدخلنا. وربما لاحظتم أنه كلما كبر المدخل، اضطررنا فعلًا إلى الانتظار قليلًا حتى تظهر النتيجة. نرى هنا في الأسفل، أو في الأعلى، أنه عندما يكون المدخل صغيرًا إلى حدٍّ ما، نعم، استغرقت صفر ثانية. إنها سريعة إلى حد أن الزمن لم يُسجَّل حتى.

صفحة المصدر 4

لكننا بدأنا في مرحلة ما نحصل على أعداد فعلية. مع 10,000 استغرقت 0.00099 ثانية. ومع 100,000 استغرقت 0.01. ومع — ما هذا، مليون واحد؟ نعم — مليون واحد استغرقت 0.05 ثانية. يمكننا فعلًا رؤية نمط صغير إذا نظرنا طويلًا بما يكفي، خصوصًا مع الأعداد الأكبر. هنا في الأسفل، هاتان القيمتان الأوليان غير واضحتين تمامًا. لكن عندما نصل إلى عدد كبير مثل مليون واحد، نقول إنها استغرقت 0.05 ثانية.

عندما نجعل المدخل عشرة أضعاف، ليصبح عشرة ملايين، استغرقت معالجة المدخل 0.5 ثانية. وعندما نجعل المدخل عشرة أضعاف مرة أخرى، استغرقت 5 ثوانٍ. إذًا يمكننا تخمين أنها عندما يصبح المدخل عشرة أضعاف مجددًا، ستحتاج إلى نحو 50 ثانية لتعمل. ويمكنكم حتى تجربة ذلك إذا أردتم الانتظار 50 ثانية. هذه هي دالة mysum.

ماذا عن square الآن؟ تذكّروا أن square كان فيها حلقتا for متداخلتان: for ثم for، ثم جمع عادي فقط بداخلهما. لنشغّلها. سريعة إلى حدٍّ ما. سريعة إلى حدٍّ ما. مربع 1,000 يستغرق بالفعل 0.05 — أو 0.06 — ثانية. ومربع 10,000 يستغرق الآن 6 ثوانٍ. ماذا نلاحظ؟

مع جولة إضافية، لو انتظرنا حساب مربع 100,000، لربما رأينا نمطًا، أو يمكننا تخمين النمط. هل يريد أحد أن يخمّن العدد التالي الذي ينبغي أن يكون هنا، إذا فكّرتم في الأمر؟

أحد الحضور (AUDIENCE):

حوالي 600.

آنا بيل (ANA BELL):

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

والملاحظة الأخرى أنه عند 10,000 بالفعل، عندما يكون المدخل مجرد 10,000، استغرق هذا 5 ثوانٍ بالفعل. في الدالة السابقة هنا، mysum، كان علينا أن نصل إلى 100 مليون كمدخل كي تعمل لمدة 5 ثوانٍ. وهذا فرق كبير أيضًا. هذا البرنامج، square، يستغرق وقتًا طويلًا جدًّا بالفعل، حتى عندما لا يكون المدخل كبيرًا جدًّا.

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

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

مثلًا، قد تستخدم حلقة for بدلًا من حلقة while. وقد يتضمن التنفيذ إنشاء متغيرات وسيطة. أو قد يكون لديك تنفيذ يستخدم بناء القائمة المختصر (List Comprehension). لكن وراء كل ذلك توجد خوارزمية ما تحاول تنفيذها.

إذًا سيختلف زمن التشغيل بين التنفيذات المختلفة، وهذا ليس حقًّا ما أريده. وسيختلف زمن التشغيل أيضًا بين الحواسيب. إذا شغّلت البرامج نفسها على حاسوب أقدم، فربما لن تستغرق 5 ثوانٍ مع مدخل مقداره 100 مليون. ربما تستغرق 10 أو 11. إذًا سيختلف التوقيت أيضًا بين الحواسيب المختلفة.

صفحة المصدر 5

وسيختلف أيضًا بين اللغات المختلفة: Java مقابل Python مقابل C. إذا كانت C شديدة الكفاءة في إدارة الذاكرة، فستعمل بسرعة كبيرة. أما إذا كانت Python أبطأ قليلًا، فستعمل ببطء أكبر. إذًا، مجددًا، نحن نلتقط فعلًا — التوقيت يلتقط — الاختلافات بين اللغات.

والتوقيت ليس قابلًا للتنبؤ جيدًا مع المدخلات الصغيرة. فإذا كنت لسبب ما، عندما شغّلت دالة square هنا بالمدخل 1، أشغّل Netflix أيضًا في الخلفية، أو قرر حاسوبي تحديث شيء ما، وقرر تخصيص الموارد لتنفيذ تلك المهمة في اللحظة التي أحاول فيها تشغيل مربع 1، فقد لا يكون هذا الزمن 0.0 ثانية. قد تنتقص المهمة من الوقت الذي يخصصه لتشغيل برنامج المربع. وما سنراه حينئذ أن هذا لن يكون 0.0 بعد الآن. قد يصبح 0.1 أو شيئًا من هذا القبيل.

إذًا توقيت البرامج ليس جيدًا جدًّا. إنه غير متسق كثيرًا مع هدفنا هنا، وهو تقييم الخوارزميات. لنرَ إن كان يمكننا تحسين ذلك بفكرة عدّ عدد العمليات. فبدلًا من التركيز على وصف برنامجنا بدلالة الزمن البشري، ثانية واحدة أو نصف ثانية وما شابه، لنحدد عمليات في Python تستغرق وحدة زمنية واحدة. وسنقول إن كل هذه العمليات الأساسية جدًّا — يمكننا القول — تستغرق الزمن نفسه. لا يهمني إن كانت تستغرق عشرة أس سالب تسعة ثانية، أو اثنين مضروبًا في عشرة أس سالب تسعة ثانية. لا أهتم بذلك. أعرف فقط أنها سريعة جدًّا.

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

مقارنة شيء بشيء: مثل a أصغر من b، أو 3 أكبر من 4، وما شابه، والمساواة أيضًا؛ كلها سريعة جدًّا، وتستغرق وحدة زمنية واحدة أيضًا. وإسناد شيء: مثل a = 3. تعليمة الإسناد (Assignment Statement) هذه سريعة أيضًا، وتستغرق وحدة زمنية واحدة. ثم الوصول إلى الكائنات في الذاكرة: سريع أيضًا، ويستغرق وحدة زمنية واحدة.

إذًا مع هذا التعريف الجديد لـ«الزمن»، بين علامتي اقتباس، حيث لدينا وحدات الزمن هذه، لنعرف كم تستغرق هذه الدوال فعلًا. دالة التحويل من سيلسيوس إلى فهرنهايت فيها ثلاث عمليات: ضرب، وقسمة، وجمع. لا تهمني — أي الفروق الصغيرة في الزمن الذي تستغرقه كل واحدة منها فعليًّا داخل ذاكرة الحاسوب — سأقول إن برنامج التحويل من سيلسيوس إلى فهرنهايت يستغرق ثلاث وحدات زمنية.

إذًا مهما يكن المدخل، سواء حوّلت صفر سيلسيوس أو مليون سيلسيوس، سيستغرق البرنامج ثلاث وحدات زمنية فقط حتى يكتمل. ماذا عن mysum؟ سنمرّ عليها خطوة بخطوة. في mysum لدي تعليمة إسناد واحدة هنا، فهذه عملية واحدة. حلقة for هنا تأخذ i وتسند إليه إحدى القيم في النطاق. هذا ببساطة ما تفعله داخليًّا. إذًا ستكون هذه عملية واحدة في كل مرور عبر الحلقة.

ثم total += i عمليتان؛ لأن لدي total + i في الجانب الأيمن. هذه عملية واحدة. ثم إسناد ذلك مجددًا إلى total هو العملية الثانية. إذًا هنا عمليتان، وهذا كل شيء. لكن لاحظوا حلقة for: هذه العمليات الثلاث — الواحدة لإسناد قيمة هنا إلى i، وهاتان العمليتان — تتكرر x+1 مرة. من 0 وحتى x. هذا إجمالي x+1 مرة.

صفحة المصدر 6

كم يستغرق هذا البرنامج فعلًا إذًا؟ نعدّ كل ذلك. الواحدة من أجل total = 0، زائد — ونضرب x+1 في ماذا؟ في 1+2، وهذا يعطينا إجمالي 3x+4 عملية. الآن نسجّل هذا بدلالة المدخل، وهذا جميل إلى حدٍّ ما. لدينا الآن هذه الصيغة الصغيرة اللطيفة: إذا عرفت أن المدخل 10، يمكنني فعلًا أن أخبرك بعدد «الوحدات الزمنية»، بين علامتي اقتباس، التي سيستغرقها هذا البرنامج.

ماذا عن square؟ ستكون مشابهة جدًّا. لدي عملية واحدة للإسناد هنا. وهذه عملية واحدة لأخذ i وجعله إحدى القيم في النطاق. وبالمثل في الحلقة الداخلية: عملية واحدة هناك. ثم زيادة مجموع المربع بمقدار 1 عمليتان للسبب نفسه: واحدة للجانب الأيمن الذي ينفّذ الجمع، والثانية لتنفيذ الإسناد.

لا ننسى حلقات for. لدينا حلقتا for هنا. الحلقة الداخلية تتكرر n مرة. ولكل مرة من المرات الخارجية الـn، ننفّذ المرات الداخلية الـn. هذه هي حالة حلقتي for المتداخلتين هنا. إذًا إجمالي الوحدات الزمنية التي تستغرقها square هو الواحدة هنا من أجل إسناد 0 إلى مجموع المربع، زائد — ثم لدي هاتان الحلقتان المتداخلتان. تمرّ الأخرى n مرة — عفوًا، n مضروبًا في العملية الواحدة — مضروبًا في الحلقة الداخلية، وهي أيضًا n مرة، مضروبًا في ماذا؟ ما العمليات المنفَّذة في الحلقة الداخلية؟ إنها هذه الواحدة زائد هاتين الاثنتين. إذًا 1+2. والإجمالي 3n²+1 عملية.

ملاحظة المترجم: هذه هي الصيغة التي تقولها المتحدثة وتكتبها الشريحة. لكن العدّ في ملف Python الرسمي يحتسب تعيين i في كل دورة خارجية أيضًا، ويعطي 3n2+n+13n^2+n+1، كما في الجداول اللاحقة. حُفظت صيغة الكلام الأصلي أعلاه.

لنشغّل هذا. الآن، بما أننا نعدّ العمليات، ينبغي أن نرى نمطًا أفضل. هذه دالة التحويل من سيلسيوس إلى فهرنهايت. تغيّرت mysum وsquare قليلًا؛ لأن لدي هذا المتغير الصغير counter داخل كل دالة. وهو يزداد كلما رأيت عملية. ومن الواضح أنه في التحويل من سيلسيوس إلى فهرنهايت يكون 3 دائمًا. عندما أنفّذ return سأرجع متغير العدّاد، ثم الشيء العادي الذي ينبغي أن ترجعه الدالة، معًا في صفّ (Tuple).

في mysum، يمثل counter = 1 تعليمة الإسناد هذه. ثم في كل مرور عبر الحلقة، أزيد عدّادي للعمليات الثلاث: إسناد إحدى القيم في النطاق إلى i، ثم عمليتين إضافيتين من أجل total += i. إذًا يزداد هذا في كل مرور عبر الحلقة. وبالمثل في square: هنا counter = 1 لهذه التعليمة. وcounter += 1 من أجل أخذ i ليكون إحدى هذه القيم. ثم counter += 3 من أجل أخذ j ليكون إحدى هذه القيم، وزيادة mysum هذا.

وبسبب مواضع العدّادات التي وضعتها، ستجمع Python ذلك كله تلقائيًّا مهما يكن عدد الحلقات. هذا هو غلافي للعدّ. وهو مختلف قليلًا عن غلاف التوقيت؛ لأنني الآن سأتتبع أيضًا مقدار زيادة عدد العمليات التي نفذتها مقارنة بالمدخل السابق. دعوني أريكم ماذا يعني هذا. لنشغّل التحويل من سيلسيوس إلى فهرنهايت مع المدخلات التالية.

أولًا، أعرض إجمالي عدد العمليات كما فعلت مع التوقيت. دائمًا ثلاث عمليات. لا مفاجأة في ذلك. هذا أساسًا ما كتبناه في الكود. لكنني أعرض هنا أيضًا — ويحدث هذا داخل دالة الغلاف، غلاف العدّ count_wrapper — كم زاد عدد هذه العمليات استنادًا إلى السابقة. الأولى غريبة قليلًا. لكن عندما يصبح مدخلي عشرة أضعاف، وانتقلت من 100 إلى 1,000، نفّذت عملية إضافية واحدة. لا تغيير، بالطبع؛ لأن إجمالي العمليات دائمًا ثلاث.

ملاحظة المترجم: عبارة «عملية إضافية واحدة» زلة في الكلام الأصلي؛ المعروض هو معامل 1.0، أي إن عدد العمليات لم يزد، كما تؤكد الجملة التالية والجداول.

صفحة المصدر 7

ومن أجل الاكتمال فقط، هذه هي الشريحة. مهما حدث للمدخل هنا، يظل عدد العمليات، بوحدات الزمن هذه التي نعدّ فيها العمليات فقط، هو 3. ماذا عن المجموع؟ تذكّروا أن المجموع كان فيه حلقة for. لنشغّله ونرَ عدد العمليات هنا. أولًا أعرض عدد العمليات. عندما يكون المدخل 100 يكون العدد 304. وعندما يكون المدخل 1,000 يكون 3,004. وعندما يكون المدخل 10,000 يكون 30,004. هذا يطابق الصيغة التي توصلنا إليها: 3x+4. وهذا جميل إلى حدٍّ ما.

ثم ترون هنا أنني أعرض الآن كم زاد عدد العمليات في هذا السطر مقارنة بالسطر السابق. إنه نحو 9.8 ضعف. فعندما يصبح مدخلي عشرة أضعاف، من 100 إلى 1,000، أنفّذ عمليات أكثر بنحو 9.88 ضعف. وعندما يزيد المدخل من 1,000 إلى 10,000، أي عشرة أضعاف أيضًا، أنفّذ عمليات أكثر بمقدار 9.988 ضعف.

إذًا نرى هنا حالة مستقرة (Steady State) لطيفة: عندما يكبر مدخلي جدًّا جدًّا، يبدو أنني أقترب من تنفيذ عدد عمليات أكبر بنحو عشرة أضعاف، عندما يكون مدخلي أكبر بعشرة أضعاف. وهذا أوضح بالطبع عندما يكون المدخل كبيرًا؛ لأن الفروق الصغيرة في الصيغة — وتحديدًا +4 — يكون أثرها أقل عندما يكون المدخل كبيرًا جدًّا. وهذا يتوافق إلى حدٍّ ما مع الدافع الذي بدأنا به.

عندما تكون بيانات المدخل كبيرة جدًّا جدًّا، أود وصف الخوارزمية والمدة التي تستغرقها. لا يهمني أن الخوارزمية تستغرق 3x+4، أو 3x مضروبًا في 3x من العمليات. عندما يكون المدخل كبيرًا جدًّا، كل ما يهمني أنها نوعًا ما من رتبة x. وهذا ما سنصل إليه في المحاضرة القادمة. لكن هذه هي الفكرة الكبرى هنا: عندما يصبح المدخل عشرة أضعاف، يبدو أنه عند الحالة المستقرة يصبح عدد عملياتنا عشرة أضعاف أيضًا. إذًا هذه علاقة خطية (Linear Relationship).

ماذا عن الدالة الأخيرة، square؟ أفعل هنا شيئًا خاصًّا قليلًا. لدي مجموعتان مختلفتان من المدخلات سأشغّل square بهما. الأولى هي L2_a. سأشغّل square مع المدخلات 128 و256 و512 و1024 — أي أزيد مدخلي بمعامل 2 أساسًا. أضرب المدخل في 2 كل مرة. ثم أشغّلها مع L2_b، حيث يزيد المدخل بمعامل 10 في كل مرة.

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

عدد العمليات عندما يتضاعف مدخلي ليس مهمًّا جدًّا. نعم، الأعداد كبيرة. لكن ما يهمني فعلًا، تمامًا كما رأينا في مثال mysum، هو ما يحدث عند الحالة المستقرة بينما يكبر المدخل جدًّا. كم يزيد عدد العمليات التي ننفّذها؟ ما نراه أن عدد العمليات، عندما يكبر المدخل جدًّا، يكون أكبر بنحو أربعة أضعاف، في الحالة التي أضاعف فيها المدخل في كل جولة.

إذًا عندما أضاعف المدخل، يصبح عدد العمليات أربعة أضعاف. وماذا عندما أزيد المدخل بمعامل 10؟ 1 و100 و1,000، وهكذا. مجددًا، لا أهتم كثيرًا بعدد العمليات نفسه، بل بما يحدث عند الحالة المستقرة. مع عدد قليل جدًّا من العمليات يصعب التحديد. لكن مع الزيادة نرى أنه يتجه نحو 100 تقريبًا. إذًا عندما يصبح المدخل عشرة أضعاف، يؤدي ذلك إلى زيادة عدد العمليات إلى نحو مئة ضعف.

صفحة المصدر 8

هل تستطيعون الآن رؤية العلاقة بين مدخل square وعدد العمليات؟ تستطيعون، أليس كذلك؟ إنها تقريبًا علاقة من نوع n تربيع. عندما يكون المدخل n، يكون عدد العمليات من رتبة n².

إذًا عدّ العمليات أفضل كثيرًا من التوقيت، كما نرى. لقد تخلصنا من مجموعة من علامات X الحمراء هذه. لم نعد مضطرين للتعامل مع الاختلافات بين الحواسيب؛ لأننا إذا عددنا هذا على حاسوب بطيء أو سريع، فإننا لا نزال نعدّ المقدار نفسه. واللغات، مجددًا، لن يكون لها تأثير؛ لأنكم ستنفّذونه بطريقة مشابهة. ما زالت المدخلات الصغيرة غير واضحة تمامًا. رأينا أن square كانت غير قابلة للتنبؤ قليلًا عندما كان المدخل صغيرًا جدًّا، هنا في الأسفل: 60 ثم مباشرة إلى 90.

لكن لم نحتج إلى وقت طويل لنرى الحالة المستقرة. إذًا هي أفضل فعلًا من السابق، أفضل من التوقيت. على الأقل ليست 0. لكن المشكلة الآن تصبح نوعًا ما: ما تعريف العمليات التي يجب عدّها؟ لاحظوا أن دوالنا لها قيمة إرجاع (Return Value). هل نعدّ return عملية؟ من الناحية التقنية ينبغي أن نفعل. إنها قيمة تُمرَّر بين الدوال، لذا سيستغرق تشغيل ذلك بعض الوقت.

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

لكن، مجددًا، الفكرة الكبرى هنا أننا نحاول الوصول إلى تقييم عدد قليل فقط من الخوارزميات المختلفة: ما رتبة النمو بينما يكبر المدخل جدًّا جدًّا؟ لأن كل ما يهمنا هو مدى قابلية توسع (Scalability) هذه البرامج التي نشغّلها عندما يكون المدخل كبيرًا جدًّا، وعندما نتعامل مع البيانات الضخمة (Big Data). وهذا ما سنبدأ الحديث عنه في المحاضرة القادمة.