شرائح المحاضرة 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مرة.
ملاحظة المترجم بشأن فرق في الأصل: الصيغة المكتوبة في الشريحة هي ؛ لكن احتساب تعيين
iمرة لكل دورة خارجية، كما تفعل نسخة الكود الرسمية، يعطي . القيم في الشريحة 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)