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

شرائح المحاضرة 21: توقيت البرامج وعدّ العمليات

شرائح المحاضرة 21: توقيت البرامج وعدّ العمليات

المصادر الأصلية: الشرائح الرسمية، وملف الكود الرسمي.

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

ملاحظة المترجم: الأرقام التالية هي أرقام صفحات الشرائح الأصلية، بما فيها صفحة النسبة. نُقلت مخرجات لقطات الشاشة إلى جداول بعد قراءة الأصل، ولم يُعتمد على استخراج النص وحده. بقي كود Python دون تغيير. توجد في الأصل فروق بين صيغة عدّ عمليات square في الشريحة 13 وبين العدّ الفعلي في الكود والجداول؛ حُفظت الصيغة المنشورة ووُضع توضيح مستقل عندها.

الشريحة 1: توقيت البرامج وعدّ العمليات

نزّل الشرائح وملفات .py للمتابعة.

6.100L، المحاضرة 21 — آنا بيل (Ana Bell).

الشريحة 2: كتابة برامج كفؤة

  • ركّزنا حتى الآن على صحة النتائج (Correctness). إنها أول ما ينبغي الاهتمام به! لكن هذا لا يكفي أحيانًا.
  • قد تكون المسائل شديدة التعقيد.
  • وقد تكون مجموعات البيانات ضخمة جدًّا: في عام 2014، كانت Google تخدم 30,000,000,000,000 صفحة تغطي 100,000,000 GB من البيانات.

الشريحة 3: الكفاءة مهمة

  • نفصل بين الكفاءة الزمنية (Time Efficiency) والكفاءة المكانية (Space Efficiency) للبرنامج.
  • توجد مفاضلة (Tradeoff) بينهما: يمكن استخدام قدر إضافي من الذاكرة لتخزين قيم والبحث عنها بسرعة أكبر لاحقًا.
    • فكّر في فيبوناتشي العودية (Recursive Fibonacci) مقابل فيبوناتشي مع حفظ النتائج (Memoization).
  • صعوبات فهم الكفاءة:
    • يمكن تنفيذ البرنامج بطرق مختلفة كثيرة.
    • يمكنك حل مسألة باستخدام عدد قليل فقط من الخوارزميات المختلفة.
  • نريد فصل اختيار التنفيذ (Implementation) عن اختيار الخوارزمية (Algorithm) الأكثر تجريدًا.

الشريحة 4: تقييم البرامج

  • القياس بمؤقّت (Timer).
  • عدّ العمليات (Counting Operations).
  • المفهوم المجرد لرتبة النمو (Order of Growth).

وصف المترجم للتأكيد البصري: يحيط إطار أحمر بالطريقتين الأوليين؛ فهما موضوع هذه المحاضرة، بينما يأتي مفهوم رتبة النمو لاحقًا.

الشريحة 5: استطراد بشأن الوحدات

  • الوحدة (Module) مجموعة من تعريفات Python في ملف.
    • توفر Python وحدات مفيدة كثيرة: للرياضيات، والرسم البياني، وأخذ العينات العشوائية في الاحتمالات، وأدوات الإحصاء، وغيرها.
  • عليك أولًا استيراد (Import) الوحدة إلى بيئتك:
import time
import random
import dateutil
import math
  • استدعِ الدوال داخل الوحدة باستخدام اسم الوحدة وصيغة النقطة (Dot Notation):
math.sin(math.pi/2)

الشريحة 6: التوقيت

التوقيت (Timing).

الشريحة 7: توقيت برنامج

  • استخدم وحدة time.
  • تذكّر أن الاستيراد يعني إحضار ذلك الصنف إلى ملفك.
  • ابدأ الساعة.
  • استدعِ الدالة.
  • أوقف الساعة.
import time
def c_to_f(c):
    return c*9.0/5 + 32

tstart = time.time()
c_to_f(37)
dt = time.time() - tstart
print(dt, "s,")

ملاحظة المترجم: تقول الشريحة «الصنف» (Class) عند شرح الاستيراد؛ المستورَد في هذا المثال هو وحدة time. بقي معنى العبارة الأصلية أعلاه ولم يُعدّل الكود.

الشريحة 8: توقيت c_to_f

  • الدالة سريعة جدًّا، إلى حد أننا لا نستطيع حتى توقيتها بدقة.
الاستدعاء الزمن المعروض بالثواني
c_to_f(1) 0.0
c_to_f(10) 0.0
c_to_f(100) 0.0
c_to_f(1000) 0.0
c_to_f(10000) 0.0
c_to_f(100000) 0.0
c_to_f(1000000) 0.0
c_to_f(10000000) 0.0

الشريحة 9: توقيت mysum

  • كلما زاد المدخل (Input)، زاد الزمن المستغرَق أيضًا.
  • هل يوجد نمط؟
    • من 0.009 إلى 0.05 إلى 0.5 إلى 5 إلى ماذا؟
الاستدعاء الزمن المعروض بالثواني
mysum(1) 0.0
mysum(10) 0.0
mysum(100) 0.0
mysum(1000) 0.0
mysum(10000) 0.0019927024841308594
mysum(100000) 0.009970903396606445
mysum(1000000) 0.05089521408081055
mysum(10000000) 0.4966745376586914
mysum(100000000) 5.688449382781982

وصف المترجم للتأكيد البصري: تؤطر الشريحة القيم الزمنية الخمس غير الصفرية بالأحمر لإبراز نمو الزمن مع المدخل.

الشريحة 10: توقيت square

  • كلما زاد المدخل، زاد الزمن المستغرَق أيضًا.
  • لم ينتهِ استدعاء square بالمدخل 100000 خلال زمن معقول.
  • ربما يمكننا تخمين نمط إذا صبرنا لجولة إضافية؟
الاستدعاء الزمن المعروض بالثواني
square(1) 0.0
square(10) 0.0
square(100) 0.0
square(1000) 0.06244492530822754
square(10000) 5.55335428237915

الشريحة 11: توقيت البرامج غير متسق

  • الهدف: تقييم خوارزميات مختلفة.

  • ينبغي أن يختلف زمن التشغيل (Running Time) بين الخوارزميات.

  • ينبغي ألّا يختلف زمن التشغيل بين التنفيذات.

  • ينبغي ألّا يختلف زمن التشغيل بين الحواسيب.

  • ينبغي ألّا يختلف زمن التشغيل بين اللغات.

  • ينبغي أن يكون زمن التشغيل قابلًا للتنبؤ للمدخلات الصغيرة.

  • يختلف الزمن باختلاف المدخلات، لكننا لا نستطيع حقًّا التعبير عن علاقة بين المدخلات والزمن المطلوب.

  • لا يمكن قياسه إلا بعد التنفيذ (A Posteriori).

الشريحة 12: العدّ

العدّ (Counting).

الشريحة 13: عدّ العمليات

  • نفترض أن هذه الخطوات تستغرق زمنًا ثابتًا (Constant Time):
    • العمليات الرياضية.
    • المقارنات (Comparisons).
    • الإسنادات (Assignments).
    • الوصول إلى الكائنات في الذاكرة.
  • نعدّ العمليات المنفَّذة بوصفها دالة في حجم المدخل (Input Size).

c_to_f: ثلاث عمليات

def c_to_f(c):
    return c*9.0/5 + 32

وصف المترجم لعلامات العدّ: يؤطر الأصل التعبير c*9.0/5 + 32 ويضع عليه «3 عمليات»: ضرب، وقسمة، وجمع.

mysum: 1+(x+1)*(1+2) = 3x+4 عملية

def mysum(x):
    total = 0
    for i in range(x+1):
        total += i
    return total

وصف المترجم لعلامات العدّ: total = 0 عملية واحدة. تعيين قيمة i في كل مرور عملية واحدة، وtotal += i عمليتان. تتكرر الحلقة x+1 مرة.

square: الصيغة المنشورة 1+n*(1)*n*(1+2) = 3n² + 1 عملية

def square(n):
    sqsum = 0
    for i in range(n):
        for j in range(n):
            sqsum += 1
    return sqsum

وصف المترجم لعلامات العدّ: التهيئة عملية واحدة؛ تعيين i عملية واحدة، وتعيين j عملية واحدة، وsqsum += 1 عمليتان. تتكرر كل حلقة n مرة.

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

الشريحة 14: عدّ عمليات c_to_f

  • مهما يكن المدخل، يبقى عدد العمليات نفسه.
الاستدعاء عدد العمليات معامل العدد مقارنة بالسطر السابق
c_to_f(100) 3 1.0
c_to_f(1000) 3 1.0
c_to_f(10000) 3 1.0
c_to_f(100000) 3 1.0
c_to_f(1000000) 3 1.0
c_to_f(10000000) 3 1.0

الشريحة 15: عدّ عمليات mysum

  • عندما يصبح المدخل عشرة أضعاف، يصبح عدد العمليات المنفَّذة نحو عشرة أضعاف.
الاستدعاء عدد العمليات معامل العدد مقارنة بالسطر السابق
mysum(100) 304 1.0
mysum(1000) 3004 9.88158
mysum(10000) 30004 9.98802
mysum(100000) 300004 9.9988
mysum(1000000) 3000004 9.99988
mysum(10000000) 30000004 9.99999

وصف المترجم للتأكيد البصري: تؤطر الشريحة المعامل الأخير 9.99999 بالأحمر.

الشريحة 16: عدّ عمليات square

  • عندما يصبح المدخل عشرة أضعاف، يصبح عدد العمليات نحو مئة ضعف.
الاستدعاء عدد العمليات معامل العدد مقارنة بالسطر السابق
square(1) 5 1.0
square(10) 311 62.2
square(100) 30101 96.78778
square(1000) 3001001 99.69772
square(10000) 300010001 99.96998
  • عندما يتضاعف المدخل، يصبح عدد العمليات نحو أربعة أضعاف.
الاستدعاء عدد العمليات معامل العدد مقارنة بالسطر السابق
square(128) 49281 1.0
square(256) 196865 3.99474
square(512) 786945 3.99738
square(1024) 3146753 3.99869
square(2048) 12584961 3.99935
square(4096) 50335745 3.99967
square(8192) 201334785 3.99984

وصف المترجم للتأكيد البصري: يؤطر الأصل المعاملين الأخيرين 99.96998 و3.99984 بالأحمر، لإظهار اقترابهما من 100 و4.

الشريحة 17: عدّ العمليات مستقل عن اختلافات الحواسيب، لكن…

  • الهدف: تقييم خوارزميات مختلفة.

  • ينبغي أن يختلف «زمن» التشغيل بين الخوارزميات.

  • ينبغي ألّا يختلف «زمن» التشغيل بين التنفيذات.

  • ينبغي ألّا يختلف «زمن» التشغيل بين الحواسيب.

  • ينبغي ألّا يختلف «زمن» التشغيل بين اللغات.

  • ينبغي أن يكون «زمن» التشغيل قابلًا للتنبؤ للمدخلات الصغيرة.

  • لا يوجد تعريف فعلي للعمليات التي يجب عدّها.

  • يختلف العدّ باختلاف المدخلات، ويمكن اشتقاق علاقة بين المدخلات والعدّ.

الشريحة 18: …ما زلنا بحاجة إلى طريقة أفضل

  • التوقيت والعدّ يقيّمان التنفيذات.

  • التوقيت والعدّ يقيّمان الآلات.

  • نريد تقييم الخوارزمية.

  • نريد تقييم قابلية التوسع (Scalability).

  • نريد التقييم بدلالة حجم المدخل.

الشريحة 19: النسبة وشروط الاستخدام

MIT OpenCourseWare — https://ocw.mit.edu.

6.100L: مقدمة في علوم الحاسوب والبرمجة باستخدام Python، خريف 2022.

للمعلومات المتعلقة بالاستشهاد بهذه المواد أو بشروط الاستخدام: https://ocw.mit.edu/terms.

ملحق: ملف كود المحاضرة الرسمي كاملًا دون تغيير

توضيح المترجم للتعليقات: يعرض الجزء الأول توقيت ثلاث دوال: ثابتة، وخطية لجمع 0+1+…+x، وتربيعية تحسب n*n بطريقة غير كفؤة. تعرض time_wrapper التوقيت لأحجام مدخلات مختلفة. يحذّر التعليق من تشغيل square على المدخلات الكبيرة. يعيد الجزء الثاني تعريف الدوال لإرجاع العدّ والنتيجة معًا، ثم تعرض count_wrapper عدد العمليات ونسبته إلى العدّ السابق. بقيت جميع التعليقات الأصلية داخل الكود كما هي.

import time

## -------------------------------------------------- ##
## EXAMPLE: timing a program
## -------------------------------------------------- ##

# constant fcn
def c_to_f(c):
    return c*9.0/5 + 32

# linear fcn -- finds 0+1+2+...+x
def mysum(x):
    total = 0
    for i in range(x+1):
        total += i
    return total

# quadratic fcn -- finds n*n inefficiently
def square(n):
    sqsum = 0
    for i in range(n):
        for j in range(n):
            sqsum += 1
    return sqsum

# helper function to show timing
def time_wrapper(f, L):
    print('Timing', f.__name__)
    for i in L:
        t = time.time()
        f(i)
        dt = time.time()-t
        print (f"{f.__name__}({i}) took {dt} sec")

##creates a list [1, 10, 100, ...] to test different input sizes
L_N = [1]
for i in range(8):
    L_N.append(L_N[-1]*10)

## time each function
# time_wrapper(c_to_f,  L_N)
# time_wrapper(mysum, L_N)
# time_wrapper(square, L_N)  # caution this will take 500 sec, then 50000 sec


## -------------------------------------------------- ##
## EXAMPLE: counting the number of operations
## -------------------------------------------------- ##

## constant fcn with counting the number of ops
def c_to_f(c):
    counter = 3
    return (counter, c*9.0/5 + 32)

# linear fcn  with counting the number of ops
def mysum(x):
    counter = 1
    total = 0
    for i in range(x+1):
        counter += 3
        total += i
    return (counter, total)

# quadratic fcn  with counting the number of ops
def square(n):
    counter = 1
    mysum = 0
    for i in range(n):
        counter += 1
        for j in range(n):
            counter += 3
            mysum += 1
    return (counter, mysum)

# helper function to show number of operations
def count_wrapper(f, L):
    print('Counting', f.__name__)
    for i in L:
        counter = f(i)[0]
        if i == min(L):
            multiplier = 1.0
        else:
            multiplier = counter/float(prev)
        prev = counter
        print(f"{f.__name__}({i}): {counter} ops, {round(multiplier,5)} x more")


L1 = [100]
for i in range(5):
    L1.append(L1[-1]*10)

L2_a = [128, 256, 512, 1024, 2048, 4096, 8192]
L2_b = [1, 10, 100, 1000, 10000]
# count_wrapper(c_to_f, L1)
# count_wrapper(mysum, L1)
# count_wrapper(square, L2_a)
# count_wrapper(square, L2_b)