Python 性能分析大全

雖然運行速度慢是 Python 與生俱來的特色,大多數時候咱們用 Python 就意味着放棄對性能的追求。可是,就算是用純 Python 完成同一個任務,老手寫出來的代碼可能會比菜鳥寫的代碼塊幾倍,甚至是幾十倍(這裏不考慮算法的因素,只考慮語言方面的因素)。不少時候,咱們將本身的代碼運行緩慢地緣由歸結於python原本就很慢,從而問心無愧地放棄深刻探究。html

可是,事實真的是這樣嗎?面對python代碼,你有分析下面這些問題嗎:python

  • 程序運行的速度如何?git

  • 程序運行時間的瓶頸在哪裏?github

  • 可否稍加改進以提升運行速度呢?算法

爲了更好了解python程序,咱們須要一套工具,可以記錄代碼運行時間,生成一個性能分析報告,方便完全瞭解代碼,從而進行鍼對性的優化(本篇側重於代碼性能分析,不關注如何優化)。shell

誰快誰慢

假設有一個字符串,想將裏面的空格替換爲字符‘-’,用python實現起來很簡單,下面是四種方案:segmentfault

def slowest_replace():
    replace_list = []
    for i, char in enumerate(orignal_str):
        c = char if char != " " else "-"
        replace_list.append(c)
    return "".join(replace_list)

def slow_replace():
    replace_str = ""
    for i, char in enumerate(orignal_str):
        c = char if char != " " else "-"
        replace_str += c
    return replace_str

def fast_replace():
    return "-".join(orignal_str.split())

def fastest_replace():
    return orignal_str.replace(" ", "-")

這四種方案的效率如何呢,哪一種方案比較慢呢?這是一個問題!性能優化

時間斷點

最直接的想法是在開始 replace 函數以前記錄時間,程序結束後再記錄時間,計算時間差即爲程序運行時間。python提供了模塊 time,其中 time.clock() 在Unix/Linux下返回的是CPU時間(浮點數表示的秒數),Win下返回的是以秒爲單位的真實時間(Wall-clock time)。app

因爲替換函數耗時可能很是短,因此這裏考慮分別執行 100000次,而後查看不一樣函數的效率。咱們的性能分析輔助函數以下:ide

def _time_analyze_(func):
    from time import clock
    start = clock()
    for i in range(exec_times):
        func()
    finish = clock()
    print "{:<20}{:10.6} s".format(func.__name__ + ":", finish - start)

這樣就能夠了解上面程序的運行時間狀況:

第一種方案耗時是第四種的 45 倍多,大跌眼鏡了吧!一樣是 python代碼,完成同樣的功能,耗時能夠差這麼多。

爲了不每次在程序開始、結束時插入時間斷點,而後計算耗時,能夠考慮實現一個上下文管理器,具體代碼以下:

class Timer(object):
    def __init__(self, verbose=False):
        self.verbose = verbose

    def __enter__(self):
        self.start = clock()
        return self

    def __exit__(self, *args):
        self.end = clock()
        self.secs = self.end - self.start
        self.msecs = self.secs * 1000  # millisecs
        if self.verbose:
            print 'elapsed time: %f ms' % self.msecs

使用時只須要將要測量時間的代碼段放進 with 語句便可,具體的使用例子放在 gist 上。

timeit

上面手工插斷點的方法十分原始,用起來不是那麼方便,即便用了上下文管理器實現起來仍是略顯笨重。還好 Python 提供了timeit模塊,用來測試代碼塊的運行時間。它既提供了命令行接口,又能用於代碼文件之中。

命令行接口

命令行接口能夠像下面這樣使用:

$ python -m timeit -n 1000000 '"I like to reading.".replace(" ", "-")'
1000000 loops, best of 3: 0.253 usec per loop
$ python -m timeit -s 'orignal_str = "I like to reading."' '"-".join(orignal_str.split())'
1000000 loops, best of 3: 0.53 usec per loop

具體參數使用能夠用命令 python -m timeit -h 查看幫助。使用較多的是下面的選項:

  • -s S, --setup=S: 用來初始化statement中的變量,只運行一次;

  • -n N, --number=N: 執行statement的次數,默認會選擇一個合適的數字;

  • -r N, --repeat=N: 重複測試的次數,默認爲3;

Python 接口

能夠用下面的程序測試四種 replace函數的運行狀況(完整的測試程序能夠在 gist 上找到):

def _timeit_analyze_(func):
    from timeit import Timer
    t1 = Timer("%s()" % func.__name__, "from __main__ import %s" % func.__name__)
    print "{:<20}{:10.6} s".format(func.__name__ + ":", t1.timeit(exec_times))

運行結果以下:

Python的timeit提供了 timeit.Timer() 類,類構造方法以下:

Timer(stmt='pass', setup='pass', timer=<timer function>)

其中:

  • stmt: 要計時的語句或者函數;

  • setup: 爲stmt語句構建環境的導入語句;

  • timer: 基於平臺的時間函數(timer function);

Timer()類有三個方法:

  • timeit(number=1000000): 返回stmt執行number次的秒數(float);

  • repeat(repeat=3, number=1000000): repeat爲重複整個測試的次數,number爲執行stmt的次數,返回以秒記錄的每一個測試循環的耗時列表;

  • print_exc(file=None): 打印stmt的跟蹤信息。

此外,timeit 還提供了另外三個函數方便使用,參數和 Timer 差很少。

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000)
timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=3, number=1000000)
timeit.default_timer()

profile

以上方法適用於比較簡單的場合,更復雜的狀況下,能夠用標準庫裏面的profile或者cProfile,它能夠統計程序裏每個函數的運行時間,而且提供了可視化的報表。大多狀況下,建議使用cProfile,它是profile的C實現,適用於運行時間長的程序。不過有的系統可能不支持cProfile,此時只好用profile。

能夠用下面程序測試 timeit_profile() 函數運行時間分配狀況。

import cProfile
from time_profile import *

cProfile.run("timeit_profile()")

這樣的輸出可能會很長,不少時候咱們感興趣的可能只有耗時最多的幾個函數,這個時候先將cProfile 的輸出保存到診斷文件中,而後用 pstats 定製更加有好的輸出(完整代碼在 gist 上)。

cProfile.run("timeit_profile()", "timeit")
p = pstats.Stats('timeit')
p.sort_stats('time')
p.print_stats(6)

輸出結果以下:

若是以爲 pstas 使用不方便,還可使用一些圖形化工具,好比 gprof2dot 來可視化分析 cProfile 的診斷結果。

vprof

vprof 也是一個不錯的可視化工具,能夠用來分析 Python 程序運行時間狀況。以下圖:

line_profiler

上面的測試最多統計到函數的執行時間,不少時候咱們想知道函數裏面每一行代碼的執行效率,這時候就能夠用到 line_profiler 了。

line_profiler 的使用特別簡單,在須要監控的函數前面加上 @profile 裝飾器。而後用它提供的 kernprof -l -v [source_code.py] 行進行診斷。下面是一個簡單的測試程序 line_profile.py:

from time_profile import slow_replace, slowest_replace

for i in xrange(10000):
    slow_replace()
    slowest_replace()

運行後結果以下:

輸出每列的含義以下:

  • Line #: 行號

  • Hits: 當前行執行的次數.

  • Time: 當前行執行耗費的時間,單位爲 "Timer unit:"

  • Per Hit: 平均執行一次耗費的時間.

  • % Time: 當前行執行時間佔總時間的比例.

  • Line Contents: 當前行的代碼

line_profiler 執行時間的估計不是特別精確,不過能夠用來分析當前函數中哪些行是瓶頸。

博客地址

更多閱讀

A guide to analyzing Python performance
timeit – Time the execution of small bits of Python code
Profiling Python using cProfile: a concrete case
profile, cProfile, and pstats – Performance analysis of Python programs.
How can you profile a Python script?
檢測Python程序執行效率及內存和CPU使用的7種方法
代碼優化概要
Python性能優化的20條建議

相關文章
相關標籤/搜索