簡體   English   中英

有什么簡單的方法可以對 Python 腳本進行基准測試嗎?

[英]Is there any simple way to benchmark Python script?

通常我使用 shell 命令time 我的目的是測試數據是小、中、大還是非常大的集合,需要多少時間和 memory 的使用量。

Linux 或僅 Python 的任何工具可以做到這一點?

看看timeitpython 分析器pycallgraph 還要確保看看下面nikicc提到“ SnakeViz的評論 它為您提供了另一個有用的分析數據可視化。

時間

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

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

    # For Python>=3.5 one can also write:
    print(timeit.timeit("test()", globals=locals()))

本質上,您可以將 python 代碼作為字符串參數傳遞給它,它會以指定的次數運行並打印執行時間。 文檔中的重要部分:

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)使用給定的語句、設置代碼和定時器函數創建一個Timer實例並運行它的timeit方法與數字處決。 可選的globals參數指定要在其中執行代碼的命名空間。

... 和:

Timer.timeit(number=1000000)主語句執行的時間 這將執行一次 setup 語句,然后返回多次執行主語句所需的時間,以秒為單位作為浮點數。 參數是循環的次數,默認為一百萬。 main 語句、setup 語句和要使用的定時器函數被傳遞給構造函數。

注意:默認情況下, timeit在計時期間暫時關閉garbage collection 這種方法的優點是它使獨立時序更具可比性。 這個缺點是 GC 可能是被測量函數性能的一個重要組成部分。 如果是這樣,GC 可以作為設置字符串中的第一個語句重新啟用。 例如:

timeit.Timer('for i in xrange(10): oct(i)', 'gc.enable()').timeit()

剖析

分析將使您對正在發生的事情更詳細的了解。 這是官方文檔中的“即時示例”:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

這會給你:

      197 function calls (192 primitive calls) in 0.002 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.001    0.001 <string>:1(<module>)
     1    0.000    0.000    0.001    0.001 re.py:212(compile)
     1    0.000    0.000    0.001    0.001 re.py:268(_compile)
     1    0.000    0.000    0.000    0.000 sre_compile.py:172(_compile_charset)
     1    0.000    0.000    0.000    0.000 sre_compile.py:201(_optimize_charset)
     4    0.000    0.000    0.000    0.000 sre_compile.py:25(_identityfunction)
   3/1    0.000    0.000    0.000    0.000 sre_compile.py:33(_compile)

這兩個模塊都應該讓您了解在哪里尋找瓶頸。

另外,要掌握profile的輸出,請查看這篇文章

pycallgraph

注意pycallgraph 已於2018 年 2 月正式棄用。 截至 2020 年 12 月,它仍在使用 Python 3.6。 只要 python 公開分析 API 的方式沒有核心變化,它應該仍然是一個有用的工具。

該模塊使用 graphviz 創建如下所示的調用圖:

調用圖示例

您可以通過顏色輕松查看哪些路徑使用時間最長。 您可以使用 pycallgraph API 或使用打包腳本創建它們:

pycallgraph graphviz -- ./mypythonscript.py

雖然開銷相當可觀。 因此,對於已經長時間運行的流程,創建圖表可能需要一些時間。

我使用一個簡單的裝飾器來計時 func

import time

def st_time(func):
    """
        st decorator to calculate the total time of a func
    """

    def st_func(*args, **keyArgs):
        t1 = time.time()
        r = func(*args, **keyArgs)
        t2 = time.time()
        print("Function=%s, Time=%s" % (func.__name__, t2 - t1))
        return r

    return st_func

timeit模塊又慢又奇怪,所以我寫了這個:

def timereps(reps, func):
    from time import time
    start = time()
    for i in range(0, reps):
        func()
    end = time()
    return (end - start) / reps

例子:

import os
listdir_time = timereps(10000, lambda: os.listdir('/'))
print "python can do %d os.listdir('/') per second" % (1 / listdir_time)

對我來說,它說:

python can do 40925 os.listdir('/') per second

這是一種原始的基准測試,但已經足夠好了。

我通常會快速time ./script.py以查看需要多長時間。 但是,這並沒有向您顯示內存,至少不是默認情況。 您可以使用/usr/bin/time -v ./script.py獲取大量信息,包括內存使用情況。

內存分析器滿足您的所有內存需求。

https://pypi.python.org/pypi/memory_profiler

運行 pip 安裝:

pip install memory_profiler

導入庫:

import memory_profiler

為您要分析的項目添加裝飾器:

@profile
def my_func():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == '__main__':
    my_func()

執行代碼:

python -m memory_profiler example.py

接收輸出:

 Line #    Mem usage  Increment   Line Contents
 ==============================================
 3                           @profile
 4      5.97 MB    0.00 MB   def my_func():
 5     13.61 MB    7.64 MB       a = [1] * (10 ** 6)
 6    166.20 MB  152.59 MB       b = [2] * (2 * 10 ** 7)
 7     13.61 MB -152.59 MB       del b
 8     13.61 MB    0.00 MB       return a

示例來自上面鏈接的文檔。

用於 cProfile 的snakeviz交互式查看器

https://github.com/jiffyclub/snakeviz/

https://stackoverflow.com/a/1593034/895245 中提到了 cProfile,在評論中提到了 snakeviz ,但我想進一步強調它。

僅通過查看cprofile / pstats輸出很難調試程序性能,因為它們只能cprofile計算每個函數的總次數。

但是,我們通常真正需要的是查看包含每次調用的堆棧跟蹤的嵌套視圖,以便輕松找到主要瓶頸。

這正是snakeviz 通過其默認的“冰柱”視圖所提供的。

首先,您必須將 cProfile 數據轉儲到二進制文件中,然后您可以在該文件上進行 snakeviz

pip install -u snakeviz
python -m cProfile -o results.prof myscript.py
snakeviz results.prof

這將打印一個指向標准輸出的 URL,您可以在瀏覽器上打開它,其中包含如下所示的所需輸出:

在此處輸入圖片說明

然后你可以:

  • 將每個框懸停以查看包含該函數的文件的完整路徑
  • 單擊一個框使該框顯示在頂部作為放大的一種方式

更多面向配置文件的問題: 如何配置 Python 腳本?

看看鼻子和它的一個插件,特別是這個插件。

安裝后,nose 是您路徑中的一個腳本,您可以在包含一些 Python 腳本的目錄中調用該腳本:

$: nosetests

這將查看當前目錄中的所有 python 文件,並將執行它識別為測試的任何函數:例如,它將名稱中帶有 test_ 一詞的任何函數識別為測試。

因此,您可以創建一個名為 test_yourfunction.py 的 Python 腳本並在其中編寫如下內容:

$: cat > test_yourfunction.py

def test_smallinput():
    yourfunction(smallinput)

def test_mediuminput():
    yourfunction(mediuminput)

def test_largeinput():
    yourfunction(largeinput)

然后你必須跑

$: nosetest --with-profile --profile-stats-file yourstatsprofile.prof testyourfunction.py

要讀取配置文件,請使用以下 python 行:

python -c "import hotshot.stats ; stats = hotshot.stats.load('yourstatsprofile.prof') ; stats.sort_stats('time', 'calls') ; stats.print_stats(200)"

要當心timeit很慢,要花上我的媒體處理器12秒鍾只是初始化(或者運行功能)。 你可以測試這個接受的答案

def test():
    lst = []
    for i in range(100):
        lst.append(i)

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

對於簡單的事情,我將使用time代替,在我的 PC 上它返回結果0.0

import time

def test():
    lst = []
    for i in range(100):
        lst.append(i)

t1 = time.time()

test()

result = time.time() - t1
print(result) # 0.000000xxxx

快速測試任何函數的簡單方法是使用以下語法: %timeit my_code

例如 :

%timeit a = 1

13.4 ns ± 0.781 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)

如果您不想為 timeit 編寫樣板代碼並易於分析結果,請查看benchmarkit 它還保存了以前運行的歷史記錄,因此很容易在開發過程中比較相同的功能。

# pip install benchmarkit

from benchmarkit import benchmark, benchmark_run

N = 10000
seq_list = list(range(N))
seq_set = set(range(N))

SAVE_PATH = '/tmp/benchmark_time.jsonl'

@benchmark(num_iters=100, save_params=True)
def search_in_list(num_items=N):
    return num_items - 1 in seq_list

@benchmark(num_iters=100, save_params=True)
def search_in_set(num_items=N):
    return num_items - 1 in seq_set

benchmark_results = benchmark_run(
   [search_in_list, search_in_set],
   SAVE_PATH,
   comment='initial benchmark search',
)  

打印到終端並返回包含上次運行數據的字典列表。 命令行入口點也可用。

在此處輸入圖片說明

如果您更改N=1000000並重新運行

在此處輸入圖片說明

基於劉丹雲的回答,具有一些便利功能,也許對某人有用。

def stopwatch(repeat=1, autorun=True):
    """
    stopwatch decorator to calculate the total time of a function
    """
    import timeit
    import functools
    
    def outer_func(func):
        @functools.wraps(func)
        def time_func(*args, **kwargs):
            t1 = timeit.default_timer()
            for _ in range(repeat):
                r = func(*args, **kwargs)
            t2 = timeit.default_timer()
            print(f"Function={func.__name__}, Time={t2 - t1}")
            return r
        
        if autorun:
            try:
                time_func()
            except TypeError:
                raise Exception(f"{time_func.__name__}: autorun only works with no parameters, you may want to use @stopwatch(autorun=False)") from None
        
        return time_func
    
    if callable(repeat):
        func = repeat
        repeat = 1
        return outer_func(func)
    
    return outer_func

一些測試:

def is_in_set(x):
    return x in {"linux", "darwin"}

def is_in_list(x):
    return x in ["linux", "darwin"]

@stopwatch
def run_once():
    import time
    time.sleep(0.5)

@stopwatch(autorun=False)
def run_manually():
    import time
    time.sleep(0.5)

run_manually()

@stopwatch(repeat=10000000)
def repeat_set():
    is_in_set("windows")
    is_in_set("darwin")

@stopwatch(repeat=10000000)
def repeat_list():
    is_in_list("windows")
    is_in_list("darwin")

@stopwatch
def should_fail(x):
    pass

結果:

Function=run_once, Time=0.5005391679987952
Function=run_manually, Time=0.500624185999186
Function=repeat_set, Time=1.7064883739985817
Function=repeat_list, Time=1.8905151920007484
Traceback (most recent call last):
  (some more traceback here...)
Exception: should_fail: autorun only works with no parameters, you may want to use @stopwatch(autorun=False)

line_profiler(逐行執行時間)

安裝

pip install line_profiler

用法

  • 在函數之前添加一個@profile裝飾器。 例如:
@profile
def function(base, index, shift):
    addend = index << shift
    result = base + addend
    return result
  • 使用命令kernprof -l <file_name>創建 line_profiler 的實例。 例如:
kernprof -l test.py

kernprof 將在成功時將Wrote profile results to <file_name>.lprof打印Wrote profile results to <file_name>.lprof 例如:

Wrote profile results to test.py.lprof
  • 使用命令python -m line_profiler <file_name>.lprof打印基准測試結果。 例如:
python -m line_profiler test.py.lprof

您將看到有關每行代碼的詳細信息:

Timer unit: 1e-06 s

Total time: 0.0021632 s
File: test.py
Function: function at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           @profile
     2                                           def function(base, index, shift):
     3      1000        796.4      0.8     36.8      addend = index << shift
     4      1000        745.9      0.7     34.5      result = base + addend
     5      1000        620.9      0.6     28.7      return result

memory_profiler(逐行內存使用情況)

安裝

pip install memory_profiler

用法

  • 在函數之前添加一個@profile裝飾器。 例如:
@profile
def function():
    result = []
    for i in range(10000):
        result.append(i)
    return result
  • 使用命令python -m memory_profiler <file_name>打印基准測試結果。 例如:
python -m memory_profiler test.py

您將看到有關每行代碼的詳細信息:

Filename: test.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     1   40.246 MiB   40.246 MiB           1   @profile
     2                                         def function():
     3   40.246 MiB    0.000 MiB           1       result = []
     4   40.758 MiB    0.008 MiB       10001       for i in range(10000):
     5   40.758 MiB    0.504 MiB       10000           result.append(i)
     6   40.758 MiB    0.000 MiB           1       return result

良好做法

多次調用一個函數以盡量減少對環境的影響。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM