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















 
 
 










 
 
 
 