قِس أولًا ثم حسّن
اختبار رجعي يستغرق أربع ساعات أو دالة تسعير تُستدعى مليون مرة في الثانية تجعل الجميع يريد التسريع، ويخطئ معظمهم في تخمين أين يذهب الوقت. فقد تكون الحلقة التي تبدو ثقيلة لا شيء، وقد يكون سطر بريء المظهر هو التشغيل كله. وتحليل الأداء (profiling) هو قياس أين ينفق البرنامج وقته قبل تغيير أي شيء. والقاعدة بسيطة: لا تحسّن سطرًا لم تقِسه، وبعد التغيير قِس من جديد لترى هل أفاد.
# The simplest measurement: read a clock before and after
import time
def slow_sum(n):
total = 0
for i in range(n):
total += i * i
return total
start = time.perf_counter()
result = slow_sum(200_000)
elapsed = time.perf_counter() - start
print(result) # 2666646666700000
print(elapsed > 0) # True a real duration; the exact value changes every runالضجيج والتكرار والحد الأدنى
القياس الواحد غير موثوق: نظام التشغيل وبرامج أخرى وذاكرة المعالج المؤقتة وبدء بايثون كلها تضيف ضجيجًا ولا تضيف إلا وقتًا. لذا قِس الدالة عدة مرات وخذ الحد الأدنى، أي التشغيل الأقل تشويشًا؛ وهذا ما تفعله timeit. واستعمل time.perf_counter() (ساعة رتيبة عالية الدقة) لا time.time(). وقِس بأحجام مدخلات واقعية، فقد تكون دالة سريعة على عشرة صفوف بطيئة على عشرة ملايين، وسخّن أولًا حتى لا تُحتسب التكاليف لمرة واحدة (الاستيراد، الذاكرة المؤقتة) على الشيفرة المقيسة.
# Repeat and take the minimum: the least disturbed run
import time
def work():
return sum(i * i for i in range(50_000))
def best_of(func, repeats):
best = None
for _ in range(repeats):
start = time.perf_counter()
func()
elapsed = time.perf_counter() - start
if best is None or elapsed < best:
best = elapsed
return best
runs = [best_of(work, 1) for _ in range(5)]
print(best_of(work, 5) <= max(runs)) # True the minimum is never above the worst single run
print(min(runs) > 0) # Trueأين يذهب الوقت: المحلّلات وقانون أمدال
يخبرك المؤقّت كم، ويخبرك المحلّل (profiler) أين. تسجّل cProfile في المكتبة القياسية كل نداء دالة وتبلّغ لكل دالة عن عدد النداءات والزمن المنفق في الدالة نفسها والزمن التراكمي بما نادته؛ وpython -m cProfile -s cumtime script.py يطبع الأسوأ أولًا. وحصة كل دالة هي ببساطة زمنها على الإجمالي. ومكسب إصلاح دالة تحدّه قاعدة أمدال: تسريع جزء f من التشغيل بمعامل s يعطي إجمالًا 1 / ((1 - f) + f / s). فجعل 60% من التشغيل أسرع عشر مرات يعطي 2.2 مرة فقط، وحتى نصف سريع بلا حدود يحد المكسب بـ 2. ابدأ بأكبر نقطة ساخنة.
# Shares of the total and the payoff of fixing the biggest one (the timings are made-up numbers)
timings = {"load_prices": 6.0, "price_options": 3.0, "write_report": 1.0}
total = sum(timings.values())
for name, seconds in sorted(timings.items(), key=lambda item: -item[1]):
print(f"{name:14s} {seconds / total:5.1%}") # load_prices 60.0%, price_options 30.0%, write_report 10.0%
def amdahl_gain(fraction, speedup):
return 1 / ((1 - fraction) + fraction / speedup)
print(round(amdahl_gain(0.6, 10), 2)) # 2.17 ten times faster on 60% of the run
print(round(amdahl_gain(0.1, 1000), 2)) # 1.11 a huge win on the smallest part barely helps# cProfile records every call; the function that sleeps shows up as the hotspot
import cProfile
import time
def slow_part():
time.sleep(0.02)
def fast_part():
return sum(range(10))
def run_all():
slow_part()
for _ in range(100):
fast_part()
profiler = cProfile.Profile()
profiler.runcall(run_all)
entries = {str(entry.code): entry for entry in profiler.getstats()}
slow = next(entry for name, entry in entries.items() if "slow_part" in name)
fast = next(entry for name, entry in entries.items() if "fast_part" in name)
print(fast.callcount, slow.callcount) # 100 1 calls are counted exactly
print(slow.totaltime > fast.totaltime) # True one slow call outweighs 100 fast ones- قِس قبل تغيير أي شيء؛ استعمل
perf_counterوكرّر واحتفظ بالحد الأدنى بأحجام مدخلات واقعية. - يُظهر المحلّل (
cProfile) أين يذهب الوقت: النداءات والزمن الذاتي والتراكمي لكل دالة؛ وحصة كل منها زمنها على الإجمالي. - قاعدة أمدال: تسريع جزء f بمعامل s يعطي 1 / ((1 - f) + f / s)؛ أصلح أكبر نقطة ساخنة أولًا وأعد القياس بعد كل تغيير.
التالي: تحسين الحلقات الساخنة، التغييرات التي تسرّع الشيفرة التي أشار إليها المحلّل.
المختبر: ما الذي يُقيَّم
- قِس زمن نداء واحد باستخدام
time_call25 نقطة - خذ أفضل تشغيل من عدة تشغيلات باستخدام
best_of25 نقطة - اعثر على أين يذهب الوقت باستخدام
hotspots25 نقطة - توقّع المكسب باستخدام
amdahl_gain25 نقطة