你對如何讓除錯器變得更快產生過興趣嗎?本文將分享我們在為 Python 構建除錯器時得到的一些經驗,
整段故事講的是我們在 Rookout 公司的團隊為 Python 除錯器開發不中斷斷點的經歷,以及開發程序中得到的經驗,我將在本月于舊金山舉辦的 PyBay 2019 上介紹有關 Python 除錯程序的更多細節,但現在就讓我們立刻開始這段故事,
另外要注意:不管你是為了Python就業還是興趣愛好,記住:專案開發經驗永遠是核心,如果如果你沒有2020最新python入門到高級實戰視頻教程,可以去小編的Python交流.裙 :七衣衣九七七巴而五(數字的諧音)轉換下可以找到了,里面很多新python教程專案,還可以跟老司機交流討教!
Python 除錯器的心臟:sys.set_trace
在諸多可選的 Python 除錯器中,使用最廣泛的三個是:
- pdb,它是 Python 標準庫的一部分
- PyDev,它是內嵌在 Eclipse 和 Pycharm 等 IDE 中的除錯器
- ipdb,它是 IPython 的除錯器
Python 除錯器的選擇雖多,但它們幾乎都基于同一個函式:sys.settrace, 值得一提的是, sys.settrace 可能也是 Python 標準庫中最復雜的函式,
簡單來講,settrace 的作用是為解釋器注冊一個跟蹤函式,它在下列四種情形發生時被呼叫:
- 函式呼叫
- 陳述句執行
- 函式回傳
- 例外拋出
一個簡單的跟蹤函式看上去大概是這樣:
def simple_tracer(frame, event, arg):
co = frame.f_code
func_name = co.co_name
line_no = frame.f_lineno
print("{e} {f} {l}".format(
e=event, f=func_name, l=line_no))
return simple_tracer
復制代碼
在分析函式時我們首先關注的是引數和回傳值,該跟蹤函式的引數分別是:
frame,當前堆疊幀,它是包含當前函式執行時解釋器里完整狀態的物件event,事件,它是一個值可能為call、line、return或exception的字串arg,引數,它的取值基于event的型別,是一個可選項
該跟蹤函式的回傳值是它自身,這是由于解釋器需要持續跟蹤兩類跟蹤函式:
- 全域跟蹤函式(每執行緒):該跟蹤函式由當前執行緒呼叫
sys.settrace來設定,并在解釋器創建一個新的堆疊幀時被呼叫(即代碼中發生函式呼叫時),雖然沒有現成的方式來為不同的執行緒設定跟蹤函式,但你可以呼叫threading.settrace來為所有新創建的threading模塊執行緒設定跟蹤函式, - 區域跟蹤函式(每一幀):解釋器將該跟蹤函式的值設定為全域跟蹤函式創建幀時的回傳值,同樣也沒有現成的方法能夠在幀被創建時自動設定區域跟蹤函式,
該機制的目的是讓除錯器對被跟蹤的幀有更精確的把握,以減少對性能的影響,
簡單三步構建除錯器 (我們最初的設想)
僅僅依靠上文提到的內容,用自制的跟蹤函式來構建一個真正的除錯器似乎有些不切實際,幸運的是,Python 的標準除錯器 pdb 是基于 Bdb 構建的,后者是 Python 標準庫中專門用于構建除錯器的基類,
基于 Bdb 的簡易斷點除錯器看上去是這樣的:
import bdb
import inspect
class Debugger(bdb.Bdb):
def __init__(self):
Bdb.__init__(self)
self.breakpoints = dict()
self.set_trace()
def set_breakpoint(self, filename, lineno, method):
self.set_break(filename, lineno)
try :
self.breakpoints[(filename, lineno)].add(method)
except KeyError:
self.breakpoints[(filename, lineno)] = [method]
def user_line(self, frame):
if not self.break_here(frame):
return
# Get filename and lineno from frame
(filename, lineno, _, _, _) = inspect.getframeinfo(frame)
methods = self.breakpoints[(filename, lineno)]
for method in methods:
method(frame)
復制代碼
這個除錯器類的全部構成是:
- 繼承
Bdb,定義一個簡單的建構式來初始化基類,并開始跟蹤, - 添加
set_breakpoint方法,它使用Bdb來設定斷點,并跟蹤這些斷點, - 多載
Bdb在當前用戶行呼叫的user_line方法,該方法一定被一個斷點呼叫,之后獲取該斷點的源位置,并呼叫已注冊的斷點,
這個簡易的 Bdb 除錯器效率如何呢?
Rookout 的目標是在生產級性能的使用場景下提供接近普通除錯器的使用體驗,那么,讓我們來看看先前構建出來的簡易除錯器表現的如何,
為了衡量除錯器的整體性能開銷,我們使用如下兩個簡單的函式來進行測驗,它們分別在不同的情景下執行了 1600 萬次,請注意,在所有情景下斷點都不會被執行,
def empty_method():
pass
def simple_method():
a = 1
b = 2
c = 3
d = 4
e = 5
f = 6
g = 7
h = 8
i = 9
j = 10
復制代碼
在使用除錯器的情況下需要大量的時間才能完成測驗,糟糕的結果指明了,這個簡陋 Bdb 除錯器的性能還遠不足以在生產環境中使用,

First Bdb debugger results
對除錯器進行優化
降低除錯器的額外開銷主要有三種方法:
- 盡可能的限制區域跟蹤:由于每一行代碼都可能包含大量事件,區域跟蹤比全域跟蹤的開銷要大得多,
- 優化
call事件并盡快將控制權還給解釋器:在call事件發生時除錯器的主要作業是判斷是否需要對該事件進行跟蹤, - 優化
line事件并盡快將控制權還給解釋器:在line事件發生時除錯器的主要作業是判斷我們在此處是否需要設定一個斷點,
于是我們復刻了 Bdb 專案,精簡特征、簡化代碼,針對使用場景進行優化,這些作業雖然得到了一些效果,但仍無法滿足我們的需求,因此我們又繼續進行了其它的嘗試,將代碼優化并遷移至 .pyx 使用 Cython 進行編譯,可惜結果(如下圖所示)依舊不夠理想,最終,我們在深入了解 CPython 原始碼之后意識到,讓跟蹤程序快到滿足生產需求是不可能的,
Second Bdb debugger results
放棄 Bdb 轉而嘗試位元組碼操作
熬過先前對標準除錯方法進行的試驗-失敗-再試驗回圈所帶來的失望,我們將目光轉向另一種選擇:位元組碼操作,
Python 解釋器的作業主要分為兩個階段:
- 將 Python 原始碼編譯成 Python 位元組碼:這種(對人類而言)不可讀的格式專為執行的效率而優化,它們通常快取在我們熟知的
.pyc檔案當中, - 遍歷 解釋器回圈中的位元組碼: 在這一步中解釋器會逐條的執行指令,
我們選擇的模式是:使用位元組碼操作來設定沒有全域額外開銷的不中斷斷點,這種方式的實作首先需要在記憶體中的位元組碼里找到我們感興趣的部分,然后在該部分的相關機器指令前插入一個函式呼叫,如此一來,解釋器無需任何額外的作業即可實作我們的不中斷斷點,
這種方法并不依靠魔法來實作,讓我們簡要地舉個例子,
首先定義一個簡單的函式:
def multiply(a, b):
result = a * b
return result
復制代碼
在 inspect 模塊(其包含了許多實用的單元)的檔案里,我們得知可以通過訪問 multiply.func_code.co_code 來獲取函式的位元組碼:
'|\x00\x00|\x01\x00\x14}\x02\x00|\x02\x00S'
復制代碼
使用 Python 標準庫中的 dis 模塊可以翻譯這些不可讀的字串,呼叫 dis.dis(multiply.func_code.co_code) 之后,我們就可以得到:
4 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_MULTIPLY
7 STORE_FAST 2 (result)
5 10 LOAD_FAST 2 (result)
13 RETURN_VALUE
復制代碼
與直截了當的解決方案相比,這種方法讓我們更靠近發生在除錯器背后的事情,可惜 Python 并沒有提供在解釋器中修改函式位元組碼的方法,我們可以對函式物件進行重寫,不過那樣做的效率滿足不了大多數實際的除錯場景,最后我們不得不采用一種迂回的方式來使用原生拓展才能完成這一任務,
總結
在構建一個新工具時,總會學到許多事情的作業原理,這種刨根問底的程序能夠使你的思路跳出桎梏,從而得到意料之外的解決方案,
最后注意:不管你是為了Python就業還是興趣愛好,記住:專案開發經驗永遠是核心,如果如果你沒有2020最新python入門到高級實戰視頻教程,可以去小編的Python交流.裙 :七衣衣九七七巴而五(數字的諧音)轉換下可以找到了,里面很多新python教程專案,還可以跟老司機交流討教!
在 Rookout 團隊中構建不中斷斷點的這段時間里,我學到了許多有關編譯器、除錯器、服務器框架、并發模型等等領域的知識,如果你希望更深入的了解位元組碼操作,谷歌的開源專案 cloud-debug-python 為編輯位元組碼提供了一些工具,本文的文字及圖片來源于網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,著作權歸原作者所有,如有問題請及時聯系我們以作處理,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/157932.html
標籤:Python
