timeit — 測量小段程式碼的執行時間

原始碼: Lib/timeit.py


此模組提供了一種簡單的方法來測量小段 Python 程式碼的執行時間。它既有命令列介面,也有可呼叫介面。它避免了測量執行時間時常見的陷阱。另請參閱 Tim Peters 在 O'Reilly 出版的第二版 *Python Cookbook* 中“演算法”章的引言。

基本示例

以下示例展示瞭如何使用命令列介面來比較三個不同的表示式

$ 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 例項,並使用 *number* 次執行執行其 timeit() 方法。可選的 *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';計時器函式取決於平臺(參見模組的文件字串)。*stmt* 和 *setup* 也可以包含由 ; 或換行符分隔的多個語句,只要它們不包含多行字串字面量。預設情況下,語句將在 timeit 的名稱空間中執行;此行為可以透過將名稱空間傳遞給 *globals* 來控制。

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

*setup* 的執行時間不包括在整體計時執行執行中。

*stmt* 和 *setup* 引數也可以接受不帶引數的可呼叫物件。這將把對它們的呼叫嵌入到一個計時器函式中,該函式隨後將由 timeit() 執行。請注意,在這種情況下,由於額外的函式呼叫,計時開銷會稍大一些。

3.5 版本中有所改變: 添加了可選的 *globals* 引數。

timeit(number=1000000)

對主語句進行 *number* 次執行計時。這將執行一次設定語句,然後返回執行主語句多次所需的時間。預設計時器以浮點數形式返回秒數。引數是迴圈次數,預設為一百萬次。主語句、設定語句和要使用的計時器函式都傳遞給建構函式。

備註

預設情況下,timeit() 在計時期間會暫時關閉垃圾回收。這種方法的優點是使獨立的計時更具可比性。缺點是垃圾回收可能是被測量函式效能的重要組成部分。如果是這樣,可以在 *setup* 字串中的第一個語句中重新啟用垃圾回收。例如

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

自動確定呼叫 timeit() 的次數。

這是一個便利函式,它重複呼叫 timeit(),直到總時間 >= 0.2 秒,返回最終的(迴圈次數,該迴圈次數所花費的時間)。它以序列 1、2、5、10、20、50、... 中遞增的數字呼叫 timeit(),直到所花費的時間至少為 0.2 秒。

如果給定了 *callback* 且不為 None,則在每次嘗試後,它將以兩個引數被呼叫:callback(number, time_taken)

在 3.6 版本加入。

repeat(repeat=5, number=1000000)

多次呼叫 timeit()

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

備註

從結果向量中計算平均值和標準差並報告它們是很誘人的。然而,這並不是很有用。在典型情況下,最低值給出了您的機器執行給定程式碼片段的速度下限;結果向量中較高的值通常不是由 Python 速度的可變性引起的,而是由其他程序干擾您的計時精度引起的。因此,結果的 min() 可能是您應該唯一感興趣的數字。之後,您應該檢視整個向量並應用常識而不是統計資料。

3.7 版本中有所改變: *repeat* 的預設值從 3 更改為 5。

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

計時器重複的次數(預設為 5)

-s S, --setup=S

初始執行一次的語句(預設為 pass

-p, --process

測量程序時間,而不是牆鍾時間,使用 time.process_time() 而不是預設的 time.perf_counter()

在 3.3 版本加入。

-u, --unit=U

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

在 3.5 版本加入。

-v, --verbose

列印原始計時結果;重複以獲得更高精度

-h, --help

列印簡短的使用資訊並退出

多行語句可以透過將每行指定為單獨的語句引數來給出;縮排行可以透過將引數用引號括起來並使用前導空格來實現。多個 -s 選項也以類似方式處理。

如果未給出 -n,則透過嘗試序列 1、2、5、10、20、50、... 中遞增的數字來計算合適的迴圈次數,直到總時間至少為 0.2 秒。

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

備註

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

示例

可以提供一個僅在開始時執行一次的 setup 語句

$ 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 來測試缺失和存在的物件屬性的開銷

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