虛擬機(jī)是怎么執(zhí)行字節(jié)碼的?背后都經(jīng)歷了哪些過(guò)程
楔子
當(dāng)解釋器啟動(dòng)后,首先會(huì)進(jìn)行運(yùn)行時(shí)環(huán)境的初始化,注意這里的運(yùn)行時(shí)環(huán)境,它和之前說(shuō)的執(zhí)行環(huán)境是不同的概念。運(yùn)行時(shí)環(huán)境是一個(gè)全局的概念,而執(zhí)行環(huán)境是一個(gè)棧幀。
關(guān)于運(yùn)行時(shí)環(huán)境的初始化是一個(gè)很復(fù)雜的過(guò)程,涉及到 Python 進(jìn)程、線程的創(chuàng)建,類型對(duì)象的完善等非常多的內(nèi)容,我們暫時(shí)先不討論。這里就假設(shè)初始化動(dòng)作已經(jīng)完成,我們已經(jīng)站在了虛擬機(jī)的門(mén)檻外面,只需要輕輕推動(dòng)第一張骨牌,整個(gè)執(zhí)行過(guò)程就像多米諾骨牌一樣,一環(huán)扣一環(huán)地展開(kāi)。
在介紹字節(jié)碼的時(shí)候我們說(shuō)過(guò),解釋器可以看成是:編譯器+虛擬機(jī),編譯器負(fù)責(zé)將源代碼編譯成 PyCodeObject 對(duì)象,而虛擬機(jī)則負(fù)責(zé)執(zhí)行。整個(gè)過(guò)程如下:
圖片
所以我們的重點(diǎn)就是虛擬機(jī)是怎么執(zhí)行 PyCodeObject 對(duì)象的?整個(gè)過(guò)程是什么,掌握了這些,你對(duì)虛擬機(jī)會(huì)有一個(gè)更深的理解。
虛擬機(jī)的運(yùn)行框架
在介紹棧幀的時(shí)候我們說(shuō)過(guò),Python 是一門(mén)動(dòng)態(tài)語(yǔ)言,一個(gè)變量指向什么對(duì)象需要在運(yùn)行時(shí)才能確定,這些信息不可能靜態(tài)存儲(chǔ)在 PyCodeObject 對(duì)象中。
所以虛擬機(jī)在運(yùn)行時(shí)會(huì)基于 PyCodeObject 對(duì)象動(dòng)態(tài)創(chuàng)建出一個(gè)棧幀對(duì)象,然后在棧幀里面執(zhí)行字節(jié)碼。而創(chuàng)建棧幀,主要使用以下兩個(gè)函數(shù):
// Python/ceval.c
/* 基于 PyCodeObject、全局名字空間、局部名字空間,創(chuàng)建棧幀
* 參數(shù)非常簡(jiǎn)單,所以它一般適用于模塊這種參數(shù)不復(fù)雜的場(chǎng)景
* 我們說(shuō)模塊也會(huì)對(duì)應(yīng)一個(gè)棧幀,并且它位于棧幀鏈的最頂層
*/
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals);
/* 相比 PyEval_EvalCode 多了很多的參數(shù)
* 比如里面有位置參數(shù)以及個(gè)數(shù),關(guān)鍵字參數(shù)以及個(gè)數(shù)
* 還有默認(rèn)參數(shù)以及個(gè)數(shù),閉包等等,顯然它用于函數(shù)等復(fù)雜場(chǎng)景
* 注:此 API 已廢棄,內(nèi)部不再使用
*/
PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject *const *args, int argcount,
PyObject *const *kws, int kwcount,
PyObject *const *defs, int defcount,
PyObject *kwdefs, PyObject *closure);
這兩個(gè)函數(shù)最終都會(huì)調(diào)用 _PyEval_Vector 函數(shù),這個(gè)函數(shù)內(nèi)部的邏輯我們一會(huì)兒再看??傊坏瑢?duì)象初始化完畢,那么就要進(jìn)行處理了,在棧幀中執(zhí)行字節(jié)碼(幀評(píng)估)。
關(guān)于在棧幀中執(zhí)行字節(jié)碼,虛擬機(jī)內(nèi)部有兩個(gè)重要的 C 函數(shù)。
// Python/ceval.c
PyObject *
PyEval_EvalFrame(PyFrameObject *f)
{
PyThreadState *tstate = _PyThreadState_GET();
return _PyEval_EvalFrame(tstate, f->f_frame, 0);
}
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
PyThreadState *tstate = _PyThreadState_GET();
return _PyEval_EvalFrame(tstate, f->f_frame, throwflag);
}
當(dāng)然啦,上面這兩個(gè)函數(shù)屬于高層的封裝,最終都會(huì)調(diào)用 _PyEval_EvalFrame 函數(shù)。
// Include/internal/pycore_ceval.h
static inline PyObject*
_PyEval_EvalFrame(PyThreadState *tstate,
struct _PyInterpreterFrame *frame,
int throwflag)
{
EVAL_CALL_STAT_INC(EVAL_CALL_TOTAL);
// 如果 tstate->interp->eval_frame 為 NULL
// 會(huì)調(diào)用 _PyEval_EvalFrameDefault 函數(shù)執(zhí)行字節(jié)碼
if (tstate->interp->eval_frame == NULL) {
return _PyEval_EvalFrameDefault(tstate, frame, throwflag);
}
// 另外虛擬機(jī)也支持自定義,允許我們替換掉 _PyEval_EvalFrameDefault
// 只需將具體實(shí)現(xiàn)賦值給 tstate->interp->eval_frame 即可
// 因此這個(gè)做法就使得性能分析器、調(diào)試器等工具的實(shí)現(xiàn)成為可能
return tstate->interp->eval_frame(tstate, frame, throwflag);
}
因此虛擬機(jī)最終會(huì)通過(guò) _PyEval_EvalFrameDefault 函數(shù)來(lái)完成字節(jié)碼的執(zhí)行。
目前出現(xiàn)了好幾個(gè)函數(shù),我們用一張圖來(lái)描述一下它們的關(guān)系:
圖片
當(dāng)然啦,_PyEval_Vector 函數(shù)內(nèi)部在拿到棧幀之后,其實(shí)會(huì)直接調(diào)用 _PyEval_EvalFrame。
// Python/ceval.c
PyObject *
_PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
PyObject *locals,
PyObject* const* args, size_t argcount,
PyObject *kwnames)
{
// 解釋一下相關(guān)參數(shù)的含義
/* tstate:線程狀態(tài)對(duì)象(在后續(xù)的章節(jié)剖析)
* func:函數(shù)對(duì)象(在后續(xù)的章節(jié)剖析)
* locals:局部名字空間
* args:調(diào)用函數(shù)時(shí)傳遞的參數(shù)
* argcount:通過(guò)位置參數(shù)傳遞的參數(shù)的個(gè)數(shù)
* kwnames:通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù)的名稱
舉個(gè)例子,比如調(diào)用是 func(1, 2, 3, c=4, d=5)
那么 args 就是 (1, 2, 3, 4, 5)
argcount 就是 3,因?yàn)橛?3 個(gè)參數(shù)通過(guò)位置參數(shù)的方式傳遞
kwname 則是 ("c", "d")
*/
// 增加引用計(jì)數(shù)
Py_INCREF(func);
Py_XINCREF(locals);
// 給通過(guò)位置參數(shù)傳遞的參數(shù),增加引用計(jì)數(shù)
for (size_t i = 0; i < argcount; i++) {
Py_INCREF(args[i]);
}
// 給通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù),增加引用計(jì)數(shù)
if (kwnames) {
Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames);
for (Py_ssize_t i = 0; i < kwcount; i++) {
Py_INCREF(args[i+argcount]);
}
}
// 為即將執(zhí)行的函數(shù)調(diào)用,創(chuàng)建一個(gè)新的 _PyInterpreterFrame 結(jié)構(gòu)體實(shí)例
// 然后推入虛擬機(jī)為其準(zhǔn)備的 Stack 中
_PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
tstate, func, locals, args, argcount, kwnames);
if (frame == NULL) {
return NULL;
}
// 計(jì)數(shù)器,統(tǒng)計(jì) _PyEval_Vector 的調(diào)用次數(shù),用于跟蹤,我們不用關(guān)心
EVAL_CALL_STAT_INC(EVAL_CALL_VECTOR);
// 調(diào)用 _PyEval_EvalFrame 函數(shù)
return _PyEval_EvalFrame(tstate, frame, 0);
}
然后在 _PyEval_EvalFrame 內(nèi)部又會(huì)調(diào)用 _PyEval_EvalFrameDefault 函數(shù),整個(gè)過(guò)程非常好理解。
注:棧幀指的是 PyFrameObject,它是一個(gè) Python 對(duì)象,而 _PyInterpreterFrame 是一個(gè)只包含棧幀核心信息的普通 C 結(jié)構(gòu)體,前面我們介紹過(guò)的。
但為了描述方便,后續(xù)在提到 _PyInterpreterFrame 時(shí),我們也會(huì)稱它為棧幀,這一點(diǎn)大家心里清楚就好。
所以不難發(fā)現(xiàn) _PyEval_EvalFrameDefault 函數(shù)是虛擬機(jī)運(yùn)行的核心,該函數(shù)較為復(fù)雜,我們會(huì)在下一篇文章中分析它的具體實(shí)現(xiàn)。至于本篇文章就先從宏觀的角度來(lái)描述一下虛擬機(jī)執(zhí)行字節(jié)碼的流程,并對(duì)之前的內(nèi)容做一個(gè)補(bǔ)充,將背后涉及的概念闡述一遍,這樣后續(xù)看源碼的時(shí)候也會(huì)事半功倍。
首先棧幀中有一個(gè) f_code 字段,它指向 PyCodeObject 對(duì)象,該對(duì)象的 co_code 字段則保存著字節(jié)碼指令序列。而虛擬機(jī)執(zhí)行字節(jié)碼就是從頭到尾遍歷整個(gè) co_code,對(duì)指令逐條執(zhí)行的過(guò)程。
另外也不要覺(jué)得字節(jié)碼指令(簡(jiǎn)稱指令)有多神秘,說(shuō)白了它就是個(gè) uint8 整數(shù),而一個(gè)程序肯定會(huì)包含多條指令,它們整體便構(gòu)成了指令集,或者指令序列。那么顯然,使用 bytes 對(duì)象來(lái)表示指令序列再合適不過(guò)了,如果站在 C 的角度,則就是一個(gè)普普通通的字符數(shù)組,一條指令就是一個(gè)字符、或者說(shuō)一個(gè)整數(shù)。
另外我們說(shuō)指令序列里面包含的不僅僅是指令,還有指令參數(shù),因?yàn)槊總€(gè)指令都會(huì)帶一個(gè)參數(shù)。因此索引為 0 2 4 6 8 ··· 的整數(shù)表示指令,索引為 1 3 5 7 9 ··· 的整數(shù)表示指令參數(shù)。
我們用 Python 來(lái)演示一下:
code_string = """
a = 1
b = 2
c = a + b
"""
code_object = compile(code_string, "<file>", "exec")
# 查看常量池
print(code_object.co_consts)
"""
(1, 2, None)
"""
# 查看符號(hào)表
print(code_object.co_names)
"""
('a', 'b', 'c')
"""
這些都比較簡(jiǎn)單,再來(lái)看一下反編譯的結(jié)果,直接 dis.dis(code_object) 即可。
/* 常量池:(1, 2, None)
* 符號(hào)表:('a', 'b', 'c')
*/
// 第一列表示源代碼行號(hào)
// 第二列表示指令的偏移量
// 第三列表示指令,在 C 中它們都是宏,表示整數(shù)
// 第四列表示指令參數(shù)
// 第五列是 dis 模塊為了方便我們閱讀而補(bǔ)充的提示信息
0 0 RESUME 0
// 指令:LOAD_CONST,指令參數(shù):0
// 表示從常量池中加載索引為 0 的常量,并壓入運(yùn)行時(shí)棧(關(guān)于運(yùn)行時(shí)棧,一會(huì)兒詳細(xì)說(shuō)明)
// 索引為 0 的常量顯然是 1,而括號(hào)里面的提示信息顯示的也是 1
2 2 LOAD_CONST 0 (1)
// 指令:STORE_NAME,指令參數(shù):0
// 表示從符號(hào)表中加載索引為 0 的符號(hào),顯然結(jié)果是 "a"
// 然后彈出運(yùn)行時(shí)棧的棧頂元素,顯然是上一條指令壓入的 1
// 將 "a" 和 1 組成鍵值對(duì),存儲(chǔ)在當(dāng)前的名字空間中
// 到此 a = 1 這條語(yǔ)句便完成了,或者說(shuō)完成了變量和值的綁定
4 STORE_NAME 0 (a)
// 從常量池中加載索引為 1 的常量(結(jié)果是 2),并壓入運(yùn)行時(shí)棧
3 6 LOAD_CONST 1 (2)
// 從符號(hào)表中加載索引為 1 的符號(hào)(結(jié)果是 "b")
// 然后從棧頂彈出元素 2,將 "b" 和 2 綁定起來(lái)
8 STORE_NAME 1 (b)
// 加載符號(hào)表中索引為 0 的符號(hào)對(duì)應(yīng)的值,并壓入運(yùn)行時(shí)棧
4 10 LOAD_NAME 0 (a)
// 加載符號(hào)表中索引為 1 的符號(hào)對(duì)應(yīng)的值,并壓入運(yùn)行時(shí)棧
12 LOAD_NAME 1 (b)
// 將運(yùn)行時(shí)棧的兩個(gè)元素彈出,并執(zhí)行加法運(yùn)算
// 將運(yùn)算之后的結(jié)果(a + b)再壓入運(yùn)行時(shí)棧
14 BINARY_OP 0 (+)
// 從符號(hào)表中加載索引為 2 的符號(hào),結(jié)果是 "c"
// 將運(yùn)行時(shí)棧的棧頂元素彈出,這里是 a + b 的運(yùn)算結(jié)果
// 然后進(jìn)行綁定,完成 c = a + b 這條賦值語(yǔ)句
18 STORE_NAME 2 (c)
// 從常量池中加載索引為 2 的元素并返回,有一個(gè)隱式的 return None
20 RETURN_CONST 2 (None)
這些指令的源碼實(shí)現(xiàn)后續(xù)都會(huì)說(shuō),但是不難發(fā)現(xiàn),程序的主干邏輯都體現(xiàn)在字節(jié)碼中,而依賴的信息則由其它字段來(lái)維護(hù)。所謂執(zhí)行源代碼,其實(shí)就是虛擬機(jī)執(zhí)行編譯之后的字節(jié)碼。通過(guò)遍歷 co_code,然后基于不同的指令,執(zhí)行不同的邏輯。
然后我們基于上面這些輸出信息,看看能否將字節(jié)碼指令集還原出來(lái),當(dāng)然在還原之前首先要知道這些指令代表的數(shù)值是多少。
圖片
下面我們來(lái)進(jìn)行還原。
"""
0 RESUME 0
2 LOAD_CONST 0
4 STORE_NAME 0
6 LOAD_CONST 1
8 STORE_NAME 1
10 LOAD_NAME 0
12 LOAD_NAME 1
14 BINARY_OP 0 (+)
18 STORE_NAME 2
20 RETURN_CONST 2
"""
CACHE = 0
STORE_NAME = 90
LOAD_CONST = 100
LOAD_NAME = 101
RETURN_CONST = 121
BINARY_OP = 122
RESUME = 151
codes = [
RESUME, 0,
# a = 1
LOAD_CONST, 0,
STORE_NAME, 0,
# b = 2
LOAD_CONST, 1,
STORE_NAME, 1,
# c = a + b
LOAD_NAME, 0, # 加載 a
LOAD_NAME, 1, # 加載 b
BINARY_OP, 0, # 計(jì)算 a + b
# 從 dis 的輸出信息可以看到,索引為 16 的指令沒(méi)有顯示
# 這是因?yàn)樗饕秊?16 的指令和相應(yīng)的指令參數(shù)都是 0
# 也就是說(shuō),在將 c 和 a + b 綁定之前,插入了一條 CACHE 指令
# 這條指令是做什么的,我們后續(xù)再探討
CACHE, 0,
STORE_NAME, 2,
# 所有代碼塊都隱式地包含了一個(gè) return None
RETURN_CONST, 2
]
print(bytes(codes))
"""
b'\x97\x00d\x00Z\x00d\x01Z\x01e\x00e\x01z\x00\x00\x00Z\x02y\x02'
"""
那么字節(jié)碼是不是我們還原的這個(gè)樣子呢?來(lái)對(duì)比一下。
結(jié)果是一樣的,到此相信你對(duì) Python 源代碼的執(zhí)行過(guò)程應(yīng)該有更深的了解了,簡(jiǎn)單來(lái)講,其實(shí)就是以下幾個(gè)步驟。
1)源代碼被編譯成 PyCodeObject 對(duì)象,該對(duì)象的 co_code 字段指向字節(jié)碼指令序列,它包含了程序執(zhí)行的主干邏輯,剩余字段則保存常量池、符號(hào)表等其它靜態(tài)信息。
2)虛擬機(jī)在 PyCodeObject 對(duì)象的基礎(chǔ)上構(gòu)建棧楨對(duì)象。
3)虛擬機(jī)在棧楨對(duì)象內(nèi)部執(zhí)行字節(jié)碼(幀評(píng)估),具體流程就是遍歷指令集和,根據(jù)不同指令執(zhí)行不同的處理邏輯,而這一過(guò)程便由 _PyEval_EvalFrameDefault 函數(shù)負(fù)責(zé)完成。
什么是運(yùn)行時(shí)棧
之前一直提到一個(gè)概念,叫運(yùn)行時(shí)棧,那什么是運(yùn)行時(shí)棧呢?別急,我們先來(lái)回顧一下棧楨的基本結(jié)構(gòu)。
圖片
大部分字段都很好理解,因?yàn)橹巴ㄟ^(guò) Python 代碼演示過(guò)。但有幾個(gè)字段是虛擬機(jī)用于執(zhí)行指令的,后續(xù)會(huì)遇到,所以這里再拿出來(lái)解釋一下。
prev_instr
指向上一條剛執(zhí)行完的字節(jié)碼指令,因?yàn)槊總€(gè)指令要帶一個(gè)參數(shù),所以該字段的類型為 uint16 *,前 8 字節(jié)表示指令,后 8 字節(jié)表示參數(shù)。假設(shè)虛擬機(jī)要執(zhí)行偏移量為 14 的指令,那么 prev_instr 便指向偏移量為 12 的指令。
localsplus
一個(gè)柔性數(shù)組,它的內(nèi)存大小被分為 4 個(gè)部分。
注:localsplus 是一個(gè)數(shù)組,所以它是一段連續(xù)的內(nèi)存,只不過(guò)按照用途被分成了 4 個(gè)部分。如果用新一團(tuán)團(tuán)長(zhǎng)丁偉的說(shuō)法:每個(gè)部分之間是雞犬相聞,但又老死不相往來(lái)。
然后再著重強(qiáng)調(diào)一下運(yùn)行時(shí)棧,虛擬機(jī)在執(zhí)行字節(jié)碼指令時(shí)高度依賴它,因?yàn)橐粋€(gè)指令只能帶一個(gè)參數(shù),那么剩余的參數(shù)就必須通過(guò)運(yùn)行時(shí)棧給出。比如 a = 1 會(huì)對(duì)應(yīng)兩條字節(jié)碼:LOAD_CONST 和 STORE_NAME。
STORE_NAME 的作用是從符號(hào)表中獲取符號(hào),或者說(shuō)變量名,然后和值綁定起來(lái)。而要加載符號(hào),那么必須要知道它在符號(hào)表中的索引,顯然這可以通過(guò)指令參數(shù)給出,但問(wèn)題是與之綁定的值怎么獲???
毫無(wú)疑問(wèn),要通過(guò)運(yùn)行時(shí)棧,因此 LOAD_CONST 將值讀取進(jìn)來(lái)之后,還要壓入運(yùn)行時(shí)棧。然后 STORE_NAME 會(huì)將值從運(yùn)行時(shí)棧中彈出,從而完成符號(hào)(變量)和值的綁定。關(guān)于運(yùn)行時(shí)棧,我們?cè)倏磦€(gè)復(fù)雜的例子:
圖片
偏移量為 8 的指令表示要構(gòu)建一個(gè)字典,指令參數(shù) 2 表示構(gòu)建的字典的長(zhǎng)度為 2,但問(wèn)題是字典的鍵值對(duì)在什么地方?顯然它們已經(jīng)被提前壓入了運(yùn)行時(shí)棧,執(zhí)行 BUILD_CONST_KEY_MAP 的時(shí)候直接彈出即可。
所以這就是運(yùn)行時(shí)棧的作用,如果某個(gè)指令需要 n 個(gè)參數(shù),那么其中的 n - 1 個(gè)必須要提前壓入運(yùn)行時(shí)棧,然后在該指令執(zhí)行的時(shí)候依次彈出,因?yàn)橐粋€(gè)指令只能帶一個(gè)參數(shù)。
stacktop
運(yùn)行時(shí)棧的棧頂相對(duì)于 localsplus 數(shù)組的偏移量,那么顯然 localsplus 加上 stacktop 便指向運(yùn)行時(shí)棧的棧頂。
我們通過(guò)源碼驗(yàn)證一下:
// Inlcude/internal/pycore_frame.h
// 獲取 localsplus 字段
static inline PyObject**
_PyFrame_GetLocalsArray(_PyInterpreterFrame *frame)
{
return frame->localsplus;
}
// 獲取指針(PyObject **),它指向運(yùn)行時(shí)棧的棧頂,我們稱之為 stack_pointer
// 而元素的入棧和出棧,顯然都是通過(guò)操作 stack_pointer 完成的
// 執(zhí)行 *stack_pointer++ = v,一個(gè)元素就入棧了
// 執(zhí)行 v = *--stack_pointer,一個(gè)元素就出棧了
static inline PyObject**
_PyFrame_GetStackPointer(_PyInterpreterFrame *frame)
{
// localsplus + stacktop 便指向運(yùn)行時(shí)棧的棧頂
// 因?yàn)?stacktop 的含義就是棧頂相對(duì)于 localsplus 數(shù)組(首元素的地址)的偏移量
PyObject **sp = frame->localsplus + frame->stacktop;
frame->stacktop = -1;
return sp;
}
// 隨著元素的入棧和出棧,運(yùn)行時(shí)棧的棧頂、或者說(shuō) stack_pointer 也在不斷變化
// 而 frame->stacktop 表示棧頂相對(duì)于 localsplus 的偏移量
// 那么顯然,由于棧頂發(fā)生變化,后續(xù)還要對(duì) frame->stacktop 進(jìn)行更新
static inline void
_PyFrame_SetStackPointer(_PyInterpreterFrame *frame, PyObject **stack_pointer)
{
frame->stacktop = (int)(stack_pointer - frame->localsplus);
}
這就是 stacktop 字段的作用,用于獲取運(yùn)行時(shí)棧的棧頂指針。另外我們說(shuō) localsplus 數(shù)組被分成了 4 份,最后一份給了運(yùn)行時(shí)棧,因此雖然我們稱之為棧,但它其實(shí)就是一個(gè)數(shù)組,而且還是數(shù)組的一部分。
而對(duì)于數(shù)組而言,內(nèi)存地址從左往右是增大的。
這是 localsplus 的示意圖,我們只看運(yùn)行時(shí)棧,目前棧里面有兩個(gè)元素,stack_pointer 指向棧頂。
這時(shí)如果要添加一個(gè)元素 3,那么直接 *stack_pointer++ = 3 即可。
如果要將棧頂元素彈出,那么執(zhí)行 v = *--stack_pointer 即可。
圖片
而 stack_pointer - localsplus 便是 stacktop。
運(yùn)行時(shí)棧的一些 API
相信你對(duì)運(yùn)行時(shí)棧的了解已經(jīng)足夠深了,說(shuō)白了它就是參數(shù)的容身之所,比如虛擬機(jī)在執(zhí)行 a + b 的時(shí)候,通過(guò)指令參數(shù)可以判斷這是一個(gè)加法操作。但在執(zhí)行加法的時(shí)候,加號(hào)兩邊的值要怎么獲取呢?這時(shí)候就需要一個(gè)棧來(lái)專門(mén)保存相應(yīng)的參數(shù)。
在執(zhí)行加法之前,先將 a 和 b 壓入棧中,等執(zhí)行加法的時(shí)候,再將 a 和 b 從棧里面彈出來(lái)即可,非常簡(jiǎn)單。
然后再來(lái)看看運(yùn)行時(shí)棧相關(guān)的一些 API。
圖片
API 非常多,它們都定義在 Python/ceval_macros.h 中,我們來(lái)逐一介紹。假設(shè)目前運(yùn)行時(shí)棧內(nèi)部有三個(gè)元素,從棧底到棧頂分別是整數(shù) 1、2、3,那么運(yùn)行時(shí)棧的結(jié)構(gòu)就是下面這樣。
圖片
localsplus 數(shù)組被分成 4 個(gè)區(qū)域,運(yùn)行時(shí)棧占據(jù)最后一個(gè),因此圖中的灰色區(qū)域便是運(yùn)行時(shí)棧之前的內(nèi)存。由于我們是研究運(yùn)行時(shí)棧,所以這部分區(qū)域后續(xù)就不再畫(huà)了。
然后看一下這些和運(yùn)行時(shí)棧相關(guān)的 API 都是干嘛的。
STACK_LEVEL()
返回運(yùn)行時(shí)棧的元素個(gè)數(shù),那么問(wèn)題來(lái)了,通過(guò) stack_pointer 可以獲取棧頂指針,但棧底指針怎么獲取呢?畢竟有了棧底指針,兩者相減就完事了。
#define STACK_LEVEL() ((int)(stack_pointer - _PyFrame_Stackbase(frame)))
顯然 _PyFrame_Stackbase 負(fù)責(zé)獲取運(yùn)行時(shí)棧的棧底(基址),看一下它的具體實(shí)現(xiàn)。
// Include/internal/pycore_frame.h
// PyCodeObject 對(duì)象里面有以下幾個(gè)字段
// co_nlocals:代碼塊中局部變量的個(gè)數(shù),也包括參數(shù)
// co_ncellvars:cell 變量的個(gè)數(shù),即 co_cellvars 的長(zhǎng)度
// co_nfreevars:free 變量的個(gè)數(shù),即 co_freevars 的長(zhǎng)度
// co_nlocalsplus:局部變量、cell 變量、free 變量的個(gè)數(shù)之和
static inline PyObject **_PyFrame_Stackbase(_PyInterpreterFrame *f) {
// 由于 co_nlocalsplus = co_nlocals + co_ncellvars + co_nfreevars
// 那么顯然,f->localsplus + f->f_code->co_nlocalsplus
// 便指向運(yùn)行時(shí)棧的棧底,或者說(shuō)運(yùn)行時(shí)棧對(duì)應(yīng)的內(nèi)存區(qū)域的首元素
return f->localsplus + f->f_code->co_nlocalsplus;
}
在之前的版本中(比如 3.8),棧楨內(nèi)部會(huì)有一個(gè) f_valuestack 字段,專門(mén)負(fù)責(zé)指向運(yùn)行時(shí)棧的棧底。注:STACK_LEVEL() 是動(dòng)態(tài)變化的,因?yàn)?stack_pointer 會(huì)動(dòng)態(tài)變化。
STACK_SIZE()
運(yùn)行時(shí)棧最多能存儲(chǔ)多少個(gè)元素,或者說(shuō)運(yùn)行時(shí)棧的長(zhǎng)度。
#define STACK_SIZE() (frame->f_code->co_stacksize)
我們看到它返回了 PyCodeObject 的 co_stacksize 字段,之前介紹 PyCodeObject 對(duì)象時(shí),我們說(shuō) co_stacksize 表示執(zhí)行代碼塊所需要的??臻g。這個(gè)描述其實(shí)會(huì)讓人感到困惑,不過(guò)當(dāng)了解完運(yùn)行時(shí)棧之后就清晰了,其實(shí)它表示運(yùn)行時(shí)棧最多能存儲(chǔ)多少個(gè)元素,或者說(shuō)運(yùn)行時(shí)棧的長(zhǎng)度。也可以理解為要想執(zhí)行這段代碼塊,后續(xù)創(chuàng)建棧楨時(shí),應(yīng)該給 localsplus 數(shù)組中表示運(yùn)行時(shí)棧的區(qū)域分配能存儲(chǔ)多少個(gè)元素的內(nèi)存。
比如 co_stacksize 是 1,那么表示應(yīng)該給運(yùn)行時(shí)棧分配能存儲(chǔ) 1 個(gè)元素的內(nèi)存,即運(yùn)行時(shí)棧的長(zhǎng)度為 1。我們通過(guò)反編譯的方式,實(shí)際演示一下。
import dis
def some_func():
a = 1
b = 2
c = 3
# 這里我們只保留字節(jié)碼指令
dis.dis(some_func)
"""
RESUME
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
RETURN_CONST
"""
# 也就是說(shuō),運(yùn)行時(shí)棧只要能容納一個(gè)元素,即可執(zhí)行這段代碼
print(some_func.__code__.co_stacksize) # 1
我們?cè)賮?lái)看個(gè)例子。
import dis
def some_func():
a = 1
b = 2
c = 3
lst = [a, b, c]
dis.dis(some_func)
"""
RESUME
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
LOAD_CONST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
LOAD_FAST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 1
LOAD_FAST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 2
LOAD_FAST # 將元素壓入運(yùn)行時(shí)棧,棧里的元素個(gè)數(shù)為 3
BUILD_LIST # 將棧里的三個(gè)元素彈出,構(gòu)建列表并入棧(此時(shí)元素個(gè)數(shù)為 1)
STORE_FAST # 將元素從棧頂彈出,棧里的元素個(gè)數(shù)為 0
RETURN_CONST
"""
# 不難看出,要想執(zhí)行這段代碼,運(yùn)行時(shí)棧要能容納 3 個(gè)元素
print(some_func.__code__.co_stacksize) # 3
相信你現(xiàn)在應(yīng)該理解 co_stacksize 的作用了,它表示運(yùn)行時(shí)棧最多能容納多少個(gè)元素,也就是運(yùn)行時(shí)棧的長(zhǎng)度。以上面代碼為例,由于最多會(huì)壓入 3 個(gè)元素,所以運(yùn)行時(shí)棧的長(zhǎng)度就是 3,即最多能容納 3 個(gè)元素。并且這個(gè)長(zhǎng)度在編譯之后就已經(jīng)確定了,因?yàn)榭梢酝ㄟ^(guò)遍歷指令集靜態(tài)計(jì)算出來(lái)。
我們畫(huà)一張圖描述一下執(zhí)行上面的代碼時(shí),運(yùn)行時(shí)棧的變化過(guò)程。
圖片
整個(gè)過(guò)程應(yīng)該很清晰,當(dāng)然上面只是運(yùn)行時(shí)棧的變化,localsplus 中存儲(chǔ)局部變量的內(nèi)存區(qū)域也在變化。另外如果代碼塊位于全局作用域,那么變化的就是全局名字空間。
EMPTY()
#define EMPTY() (STACK_LEVEL() == 0)
判斷運(yùn)行時(shí)棧是否為空,顯然只需判斷運(yùn)行時(shí)棧的元素個(gè)數(shù)是否為 0 即可。
TOP()
#define TOP() (stack_pointer[-1])
查看當(dāng)前運(yùn)行時(shí)棧的棧頂元素。
SECOND()
#define SECOND() (stack_pointer[-2])
查看從棧頂元素開(kāi)始的第二個(gè)元素。
THIRD()
#define THIRD() (stack_pointer[-3])
查看從棧頂元素開(kāi)始的第三個(gè)元素。
FOURTH()
#define FOURTH() (stack_pointer[-4])
查看從棧頂元素開(kāi)始的第四個(gè)元素。
PEEK(n):
#define PEEK(n) (stack_pointer[-(n)])
查看從棧頂元素開(kāi)始的第 n 個(gè)元素。
SET_TOP(v)
#define SET_TOP(v) (stack_pointer[-1] = (v))
將當(dāng)前運(yùn)行時(shí)棧的棧頂元素設(shè)置成 v。
SET_SECOND(v)
#define SET_SECOND(v) (stack_pointer[-2] = (v))
將從棧頂元素開(kāi)始的第二個(gè)元素設(shè)置成 v。
POKE(n)
#define POKE(n, v) (stack_pointer[-(n)] = (v))
將從棧頂元素開(kāi)始的第 n 個(gè)元素設(shè)置成 v。
PUSH(v)
#define PUSH(v) BASIC_PUSH(v)
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
往運(yùn)行時(shí)棧中壓入一個(gè)元素,并且壓入之后,棧中已存儲(chǔ)的元素個(gè)數(shù)一定不超過(guò) co_stacksize。換句話說(shuō),STACK_LEVEL() 永遠(yuǎn)小于等于 STACK_SIZE()。
然后入棧和出棧,都是通過(guò)操作 stack_pointer 完成的。假設(shè)當(dāng)前棧里有一個(gè)元素 1,然后添加一個(gè)元素 2。
圖片
Python 的變量都是一個(gè)指針,所以 stack_pointer 是一個(gè)二級(jí)指針,它永遠(yuǎn)指向棧頂位置,只不過(guò)棧頂位置會(huì)變。
注:運(yùn)行時(shí)棧的內(nèi)存一開(kāi)始就申請(qǐng)好了,初始狀態(tài)下,里面的元素全部為 NULL。而往棧里面壓入一個(gè)元素,其實(shí)就是修改 stack_pointer 指向的內(nèi)存單元,然后執(zhí)行 stack_pointer++。
POP()
#define POP() BASIC_POP()
#define BASIC_POP() (*--stack_pointer)
彈出棧頂元素,注意它和 TOP 的區(qū)別,TOP 是返回棧頂元素,但不彈出。
圖片
stack_pointer 指向棧頂位置,所以它向棧底移動(dòng)一個(gè)位置,就相當(dāng)于元素被彈出了。
關(guān)于彈出元素需要做一個(gè)說(shuō)明,所謂彈出元素本質(zhì)上就是將 stack_pointer 向棧底移動(dòng)一個(gè)位置。我們看一下上圖,一開(kāi)始棧里面有兩個(gè)元素,分別是整數(shù) 1 和整數(shù) 2,當(dāng)然準(zhǔn)確來(lái)說(shuō)應(yīng)該是指向它們的指針,但為了描述方便,我們就用對(duì)象本身代替了。
然后執(zhí)行 POP(),將整數(shù) 2 彈出,但我們發(fā)現(xiàn) POP() 之后,整數(shù) 2 還在棧里面。關(guān)于這一點(diǎn)很好理解,因?yàn)?stack_pointer 始終指向棧頂位置,而它向棧底移動(dòng)了一個(gè)位置,那么整數(shù) 2 就已經(jīng)不是棧頂元素了。當(dāng)下一個(gè)元素入棧時(shí),會(huì)把整數(shù) 2 替換掉。因此雖然整數(shù) 2 還在運(yùn)行時(shí)棧里面,但和不在沒(méi)有任何區(qū)別,此時(shí)我們依舊認(rèn)為整數(shù) 2 被彈出了。
不過(guò)在后續(xù)的文章中,在畫(huà)運(yùn)行時(shí)棧的時(shí)候,我們會(huì)這么畫(huà)。
圖片
為了閱讀清晰,stack_pointer 之后的元素就不寫(xiě)了。另外還要注意一點(diǎn),運(yùn)行時(shí)棧的內(nèi)存一開(kāi)始就已經(jīng)申請(qǐng)好了,是固定的,彈出元素只是改變棧頂指針 stack_pointer 的指向,內(nèi)存區(qū)域的大小不變。
當(dāng)然這些內(nèi)容都比較簡(jiǎn)單,但為了避免出現(xiàn)歧義,這里單獨(dú)解釋一下。
STACK_GROW(n)
#define STACK_GROW(n) BASIC_STACKADJ(n)
#define BASIC_STACKADJ(n) (stack_pointer += n)
改變運(yùn)行時(shí)棧的棧頂,注:運(yùn)行時(shí)棧的大小是固定的,但棧頂是由 stack_pointer 決定的。
圖片
那么問(wèn)題來(lái)了,假設(shè)要往運(yùn)行時(shí)棧壓入兩個(gè)元素,分別是 2、3,該怎么做呢?首先肯定可以通過(guò) PUSH 實(shí)現(xiàn)。
PUSH(2);
PUSH(3);
但如果不讓你用 PUSH,該怎么做呢?
STACK_GROW(2);
// 設(shè)置元素
POKE(1, 3); // stack_pointer[-1] = 3
POKE(2, 2); // stack_pointer[-2] = 2
兩種做法都是可以的。
STACK_SHRINK(n)
#define STACK_SHRINK(n) BASIC_STACKADJ(-(n))
#define BASIC_STACKADJ(n) (stack_pointer += n)
它的作用和 STACK_GROWN 正好相反。
圖片
以上就是運(yùn)行時(shí)棧的一些 API,后續(xù)再看源碼的時(shí)候,會(huì)經(jīng)常遇到。
小結(jié)
本篇文章我們從宏觀的角度介紹了虛擬機(jī)執(zhí)行字節(jié)碼的流程,說(shuō)白了虛擬機(jī)就是把自己當(dāng)成一個(gè) CPU,在棧楨中執(zhí)行指令。通過(guò)遍歷字節(jié)碼指令集,基于不同的指令執(zhí)行不同的處理邏輯。
然后是運(yùn)行時(shí)棧,因?yàn)橐粋€(gè)指令只能帶一個(gè)參數(shù),那么剩余的參數(shù)就需要通過(guò)運(yùn)行時(shí)棧給出。比如 LOAD_CONST 指令,它在加載完常量之后,會(huì)將常量壓入運(yùn)行時(shí)棧,然后 STORE_NAME 或 STORE_FAST 指令再將常量從運(yùn)行時(shí)棧的頂部彈出,和某個(gè)符號(hào)(變量)綁定起來(lái)。另外關(guān)于這些指令,我們后面會(huì)詳細(xì)說(shuō)。
最后我們介紹了運(yùn)行時(shí)棧的一些 API,或者說(shuō)宏,因?yàn)閳?zhí)行指令的時(shí)候會(huì)反復(fù)操作運(yùn)行時(shí)棧,所以底層封裝了很多的宏。