pretty code

2023年6月2日 星期五

Python 優化小經驗

這幾天看數學看到有點煩,剛好之前有寫一個函數實作 warpPerspective 的功能,只是一個很簡單的做插值計算並將數值填回去 numpy array 的程式,Python 居然要花到好幾秒鐘,為了轉換一下心情,底下是我有嘗試過的一些優化技巧,做個記錄先,有些我也不知如何解釋?

01. 陣列運算一次做比在 two for loop 中做還快。

比如說將一張圖像的齊次座標乘上 Homography matrix,將 3 x (width x height) 的座標先填上 ndarray,之後在一起乘 H matrix,並除以第 3 個分量,比在 loop 中每個 pixel 單獨計算還來得快。

這個好理解,numpy 底層應該有對 matrix 運算優化,可能是 multiprocess 之類的,故有吃到 numpy 的 buffer 靈氣。

02. image[h, w, :] = xx1 * yy1[h, w, :] + xx2 * yy2[h, w, :] + xx3 * yy3[h, w, :] + xx4 * yy4[h, w, :] 優化。

上句右邊計算結果也是一個 ndarray,將上句拆成 3 句可以優化。

image[h, w, 0] = xx1 * yy1[h, w, 0] + ...
image[h, w, 1] = xx1 * yy1[h, w, 1] + ...
image[h, w, 2] = xx1 * yy1[h, w, 2] + ...

這個也好理解,可能是少掉中間那個臨時變數的消耗?

03. 使用 cProfile 做 profile 追蹤,看是否能看出端倪?

---------
command line

py -m cProfile -o xxx.prof xxxx.py
py -m cProfile -s tottime xxx.py

---------
In xxx.py

import cProfile, pstats, io
from pstats import SortKey

pr = cProfile.Profile()
pr.enable()

warpPerspective(img, M, (maxWidth, maxHeight))

pr.disable()
s = io.StringIO()
sortby = 'tottime'
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats(20)
print(s.getvalue())
---------

cProfile 可以在程式碼中直接呼叫相關 class 也可以在 command line 中觸發,如果在 command line 觸發時有儲存檔案 xxx.prof,可以用 SnakeViz 這個第三方模組開啟並作分析,SnakeViz 會另外開啟一個 browser window 方便看 call stack,會比原來的 cProfile 結果好看。

---------
pip install snakeviz
snakeviz xxx.prof
---------

但因為我已經知道是那個函數花時間而我的函數又都是計算賦值的工作(純 Python code,沒有呼叫 other functions),故這個工具幫助不大。

04. 改用 multiprocess 並配合 SharedMemory 分享 read data 和 write data。

share read data 沒有什麼問題,但 share write data 一直沒成功,後來才知道要如何寫,如果照文件說的應該同時讀寫是沒問題的?至少我在 Process 和 Thread 中都沒有使用 lock 來保護 share write data。

---------
This style of shared memory permits distinct processes to potentially read and write to a common (or shared) region of volatile memory.
---------

分享 write data 的用法,重點是在 main process 中複製建立的 blank_ 而不是建立 Process 前將 blank 指給 blank_(在個別的 Process 中寫到的還是 blank_ 而不會是 blank)。

---------
blank = np.ones((max_h, max_w, 3), np.uint8)

share_blank = shared_memory.SharedMemory(name='share_blank', create=True, size=blank.nbytes)
blank_ = np.ndarray((max_h, max_w, 3), dtype=np.uint8, buffer=share_blank.buf)

p_count = 1
p = Process(target=warpPerspective_task, args=(p_count, i, H_Inv, img.shape[1], img.shape[0], max_w, max_h))
p.start()
p.join()

blank = blank_.copy()

p.close()
p.unlink()
---------

另外,我電腦有 4 核心,開 4 個 process 分區塊寫 blank_ 對加速並沒有幫助,跟沒有開 Process 時運行時間差不了太多?

05. 將 01 步驟中的大 matrix 計算,配合 4 個 process,各個 process 在 task 中各自計算自己那部份的 matrix。

看起來跟 04 步驟很像,但這樣卻可以再省個 300 ~ 400 毫秒?

06. 如果在迴圈中有存取 class variable,用一個 local variable 來接它,可以減少執行時間。


總之,在不懂 Python 底層的情況下,我目前也只能先試到這樣﹍

沒有留言: