timeit --- 測量小量程式片段的執行時間

原始碼:Lib/timeit.py


該模組提供了一種對少量 Python 程式碼進行計時的簡單方法。它有一個命令列介面和一個可呼叫介面,它避免了許多測量執行時間的常見陷阱。另請參閱由 O'Reilly 出版的 Python 錦囊妙計 (Python Cookbook) 第二版中 Tim Peters 所寫的「演算法」章節的介紹。

基礎範例

以下範例展示了如何使用命令列介面來比較三個不同的運算式:

$ 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

這可以透過 Python 介面來實現:

>>> 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

也可以在 Python 介面傳遞可呼叫物件:

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

但請注意,僅當使用命令列介面時 timeit() 才會自動確定重複次數。在範例章節中有更進階的範例。

Python 介面

該模組定義了三個便利函式和一個公開類別:

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

使用給定的陳述式、setup 程式碼和 timer 函式建立一個 Timer 實例,並執行其 timeit() 方法 number 次。可選的 globals 引數指定會在其中執行程式碼的命名空間。

在 3.5 版的變更: 新增 globals 選用參數。

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

使用給定的陳述式、setup 程式碼和 timer 函式建立一個 Timer 實例,並使用給定的 repeat 計數和 number 來運行其 repeat() 方法。可選的 globals 引數指定會在其中執行程式碼的命名空間。

在 3.5 版的變更: 新增 globals 選用參數。

在 3.7 版的變更: repeat 的預設值從 3 更改為 5。

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';計時器函式會與平台相依(請參閱模組文件字串 (doc string))。stmtsetup 還可以包含由 ; 或換行符號分隔的多個陳述式,只要它們不包含多行字串文字即可。預設情況下,該陳述式將在 timeit 的命名空間內執行;可以透過將命名空間傳遞給 globals 來控制此行為。

要測量第一個陳述式的執行時間,請使用 timeit() 方法。repeat()autorange() 方法是多次呼叫 timeit() 的便捷方法。

setup 的執行時間不包含在總體運行計時中。

stmtsetup 參數還可以接受無需引數即可呼叫的物件。這會把對它們的呼叫嵌入到計時器函式中,然後由 timeit() 去執行。請注意,在這種情況下,因有額外的函式呼叫,時間開銷 (timing overhead) 會稍大一些。

在 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, target_time=None)

自動決定呼叫 timeit() 次數。

This is a convenience function that calls timeit() repeatedly so that the total time >= Timer.target_time seconds, returning the eventual (number of loops, time taken for that number of loops). It calls timeit() with increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the time taken is at least target_time seconds.

如果有給定 callback 且不是 None,則每次試驗後都會使用兩個引數來呼叫它:callback(number, time_taken)

在 3.6 版被加入.

在 3.15 版的變更: The optional target_time parameter was added.

repeat(repeat=5, number=1000000)

呼叫 timeit() 數次。

這是一個方便的函式,它會重複呼叫 timeit() 並回傳結果列表。第一個引數指定呼叫 timeit() 的次數,第二個引數指定 timeit()number 引數。

備註

人們很容易根據結果向量來計算出平均值和標準差並將其作為依歸,然而這並不是很有用。在典型情況下,最低值給出了機器運行給定程式碼片段的速度的下限;結果向量中較高的值通常不是由 Python 速度的變化所引起,而是由干擾計時精度的其他行程造成的。因此,結果中的 min() 可能是你應該感興趣的唯一數字。在解讀該數據後,你應該查看整個向量並以常識判讀而非單純仰賴統計資訊。

在 3.7 版的變更: repeat 的預設值從 3 更改為 5。

print_exc(file=None)

從計時程式碼印出回溯 (traceback) 的輔助函式。

典型用法:

t = Timer(...)       # 在 try/except 之外
try:
    t.timeit(...)    # 或 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

執行 'statement' 多少次

-r N, --repeat=N

計時器重複多少次(預設 5)

-s S, --setup=S

會在一開始執行一次的陳述式(預設為 pass

-p, --process

若要測量行程時間 (process time) 而非掛鐘時間 (wallclock time),請使用 time.process_time() 而不是預設的 time.perf_counter()

在 3.3 版被加入.

-u, --unit=U

指定定時器輸出的時間單位;可以選擇 nsecusecmsecsec

在 3.5 版被加入.

-t, --target-time=T

if --number is 0, the code will run until it takes at least this many seconds (default: 0.2)

在 3.15 版被加入.

-v, --verbose

印出原始計時結果;重複執行以獲得更高的數字精度

-h, --help

印出一條簡短的使用訊息並退出

可以透過將每一列陳述式指定為單獨引數來給定多列陳述式;可透過將引數括在引號中並使用前導空格以實現縮進列 (indented lines)。多個 -s 選項的作用類似。

If -n is not given, a suitable number of loops is calculated by trying increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the total time is at least --target-time seconds (default: 0.2).

default_timer() 測量可能會受到同一台機器上運行的其他程式的影響,因此,當需要精確計時時,最好的做法是重複計時幾次並使用最佳時間。-r 選項對此很有用;在大多數情況下,預設的重複 5 次可能就足夠了。你可以使用 time.process_time() 來測量 CPU 時間。

備註

執行 pass 陳述式會產生一定的基本開銷。這裡的程式碼並不試圖隱藏它,但你應該意識到它的存在。可以透過不帶引數呼叫程式來測量這個基本開銷,且不同 Python 版本之間的基本開銷可能有所不同。

範例

可以提供一個僅會在開始時執行一次的設定陳述式:

$ 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

輸出中包含三個欄位。迴圈計數,它告訴你每次計時迴圈內重複運行陳述式主體的次數。重複計數(「最好的 5 次」)告訴你計時迴圈重複了多少次。以及最後陳述式主體在計時迴圈的最佳的幾次重複執行內平均花費的時間。也就是說,最快的幾次重複執行所花費的總時間除以迴圈計數。

>>> 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 來測試缺失和存在之物件屬性並比較其花費 (cost):

$ 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 模組存取你定義的函式,你可以傳遞一個包含 import 陳述式的 setup 參數:

def test():
    """一個笨笨的測試函式"""
    L = [i for i in range(100)]

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

另一種選擇是將 globals() 傳遞給 globals 參數,這將導致程式碼在目前的全域命名空間中執行,這比單獨指定 import 更方便:

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()))