timeit --- اندازه‌گیری زمان اجرای قطعه‌کدهای کوچک

کد منبع: Lib/timeit.py


این ماژول روش ساده‌ای برای زمان‌سنجی قطعه‌های کوچک کد پایتون فراهم می‌کند. این ماژول هم یک رابط خط فرمان و هم یک رابط فراخوانی‌پذیر دارد و از شماری از دام‌های رایج در اندازه‌گیری زمان‌های اجرا پرهیز می‌کند. همچنین به مقدمه‌ی Tim Peters بر فصل «Algorithms» در ویرایش دوم Python Cookbook، که توسط O'Reilly منتشر شده است، مراجعه کنید.

مثال‌های پایه

مثال زیر نشان می‌دهد که چگونه می‌توان از رابط خط فرمان برای مقایسه‌ی سه عبارت متفاوت استفاده کرد:

$ python -m timeit "'-'.join(str(n) for n in range(100))"
10000 loops, best of 5: 30.2 usec per loop
$ python -m timeit "'-'.join([str(n) for n in range(100)])"
10000 loops, best of 5: 27.5 usec per loop
$ python -m timeit "'-'.join(map(str, range(100)))"
10000 loops, best of 5: 23.2 usec per loop

این کار را می‌توان از رابط پایتون به این صورت انجام داد:

>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.3018611848820001
>>> timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
0.2727368790656328
>>> timeit.timeit('"-".join(map(str, range(100)))', number=10000)
0.23702679807320237

همچنین می‌توان یک شیء فراخوانی‌پذیر را از رابط پایتون ارسال کرد:

>>> timeit.timeit(lambda: "-".join(map(str, range(100))), number=10000)
0.19665591977536678

با این حال توجه داشته باشید که timeit() تعداد تکرارها را فقط زمانی که از رابط خط فرمان استفاده شود، به‌طور خودکار تعیین می‌کند. در بخش مثال‌ها می‌توانید مثال‌های پیشرفته‌تری پیدا کنید.

رابط پایتون

این ماژول ۳ تابع کمکی و یک کلاس عمومی را تعریف می‌کند:

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)

یک نمونه Timer با دستور داده‌شده، کد setup و تابع timer ایجاد کنید و متد timeit() آن را به تعداد number اجرا کنید. آرگومان اختیاری globals فضای نامی را مشخص می‌کند که کد در آن اجرا می‌شود.

تغییر یافته در نسخه‌ی 3.5: پارامتر اختیاری globals افزوده شد.

timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None)

یک نمونه Timer با دستور، کد setup و تابع timer داده‌شده ایجاد کنید و متد repeat() آن را با تعداد repeat و تعداد اجرای داده‌شده توسط number اجرا کنید. آرگومان اختیاری globals فضای نامی را مشخص می‌کند که کد در آن اجرا می‌شود.

تغییر یافته در نسخه‌ی 3.5: پارامتر اختیاری globals افزوده شد.

تغییر یافته در نسخه‌ی 3.7: مقدار پیش‌فرض repeat از ۳ به ۵ تغییر کرد.

timeit.default_timer()

زمان‌سنج پیش‌فرض، که همیشه time.perf_counter() است، ثانیه‌ها را به‌صورت شناور برمی‌گرداند. یک جایگزین، time.perf_counter_ns، نانوثانیه‌ها را به‌صورت عدد صحیح برمی‌گرداند.

تغییر یافته در نسخه‌ی 3.3: time.perf_counter() اکنون زمان‌سنج پیش‌فرض است.

class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None)

کلاسی برای زمان‌سنجی سرعت اجرای قطعه‌کدهای کوچک.

سازنده یک دستور برای زمان‌سنجی، یک دستور اضافی برای راه‌اندازی و یک تابع زمان‌سنج را دریافت می‌کند. هر دو دستور به‌طور پیش‌فرض 'pass' هستند؛ تابع زمان‌سنج وابسته به سکو است (رشته‌ی مستند ماژول را ببینید). stmt و setup همچنین می‌توانند شامل چندین دستور باشند که با ; یا خط جدید جدا شده‌اند، تا زمانی که شامل رشته‌های چندخطی نباشند. دستور به‌طور پیش‌فرض در فضای نام timeit اجرا می‌شود؛ این رفتار را می‌توان با ارسال یک فضای نام به globals کنترل کرد.

برای اندازه‌گیری زمان اجرای نخستین دستور، از متد timeit() استفاده کنید. متدهای repeat() و autorange() متدهای سهولت‌بخشی هستند که timeit() را چندین بار فراخوانی می‌کنند.

زمان اجرای setup از اجرای زمان‌سنجی‌شده‌ی کلی مستثنی است.

پارامترهای stmt و setup همچنین می‌توانند اشیایی را بپذیرند که بدون آرگومان فراخوانی‌پذیر هستند. این کار فراخوانی‌های آن‌ها را در یک تابع زمان‌سنج تعبیه می‌کند که سپس توسط timeit() اجرا می‌شود. توجه داشته باشید که در این حالت، سربار زمان‌سنجی به دلیل فراخوانی‌های اضافی تابع کمی بیشتر است.

تغییر یافته در نسخه‌ی 3.5: پارامتر اختیاری globals افزوده شد.

timeit(number=1000000)

زمان اجرای دستور اصلی به تعداد number را اندازه‌گیری می‌کند. این متد، دستور آماده‌سازی را یک‌بار اجرا می‌کند و سپس زمانی را که طول می‌کشد تا دستور اصلی به تعداد مشخص اجرا شود برمی‌گرداند. زمان‌سنج پیش‌فرض، ثانیه‌ها را به‌صورت یک عدد اعشاری برمی‌گرداند. آرگومان، تعداد دفعات پیمایش حلقه است که مقدار پیش‌فرض آن یک میلیون است. دستور اصلی، دستور آماده‌سازی و تابع زمان‌سنج مورد استفاده به سازنده پاس داده می‌شوند.

توجه

به‌طور پیش‌فرض، timeit() به‌طور موقت زباله‌روبی را در طول زمان‌سنجی غیرفعال می‌کند. مزیت این روش آن است که زمان‌سنجی‌های مستقل را قابل‌مقایسه‌تر می‌کند. عیب آن این است که GC ممکن است کامپوننت مهمی از عملکرد تابع در حال اندازه‌گیری باشد. در این صورت، می‌توان GC را به‌عنوان اولین دستور در رشته‌ی setup دوباره فعال کرد. برای مثال:

timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()
autorange(callback=None)

به‌طور خودکار تعیین می‌کند که چند بار timeit() فراخوانی شود.

این یک تابع کمکی است که timeit() را به‌طور مکرر فراخوانی می‌کند تا زمان کل >= ۰٫۲ ثانیه شود و در نهایت (تعداد حلقه‌ها، زمان صرف‌شده برای آن تعداد حلقه) را برمی‌گرداند. این تابع timeit() را با اعداد فزاینده از دنباله‌ی ۱، ۲، ۵، ۱۰، ۲۰، ۵۰، ... فراخوانی می‌کند تا زمانی که زمان صرف‌شده حداقل ۰٫۲ ثانیه باشد.

اگر callback ارائه‌شده باشد و None نباشد، پس از هر آزمایش با دو آرگومان فراخوانی می‌شود: callback(number, time_taken).

اضافه شده در نسخه‌ی 3.6.

repeat(repeat=5, number=1000000)

timeit() را چند بار فراخوانی کنید.

این تابعی کمکی است که timeit() را به‌طور مکرر فراخوانی می‌کند و فهرستی از نتایج را برمی‌گرداند. آرگومان اول مشخص می‌کند که timeit() چند بار فراخوانی شود. آرگومان دوم، آرگومان number برای timeit() را مشخص می‌کند.

توجه

ممکن است وسوسه شوید که میانگین و انحراف معیار را از بردار نتیجه محاسبه کنید و این مقادیر را گزارش دهید. با این حال، این کار چندان مفید نیست. در حالت معمول، کم‌ترین مقدار، کران پایینی برای سرعت اجرای قطعه‌کد داده‌شده روی ماشین شما است؛ مقادیر بالاتر در بردار نتیجه معمولاً ناشی از تغییرات سرعت پایتون نیستند، بلکه ناشی از فرایندهای دیگری هستند که در دقت زمان‌سنجی شما اختلال ایجاد می‌کنند. بنابراین، min() نتیجه احتمالاً تنها عددی است که باید مورد توجه شما باشد. پس از آن، باید کل بردار را بررسی کنید و به‌جای آمار، عقل سلیم را به کار ببرید.

تغییر یافته در نسخه‌ی 3.7: مقدار پیش‌فرض repeat از ۳ به ۵ تغییر کرد.

print_exc(file=None)

کمکی برای چاپ ردگیری پشته از کد زمان‌سنجی‌شده.

استفاده معمول:

t = Timer(...)       # outside the try/except
try:
    t.timeit(...)    # or t.repeat(...)
except Exception:
    t.print_exc()

مزیت آن نسبت به ردگیری پشته استاندارد این است که ردیفهای منبع در قالب کامپایل‌شده نمایش داده خواهند شد. آرگومان اختیاری file مشخص می‌کند که ردگیری پشته به کجا ارسال می‌شود؛ پیش‌فرض آن sys.stderr است.

رابط خط فرمان

هنگامی که به‌عنوان یک برنامه از خط فرمان فراخوانی شود، از قالب زیر استفاده می‌شود:

python -m timeit [-n N] [-r N] [-u U] [-s S] [-p] [-v] [-h] [statement ...]

در مواردی که گزینه‌های زیر قابل فهم هستند:

-n N, --number=N

تعداد دفعاتی که «دستور» اجرا می‌شود

-r N, --repeat=N

تعداد دفعات تکرار زمان‌سنج (پیش‌فرض ۵)

-s S, --setup=S

دستوری که یک بار در ابتدا اجرا می‌شود (پیش‌فرض pass)

-p, --process

اندازه‌گیری زمان فرایند، نه زمان دیواری، با استفاده از time.process_time() به‌جای time.perf_counter() که پیش‌فرض است

اضافه شده در نسخه‌ی 3.3.

-u, --unit=U

یک واحد زمانی برای خروجی زمان‌سنج مشخص کنید؛ می‌توانید nsec، usec، msec یا sec را انتخاب کنید

اضافه شده در نسخه‌ی 3.5.

-v, --verbose

چاپ نتایج خام زمان‌سنجی؛ برای دقت بیشتر در ارقام تکرار کنید

-h, --help

یک پیام کوتاه کاربرد را چاپ کرده و خارج می‌شود

یک دستور چندخطی می‌تواند با مشخص کردن هر خط به‌عنوان یک آرگومان دستور جداگانه داده شود؛ سطرهای دارای تورفتگی با قرار دادن آرگومان در علامت‌های نقل‌قول و استفاده از فاصله‌های ابتدایی امکان‌پذیر است. گزینه‌های متعدد -s نیز به‌طور مشابه رفتار می‌شوند.

اگر -n داده نشده باشد، تعداد مناسبی برای حلقه‌ها با امتحان کردن اعداد فزاینده از دنباله‌ی ۱، ۲، ۵، ۱۰، ۲۰، ۵۰، ... محاسبه می‌شود، تا زمانی که زمان کل حداقل ۰.۲ ثانیه شود.

اندازه‌گیری‌های default_timer() می‌توانند تحت تأثیر سایر برنامه‌های در حال اجرا روی همان ماشین قرار بگیرند، بنابراین بهترین کار هنگامی که زمان‌سنجی دقیق ضروری است این است که زمان‌سنجی را چند بار تکرار کنید و از بهترین زمان استفاده کنید. گزینه‌ی -r برای این کار مناسب است؛ مقدار پیش‌فرض ۵ تکرار احتمالاً در بیشتر موارد کافی است. می‌توانید از time.process_time() برای اندازه‌گیری زمان CPU استفاده کنید.

توجه

اجرای دستور pass با مقدار مشخصی سربار پایه همراه است. این کد سعی نمی‌کند آن را پنهان کند، اما شما باید از آن آگاه باشید. سربار پایه را می‌توان با فراخوانی برنامه بدون آرگومان اندازه‌گیری کرد، و ممکن است بین نسخه‌های پایتون متفاوت باشد.

مثال‌ها

می‌توان یک دستور راه‌اندازی ارائه کرد که تنها یک بار در ابتدا اجرا می‌شود:

$ python -m timeit -s "text = 'sample string'; char = 'g'" "char in text"
5000000 loops, best of 5: 0.0877 usec per loop
$ python -m timeit -s "text = 'sample string'; char = 'g'" "text.find(char)"
1000000 loops, best of 5: 0.342 usec per loop

در خروجی، سه فیلد وجود دارد. تعداد حلقه، که به شما می‌گوید بدنه دستور در هر تکرار حلقه زمان‌سنجی چند بار اجرا شده است. تعداد تکرار («بهترین از ۵») که به شما می‌گوید حلقه زمان‌سنجی چند بار تکرار شده است، و در نهایت زمانی که بدنه دستور به‌طور میانگین در بهترین تکرار حلقه زمان‌سنجی طول کشیده است. یعنی زمانی که سریع‌ترین تکرار طول کشیده است، تقسیم بر تعداد حلقه.

>>> import timeit
>>> timeit.timeit('char in text', setup='text = "sample string"; char = "g"')
0.41440500499993504
>>> timeit.timeit('text.find(char)', setup='text = "sample string"; char = "g"')
1.7246671520006203

همین کار را می‌توان با استفاده از کلاس Timer و متدهای آن انجام داد:

>>> import timeit
>>> t = timeit.Timer('char in text', setup='text = "sample string"; char = "g"')
>>> t.timeit()
0.3955516149999312
>>> t.repeat()
[0.40183617287970225, 0.37027556854118704, 0.38344867356679524, 0.3712595970846668, 0.37866875250654886]

مثال‌های زیر نشان می‌دهند که چگونه عبارت‌هایی را که شامل چند خط هستند زمان‌سنجی کنیم. در اینجا هزینه‌ی استفاده از hasattr() در مقایسه با try/except را برای بررسی نبودن یا وجود داشتن ویژگی‌های شیء مقایسه می‌کنیم:

$ python -m timeit "try:" "  str.__bool__" "except AttributeError:" "  pass"
20000 loops, best of 5: 15.7 usec per loop
$ python -m timeit "if hasattr(str, '__bool__'): pass"
50000 loops, best of 5: 4.26 usec per loop

$ python -m timeit "try:" "  int.__bool__" "except AttributeError:" "  pass"
200000 loops, best of 5: 1.43 usec per loop
$ python -m timeit "if hasattr(int, '__bool__'): pass"
100000 loops, best of 5: 2.23 usec per loop
>>> import timeit
>>> # attribute is missing
>>> s = """\
... try:
...     str.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.9138244460009446
>>> s = "if hasattr(str, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.5829014980008651
>>>
>>> # attribute is present
>>> s = """\
... try:
...     int.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.04215312199994514
>>> s = "if hasattr(int, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.08588060699912603

برای دادن دسترسی ماژول timeit به توابعی که تعریف می‌کنید، می‌توانید یک پارامتر setup که شامل یک دستور ایمپورت است ارسال کنید:

def test():
    """Stupid test function"""
    L = [i for i in range(100)]

if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test"))

گزینه‌ی دیگر، ارسال globals() به پارامتر globals است که باعث می‌شود کد در فضای نام سراسری کنونی شما اجرا شود. این کار می‌تواند راحت‌تر از مشخص کردن ایمپورت‌ها به‌صورت جداگانه باشد:

def f(x):
    return x**2
def g(x):
    return x**4
def h(x):
    return x**8

import timeit
print(timeit.timeit('[func(42) for func in (f,g,h)]', globals=globals()))