python性能分析

調優簡介

什麼是性能分析javascript

 

沒有優化過的程序一般會在某些子程序(subroutine)上消耗大部分的CPU指令週期(CPU cycle)。性能分析就是分析代碼和它正在使用的資源之間有着怎樣的關係。html

例如,性能分析能夠告訴你一個指令佔用了多少CPU時間,或者整個程序消耗了多少內存。java

性能分析是經過使用一種被稱爲性能分析器(profiler)的工具,對程序或者二進制可執行文件(若是能夠拿到)的源代碼進行調整來完成的。python

性能分析軟件有兩類方法論:基於事件的性能分析(event-based profiling)和統計式性能分析(statistical profiling)。

 

支持這類基於事件的性能分析的編程語言主要有如下幾種。git

Java:JVMTI(JVM Tools Interface,JVM工具接口)爲性能分析器提供了鉤子,能夠跟蹤諸如函數調用、線程相關的事件、類加載之類的事件。

.NET:和Java同樣,.NET運行時提供了事件跟蹤功能(https://en.wikibooks.org/wiki/Intro-duction_to_Software_Engineering/Testing/Profiling#Methods_of_data_gathering)。

Python: 開發者能夠用 sys.setprofile 函數,跟蹤 python_[call|return|exception]或 c_[call|return|exception] 之類的事件。

 

基於事件的性能分析器(event-based profiler,也稱爲軌跡性能分析器,tracing profiler)是經過收集程序執行過程當中的具體事件進行工做的。程序員

這些性能分析器會產生大量的數據。基本上,它們須要監聽的事件越多,產生的數據量就越大。這致使它們不太實用,在開始對程序進行性能分析時也不是首選。github

可是,當其餘性能分析方法不夠用或者不夠精確時,它們能夠做爲最後的選擇。算法

 

Python基於事件的性能分析器的簡單示例代碼

import sys
 
def profiler(frame, event, arg):
    print 'PROFILER: %r %r' % (event, arg)
    
sys.setprofile(profiler)
 
#simple (and very ineficient) example of how to calculate the Fibonacci sequence for a number.
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
def fib_seq(n):
    seq = [ ]
    if n > 0:
        seq.extend(fib_seq(n-1))
    seq.append(fib(n))
    return seq
 
print fib_seq(2)

執行結果:數據庫

$ python test.py 
PROFILER: 'call' None
PROFILER: 'call' None
PROFILER: 'call' None
PROFILER: 'call' None
PROFILER: 'return' 0
PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7f67a0>
PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7f67a0>
PROFILER: 'return' [0]
PROFILER: 'c_call' <built-in method extend of list object at 0x7f113d7e0d40>
PROFILER: 'c_return' <built-in method extend of list object at 0x7f113d7e0d40>
PROFILER: 'call' None
PROFILER: 'return' 1
PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7e0d40>
PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7e0d40>
PROFILER: 'return' [0, 1]
PROFILER: 'c_call' <built-in method extend of list object at 0x7f113d7e0758>
PROFILER: 'c_return' <built-in method extend of list object at 0x7f113d7e0758>
PROFILER: 'call' None
PROFILER: 'call' None
PROFILER: 'return' 1
PROFILER: 'call' None
PROFILER: 'return' 0
PROFILER: 'return' 1
PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7e0758>
PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7e0758>
PROFILER: 'return' [0, 1, 1]
[0, 1, 1]
PROFILER: 'return' None
PROFILER: 'call' None
PROFILER: 'c_call' <built-in method discard of set object at 0x7f113d818960>
PROFILER: 'c_return' <built-in method discard of set object at 0x7f113d818960>
PROFILER: 'return' None
PROFILER: 'call' None
PROFILER: 'c_call' <built-in method discard of set object at 0x7f113d81d3f0>
PROFILER: 'c_return' <built-in method discard of set object at 0x7f113d81d3f0>
PROFILER: 'return' None
View Code

 

統計式性能分析器以固定的時間間隔對程序計數器(program counter)進行抽樣統計。這樣作可讓開發者掌握目標程序在每一個函數上消耗的時間。編程

因爲它對程序計數器進行抽樣,因此數據結果是對真實值的統計近似。不過,這類軟件足以窺見被分析程序的性能細節,查出性能瓶頸之所在。

它使用抽樣的方式(用操做系統中斷),分析的數據更少,對性能形成的影響更小。

 

Linux統計式性能分析器OProfile(http://oprofile.sourceforge.net/news/)的分析結果:

Function name,File name,Times Encountered,Percentage
"func80000","statistical_profiling.c",30760,48.96%
"func40000","statistical_profiling.c",17515,27.88%
"func20000","static_functions.c",7141,11.37%
"func10000","static_functions.c",3572,5.69%
"func5000","static_functions.c",1787,2.84%
"func2000","static_functions.c",768,1.22%
func1500","statistical_profiling.c",701,1.12%
"func1000","static_functions.c",385,0.61%
"func500","statistical_profiling.c",194,0.31%

 

下面咱們使用statprof進行分析:

import statprof
def profiler(frame, event, arg):
    print 'PROFILER: %r %r' % (event, arg)
    
#simple (and very ineficient) example of how to calculate the Fibonacci sequence for a number.
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
  
def fib_seq(n):
    seq = [ ]
    if n > 0:
        seq.extend(fib_seq(n-1))
    seq.append(fib(n))
    return seq
 
statprof.start()
 
try:
    print fib_seq(20)
 
finally:
    statprof.stop()
statprof.display()

 

執行結果:

$ python test.py 
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
  %   cumulative      self          
 time    seconds   seconds  name    
100.00      0.01      0.01  test.py:15:fib
  0.00      0.01      0.00  test.py:21:fib_seq
  0.00      0.01      0.00  test.py:20:fib_seq
  0.00      0.01      0.00  test.py:27:<module>
---
Sample count: 2
Total time: 0.010000 seconds

注意上面代碼咱們把計算fib_seq的參數從2改爲20,由於執行時間太快的狀況下,statprof是獲取不到任何信息的。

性能分析的重要性


  性能分析並非每一個程序都要作的事情,尤爲對於那些小軟件來講,是沒多大必要的(不像那些殺手級嵌入式軟件或專門用於演示的性能分析程序)。性能分析須要花時間,並且只有在程序中發現了錯誤的時候纔有用。可是,仍然能夠在此以前進行性能分析,捕獲潛在的bug,這樣能夠節省後期的程序調試時間。

 

咱們已經擁有測試驅動開發、代碼審查、結對編程,以及其餘讓代碼更加可靠且符合預期的手段,爲何還須要性能分析?

 

隨着咱們使用的編程語言愈來愈高級(幾年間咱們就從彙編語言進化到了JavaScript),咱們越發不關心CPU循環週期、內存配置、CPU寄存器等底層細節了。新一代程序員都經過高級語言學習編程技術,由於它們更容易理解並且開箱即用。但它們依然是對硬件和與硬件交互行爲的抽象。隨着這種趨勢的增加,新的開發者愈來愈不會將性能分析做爲軟件開發中的一個步驟了。

 

現在,隨便開發一個軟件就能夠得到上千用戶。若是經過社交網絡一推廣,用戶可能立刻就會呈指數級增加。一旦用戶量激增,程序一般會崩潰,或者變得異常緩慢,最終被客戶無情拋棄。

 

上面這種狀況,顯然多是因爲糟糕的軟件設計和缺少擴展性的架構形成的。畢竟,一臺服務器有限的內存和CPU資源也可能會成爲軟件的瓶頸。可是,另外一種可能的緣由,也是被證實過許屢次的緣由,就是咱們的程序沒有作過壓力測試。咱們沒有考慮過資源消耗狀況;咱們只保證了測試已經經過,並且樂此不疲。

 

性能分析能夠幫助咱們避免項目崩潰夭折,由於它能夠至關準確地爲咱們展現程序運行的狀況,不論負載狀況如何。所以,若是在負載很是低的狀況下,經過性能分析發現軟件在I/O操做上消耗了80%的時間,那麼這就給了咱們一個提示。是產品負載太重時,內存泄漏就可能發生。性能分析能夠在負載真的太重以前,爲咱們提供足夠的證據來發現這類隱患。

 

性能分析的內容

運行時間

若是你對運行的程序有一些經驗(好比說你是一個網絡開發者,正在使用一個網絡框架),可能很清楚運行時間是否是太長。

例如,一個簡單的網絡服務器查詢數據庫、響應結果、反饋到客戶端,一共須要100毫秒。可是,若是程序運行得很慢,作一樣的事情須要花費60秒,你就得考慮作性能分析了。

import datetime
 
tstart = None
tend = None
 
def start_time():
    global tstart
    tstart = datetime.datetime.now()
    
def get_delta():
    global tstart
    tend = datetime.datetime.now()
    return tend - tstart
 
def fib(n):
    return n if n == 0 or n == 1 else fib(n-1) + fib(n-2)
 
def fib_seq(n):
    seq = [ ]
    if n > 0:
        seq.extend(fib_seq(n-1))
    seq.append(fib(n))
    return seq
 
start_time()
print "About to calculate the fibonacci sequence for the number 30"
delta1 = get_delta()
 
start_time()
seq = fib_seq(30)
delta2 = get_delta()
 
print "Now we print the numbers: "
start_time()
for n in seq:
    print n
delta3 = get_delta()
 
print "====== Profiling results ======="
print "Time required to print a simple message: %(delta1)s" % locals()
print "Time required to calculate fibonacci: %(delta2)s" % locals()
print "Time required to iterate and print the numbers: %(delta3)s" %locals()
print "====== ======="

執行結果:

$ python test.py 
About to calculate the fibonacci sequence for the number 30
Now we print the numbers: 
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
====== Profiling results =======
Time required to print a simple message: 0:00:00.000064
Time required to calculate fibonacci: 0:00:01.430740
Time required to iterate and print the numbers: 0:00:00.000075
====== =======

 可見計算部分是最消耗時間的。

發現瓶頸

只要你測量出了程序的運行時間,就能夠把注意力移到運行慢的環節上作性能分析。通常瓶頸由下面的一種或者幾種緣由組成:

* 重的I/O操做,好比讀取和分析大文件,長時間執行數據庫查詢,調用外部服務(好比HTTP請求),等等。

* 現了內存泄漏,消耗了全部的內存,致使後面的程序沒有內存來正常執行。

* 未經優化的代碼頻繁執行。

* 能夠緩存時密集的操做沒有緩存,佔用了大量資源。

 

I/O關聯的代碼(文件讀/寫、數據庫查詢等)很難優化,由於優化有可能會改變程序執行I/O操做的方式(一般是語言的核心函數操做I/O)。相反,優化計算關聯的代碼(好比程序使用的算法很糟糕),改善性能會比較容易(並不必定很簡單)。這是由於優化計算關聯的代碼就是改寫程序。

 

內存消耗和內存泄漏

內存消耗不只僅是關注程序使用了多少內存,還應該考慮控制程序使用內存的數量。跟蹤程序內存的消耗狀況比較簡單。最基本的方法就是使用操做系統的任務管理器。

它會顯示不少信息,包括程序佔用的內存數量或者佔用總內存的百分比。任務管理器也是檢查CPU時間使用狀況的好工具。

在下面的top截圖中,你會發現一個簡單的Python程序(就是前面那段程序)幾乎佔用了所有CPU(99.8%),內存只用了0.1%。

當運行過程啓動以後,內存消耗會在一個範圍內不斷增長。若是發現增幅超出範圍,並且消

耗增大以後一直沒有回落,就能夠判斷出現內存泄漏了。

過早優化的風險

優化一般被認爲是一個好習慣。可是,若是一味優化反而違背了軟件的設計原則就很差了。在開始開發一個新軟件時,開發者常常犯的錯誤就是過早優化(permature optimization)。若是過早優化代碼,結果可能會和原來的代碼大相徑庭。它可能只是完整解決方案的一部分,還可能包含因優化驅動的設計決策而致使的錯誤。

一條經驗法則是,若是你尚未對代碼作過測量(性能分析)

優化每每不是個好主意。首先,應該集中精力完成代碼,而後經過性能分析發現真正的性能瓶頸,最後對代碼進行優化。

 

運行時間複雜度

運行時間複雜度(Running Time Complexity,RTC)用來對算法的運行時間進行量化。它是對算法在必定數量輸入條件下的運行時間進行數學近似的結果。由於是數學近似,因此咱們能夠用這些數值對算法進行分類。

 

RTC經常使用的表示方法是大O標記(big O notation)。數學上,大O標記用於表示包含無限項的

函數的有限特徵(相似於泰勒展開式)。若是把這個概念用於計算機科學,就能夠把算法的運行

時間描述成漸進的有限特徵(數量級)。

主要模型有:

常數時間——O(1):好比判斷一個數是奇數仍是偶數、用標準輸出方式打印信息等。對於理論上更復雜的操做,好比在字典(或哈希表)中查找一個鍵的值,若是算法合理,就
        能夠在常數時間內完成。技術上看,在哈希表中查找元素的消耗時間是O(1)平均時間,這意味着每次操做的平均時間(不考慮特殊狀況)是固定值O(1)。

 

線性時間——O(n):好比查找無序列表中的最小元素、比較兩個字符串、刪除鏈表中的最後一項

 

對數時間——O(logn):對數時間(logarithmic time)複雜度的算法,表示隨着輸入數量的增長,算法的運行時間會達到固定的上限。隨着輸入數量的增長,對數函數開始增加很快,而後慢慢減速。
          它不會中止增加,可是越日後增加的速度越慢,甚至能夠忽略不計。好比:二分查找(binary search)、計算斐波那契數列(用矩陣乘法)。

 

線性對數時間——O(nlogn):把前面兩種時間類型組合起來就變成了線性對數時間(linearithmic time)。隨着x的增大,算法的運行時間會快速增加。
            好比歸併排序(merge sort)、堆排序(heap sort)、快速排序(quick sort,至少是平均運行時間)

 

階乘時間——O(n!):階乘時間(factorial time)複雜度的算法是最差的算法。其時間增速特別快,圖都很難畫。好比:用暴力破解搜索方法解貨郎擔問題(遍歷全部可能的路徑)。

 

平方時間——O(n 2 ):平方時間是另外一個快速增加的時間複雜度。輸入數量越多,須要消耗的時間越長(大多數算法都是這樣,這類算法尤爲如此)。
          平方時間複雜度的運行效率比線性時間複雜度要慢。好比冒泡排序(bubble sort)、遍歷二維數組、插入排序(insertion sort)

 

 

速度:對數>線性>線性對數>平方>階乘, 要考慮最好狀況、正常狀況和最差狀況。

性能分析最佳實踐

創建迴歸測試套件、思考代碼結構、耐心、儘量多地收集數據(其餘數據資源,如網絡應用的系統日誌、自定義日誌、系統資源快照(如操做系統任務管理器))、數據預處理、數據可視化

 

python中最出名的性能分析庫:cProfile、line_profiler。

前者是標準庫:https://docs.python.org/2/library/profile.html#module-cProfile。

後者參見:https://github.com/rkern/line_profiler。

專一於CPU時間。

 

 

 

聲明:原文連接:my.oschina.net/u/1433482/blog/709219

相關文章
相關標籤/搜索