深入理解 python 虛擬機器:位元組碼教程(2)——控制流是如何實現的?

一無是處的研究僧發表於2023-04-10

深入理解 python 虛擬機器:位元組碼教程(2)——控制流是如何實現的?

在本篇文章當中主要給大家分析 python 當中與控制流有關的位元組碼,透過對這部分位元組碼的瞭解,我們可以更加深入瞭解 python 位元組碼的執行過程和控制流實現原理。

控制流實現

控制流這部分程式碼主要涉及下面幾條位元組碼指令,下面的所有位元組碼指令都會有一個引數:

  • JUMP_FORWARD,指令完整條指令會將當前執行位元組碼指令的位置加上這個引數,然後跳到對應的結果繼續執行。
  • POP_JUMP_IF_TRUE,如果棧頂元素等於 true,將位元組碼的執行位置改成引數的值。將棧頂元素彈出。
  • POP_JUMP_IF_FALSE,這條指令和 POP_JUMP_IF_TRUE 一樣,唯一差別就是判斷棧頂元素是否等於 true。
  • JUMP_IF_TRUE_OR_POP,如果棧頂元素等於等於 true 則將位元組碼執行位置設定成引數對應的值,並且不需要將棧頂元素彈出。但是如果棧頂元素是 false 的話那麼就需要將棧頂元素彈出。
  • JUMP_IF_FALSE_OR_POP,和JUMP_IF_TRUE_OR_POP一樣只不過需要棧頂元素等於 false 。
  • JUMP_ABSOLUTE,直接將位元組碼的執行位置設定成引數的值。

總的來說,這些跳轉指令可以讓 Python 的直譯器在執行位元組碼時根據特定條件來改變執行流程,實現迴圈、條件語句等基本語言結構。

現在我們使用一個例子來深入理解上面的各種指令的執行過程。

import dis


def test_control01():
    a = 1

    if a > 1:
        print("a > 1")
    elif a < 1:
        print("a < 1")
    else:
        print("a == 1")

if __name__ == '__main__':
    dis.dis(test_control01)

上面的程式輸出結果如下所示:

  6           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  8           4 LOAD_FAST                0 (a)
              6 LOAD_CONST               1 (1)
              8 COMPARE_OP               4 (>)
             10 POP_JUMP_IF_FALSE       22

  9          12 LOAD_GLOBAL              0 (print)
             14 LOAD_CONST               2 ('a > 1')
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD            26 (to 48)

 10     >>   22 LOAD_FAST                0 (a)
             24 LOAD_CONST               1 (1)
             26 COMPARE_OP               0 (<)
             28 POP_JUMP_IF_FALSE       40

 11          30 LOAD_GLOBAL              0 (print)
             32 LOAD_CONST               3 ('a < 1')
             34 CALL_FUNCTION            1
             36 POP_TOP
             38 JUMP_FORWARD             8 (to 48)

 13     >>   40 LOAD_GLOBAL              0 (print)
             42 LOAD_CONST               4 ('a == 1')
             44 CALL_FUNCTION            1
             46 POP_TOP
        >>   48 LOAD_CONST               0 (None)
             50 RETURN_VALUE

我們現在來模擬一下上面的位元組碼執行過程,我們使用 counter 表示當前位元組碼的執行位置:

在位元組碼還沒開始執行之前,棧空間和 counter 的狀態如下:

現在執行第一條位元組碼 LOAD_CONST,執行完之後 counter = 2,因為這條位元組碼佔一個位元組,引數棧一個位元組,因此下次執行的位元組碼的位置在 bytecode 的低三個位置,對應的下標為 2,因此 counter = 2 。

現在執行第二條位元組碼 STORE_FAST,讓 a 指向 1 ,同樣的 STORE_FAST 操作碼和運算元各佔一個位元組,因此執行完這條位元組碼之後棧空間沒有資料,counter = 4 。

接下來 LOAD_FAST 將 a 指向的物件也就是 1 載入進入棧中,此時的 counter = 6,LOAD_CONST 將常量 1 載入進行入棧空間當中,此時 counter = 8,在執行完這兩條指令之後,棧空間的變化如下圖所示:

接下來的一條指令是 COMPARE_OP ,這個指令有一個參數列示比較的符號,這裡是比較 a > 1,並且會將比較的結果壓入棧中,比較的結果是 false ,因為 COMPARE_OP 首先會將棧空間的兩個輸入彈出,因此在執行完這條指令之後棧空間和 counter 的值如下:

下面一條指令為 POP_JUMP_IF_FALSE,根據前面的位元組碼含義,這個位元組碼會將棧頂的 false 彈出,並且會進行跳轉,並且將 counter 的值直接程式設計引數的值,這裡他的引數是 22 ,因此 counter = 22,在執行完這條指令之後,結果如下:

因為現在已經跳轉到了 22 ,因此接下來執行的指令為 LOAD_FAST,將變數 a 載入進入棧空間,LOAD_CONST 將常量 1 載入進入棧空間,在執行完這兩條執行之後,變化情況如下:

在次執行 POP_JUMP_IF_FALSE,這回的結果也是 false ,因此繼續執行 POP_JUMP_IF_FALSE,這次的引數是 40,直接將 counter 的值設定成 40 。

接下來 LOAD_GLOBAL 載入一個全域性變數 print 函式 counter 變成 42 ,LOAD_CONST 載入字串 "a == 1" 進入棧空間,counter = 44,此時狀態如下:

CALL_FUNCTION 這個位元組碼有一個引數,表示呼叫函式的引數的個數,這裡是 1,因為 print 函式只有一個引數,然後輸出字串 "a== 1",但是這裡需要注意的是 print 函式會返回一個 None,因此執行完 CALL_FUNCTION 之後狀態如下:

至此差不多上面的函式差不多執行完了,後面幾條位元組碼很簡單,就不再進行敘述了。

總結

在 Python 中,控制流指令可以讓直譯器根據特定條件改變執行流程,實現迴圈、條件語句等基本語言結構。Python 中與控制流有關的位元組碼指令包括 JUMP_FORWARD、POP_JUMP_IF_TRUE、POP_JUMP_IF_FALSE、JUMP_IF_TRUE_OR_POP、JUMP_IF_FALSE_OR_POP 和 JUMP_ABSOLUTE 等。這些指令都有一個引數,主要是用來計算跳轉的目標位置等。透過對這些指令的瞭解,我們可以更深入地理解 Python 位元組碼的執行過程和控制流實現原理。


本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。

相關文章