基於Python的效能分析

changwan發表於2024-05-18

1、什麼是效能分析

字面意思就是對程式的效能,從使用者角度出發就是執行的速度佔用的記憶體

透過對以上情況的分析,來決定程式的哪部份能被最佳化。提高程式的速度以及記憶體的使用效率。

首先我們要弄清楚造成時間方面效能低的原因有哪些

  1. 沉重的I/O操作,比如讀取分析大檔案,長時間執行資料庫查詢,呼叫外部服務例如請求。
  2. 出現了記憶體洩露,消耗了所有記憶體,導致沒有記憶體使用程式崩潰。
  3. 未經過最佳化的程式碼被頻繁執行。
  4. 密集的操作在可以快取的時沒有快取,佔用大量資源。

大部分的效能瓶頸都是由I/O關聯的程式碼引起

2、執行時間分析

1.1、cProfile效能分析器

這個工具並不關心記憶體消耗等資訊。

import cProfile

def is_prime(n)-> bool:
    '''
    判斷一個數是否為素數
    - n : 待判斷的數
    '''
    if n <= 1:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def sum_of_primes_below_n(n)-> bool:
    '''
    計算小於等於 n 的所有素數的和
    - n : 待計算的數
    - return : 所有素數的和
    '''
    total = 0
    for i in range(2, n + 1):
        if is_prime(i):
            total += i
    return total

def factorial(n)-> int:
    '''
    計算 n 的階乘
    - n : 待計算的數
    - return : n 的階乘
    '''
    if n == 0:
        return 1
    return n * factorial(n - 1)

def fibonacci(n)-> int:
    '''
    計算斐波那契數列的第 n 個數
    - n : 待計算的數
    - return : 第 n 個斐波那契數
    '''
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

def main():
    cProfile.run('sum_of_primes_below_n(1000)')
    cProfile.run('factorial(20)')
    cProfile.run('fibonacci(20)')

if __name__ == "__main__":
    main()

image.png
這裡用於統計函式的呼叫次數以及允許的時間,用於排查程式的瓶頸。

這裡只作為舉例,感興趣的可以自行下去了解。

1.1.1、Profile類

這裡只需要修改部分程式碼即可。

不存在透明的效能分析器,雖然cProfile只消耗極小的效能分析器,仍然會對程式碼造成影響。如果大量使用會對程式造成很大影響。

if __name__ == "__main__":
    pro = cProfile.Profile()
    pro.enable()
    main()
    pro.create_stats()
    pro.print_stats()

  • ncalls: 函式呼叫的次數。
  • tottime: 函式的總執行時間。
  • percall: 平均每次函式呼叫的執行時間(tottime除以ncalls)。
  • cumtime: 函式及其所有子函式呼叫的總執行時間。
  • percall: 平均每次函式呼叫的累積執行時間(cumtime除以ncalls)。
  • filename:lineno(function): 函式所在的檔名、行號以及函式名。

這裡很顯然更詳細,更可靠。這裡補充一些其他方法,感興趣的可以下去自行了解。

enable() 開始收集效能分析資料
disable() 停止收集效能分析資料
create_stats() 停止收集資料,併為已收集的資料建立stats物件
print_stats(sort=-1) 建立一個stats物件,列印分析結果
dump_stats(filename) 把當前效能分析的內容寫進一個檔案
run(cmd) 將指定函式的效能分析結果列印出來
runctx(cmd,globals,locals) 在指定的全域性和區域性名稱空間中執行一個字串表示的 Python 命令,並對其進行效能分析
runcall(func,*args,**kwargs) 收集被呼叫函式func的效能分析資訊

1.1.2、Sats類

用於分析 Profile 收集的資料。

if __name__ == "__main__":
    pro = cProfile.Profile()
    pro.enable()
    main()
    pro.create_stats()
    p = pstats.Stats(pro)
    p.print_stats(3,1.0,'.*.py.*')

  • 3: 限制列印輸出的行數,僅列印前10行。
  • 1.0: 限制僅列印累積執行時間佔總執行時間的1%以上的函式。
  • '.*.py.*': 使用正規表示式過濾函式名,僅列印包含'.py.'的函式。

這裡還有許多其他的用法,這裡只是簡單舉例。感興趣的可以自行了解。

1.2、statprof統計式效能分析

優點:

  • 分析的資料更少:對程式執行過程中進行抽樣,不用保留每一條資料。
  • 對效能造成的影響更小:使用抽樣式(用作業系統中斷),目標程式的效能遭受的干擾更小。
import statprof
def is_prime(n)-> bool:
    '''
    判斷一個數是否為素數
    - n : 待判斷的數
    '''
    if n <= 1:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def sum_of_primes_below_n(n)-> bool:
    '''
    計算小於等於 n 的所有素數的和
    - n : 待計算的數
    - return : 所有素數的和
    '''
    total = 0
    for i in range(2, n + 1):
        if is_prime(i):
            total += i
    return total

def factorial(n)-> int:
    '''
    計算 n 的階乘
    - n : 待計算的數
    - return : n 的階乘
    '''
    if n == 0:
        return 1
    return n * factorial(n - 1)

def fibonacci(n)-> int:
    '''
    計算斐波那契數列的第 n 個數
    - n : 待計算的數
    - return : 第 n 個斐波那契數
    '''
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

def main():
    sum_of_primes_below_n(1000)
    factorial(100)
    fibonacci(30)

if __name__ == "__main__":
    statprof.start()
    main()
    statprof.stop()
    statprof.display()
 

Sample count: 72 這裡表示取樣的次數,statprof一共取樣了72次
Total time: 0.210000 seconds 整個效能分析的總時間
time 這一列為佔用程式的百分比
seconds 這一列為函式在呼叫棧中的累積時間
seconds 這一列為函式自身消耗的時間,不包括呼叫其他函式
name 這一列為函式的名稱

這裡不必將整個程式執行完畢,更節省時間(在效能需要最佳化的情況下)。其次取樣分析,資料更加可靠。

這裡依然只是做個簡單的例子,感興趣的可以自行下去了解。

總結:cProfile主要用於統計函式呼叫次數、執行時間等。相對來說效能開銷是比較小的。並且有多種輸出格式,例如JSON等資料格式,這裡只是舉例,感興趣的可以自行了解。

3、執行記憶體分析

這裡使用模組memory_profiler舉例,用對程式執行時的記憶體監控。

可以分別對單程序、多程序、記錄子程序記憶體佔用,多程序記錄子程序記憶體佔用。

並且可以使用matplotlib進行資料的視覺化,支援多種視覺化樣式。這裡分別做簡單解釋。

3.1、單程序分析

這裡為了便於理解,就不涉及複雜的程式碼,所以可能效果沒有這麼明顯,感興趣可以自己去了解

import numpy as np
import time

def create_large_array(n):
    '''建立n*n的矩陣
    - param size: 矩陣大小
    - return: 矩陣'''
    return np.zeros((n, n))

def modify_array(arr):
    '''修改矩陣的值
    arr: 矩陣'''
    size = arr.shape[0]
    arr[:size//2, :size//2] += 1
         
if __name__ == "__main__":
    large_array = create_large_array(1000)
    for i in range(10):
        modify_array(large_array)
        time.sleep(1)
        large_array = create_large_array(1000 + i * 100)   

執行mprof run main.py

會生成一個.dat檔案。可以使用matplotlib進行視覺化,效果更明顯。

如果只有一個dat檔案的話,可以直接執行mprof plot

如果有多個dat檔案的話,需要接檔名mprof plot filename.dat

可以清晰的看到程式執行時間段的記憶體使用情況,情況在業務中遠比這複雜的多。

3.2、多程序分析

import numpy as np
import time
from multiprocessing import Process, Queue

def create_large_array(n, queue):
    '''建立n*n的矩陣並將其放入佇列中
    - param n: 矩陣大小
    - param queue: 程序通訊佇列
    '''
    arr = np.zeros((n, n))
    queue.put(arr)

def modify_array(arr):
    '''修改矩陣的值
    arr: 矩陣'''
    size = arr.shape[0]
    arr[:size//2, :size//2] += 1

if __name__ == "__main__":
    queue = Queue()
    create_large_array(1000, queue)

    for i in range(5):
        large_array = queue.get()
        process = Process(target=modify_array, args=(large_array,))
        process.start()
        time.sleep(1)
        create_large_array(1000 + i * 100, queue)
        process.join()

mprof run --include-children --multiprocess filename.py

生成dat分析檔案,使用matplotlib視覺化

可以很直觀的感受程式中各程序的記憶體使用情況,可以看到是下降趨勢的,說明沒有出現記憶體洩露,如果是一直規律的向上增長,那麼你可能需要注意啦。

這裡使用matplotlib繪圖還有各種各樣的樣式,感興趣的可以下去了解。包括各式各樣的分析工具。

4、總結

這裡主要簡單分析程式的執行的執行時間和程式在執行過程中的記憶體使用情況。

時間方面,可以函式分析也可以逐句分析。這裡大家可以自行下去了解。

記憶體方面,主要是要避免出現記憶體洩露,透過分析程序、執行緒的記憶體使用情況,判斷瓶頸。

實際情況可能遠比這複雜的多,這裡的程式碼都只是用於簡單示例,感興趣的可以去研究一下。

順便分享一下我在實際專案中對記憶體分析的結果。

相關文章