自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

流程控制語(yǔ)句 if 是怎么實(shí)現(xiàn)的?

開(kāi)發(fā) 前端
本篇文章我們就分析了 if 語(yǔ)句的實(shí)現(xiàn)原理,總的來(lái)說(shuō)不難理解。依舊是在棧楨中執(zhí)行字節(jié)碼,只是多了一個(gè)指令跳轉(zhuǎn)罷了,至于怎么跳轉(zhuǎn)、跳轉(zhuǎn)到什么地方,全部都體現(xiàn)在字節(jié)碼中。?

楔子

前面我們分析了虛擬機(jī)執(zhí)行字節(jié)碼的原理,并且也介紹了不少指令,但這些指令都是從上往下順序執(zhí)行的,不涉及任何的跳轉(zhuǎn)。而像流程控制語(yǔ)句,比如 if、for、while、try 等等,它們?cè)趫?zhí)行時(shí)會(huì)發(fā)生跳轉(zhuǎn),因此 Python 底層一定還存在相應(yīng)的跳轉(zhuǎn)指令。

那么從現(xiàn)在開(kāi)始,就來(lái)分析一下這些流程控制語(yǔ)句的實(shí)現(xiàn)原理,本文先來(lái)介紹 if 語(yǔ)句。

if 字節(jié)碼

if 語(yǔ)句應(yīng)該是最簡(jiǎn)單也是最常用的流程控制語(yǔ)句,那么它的字節(jié)碼是怎么樣的呢?當(dāng)然這里的 if 語(yǔ)句指的是 if elif else 整體,里面的某個(gè)條件叫做該 if 語(yǔ)句的分支。

我們看一下 if 語(yǔ)句的字節(jié)碼長(zhǎng)什么樣子。

import dis

code_string = """
score = 90

if score >= 85:
    print("Good")
    
elif score >= 60:
    print("Normal")

else:
    print("Bad")
"""

dis.dis(compile(code_string, "<file>", "exec"))

反編譯得到的字節(jié)碼指令比較多,我們來(lái)慢慢分析。另外為了閱讀方便,源代碼行號(hào)就不顯示了。

0 RESUME                   0
      # 加載常量 90 并壓入運(yùn)行時(shí)棧
      2 LOAD_CONST               0 (90)
      # 加載符號(hào)表中索引為 0 的符號(hào) "score"
      # 彈出運(yùn)行時(shí)棧的棧頂元素 90
      # 然后將兩者綁定起來(lái),存放在當(dāng)前的名字空間中
      4 STORE_NAME               0 (score)
      
      # 加載變量 score
      6 LOAD_NAME                0 (score)
      # 加載常量 85
      8 LOAD_CONST               1 (85)
      # 進(jìn)行比較,操作符是 >=,這個(gè)指令之前介紹過(guò)的
     10 COMPARE_OP              92 (>=)
      # 如果比較結(jié)果為 False,就進(jìn)行跳轉(zhuǎn),從名字也能看出指令的含義
      # 那么跳轉(zhuǎn)到什么地方呢?指令參數(shù) 9 表示向前跳轉(zhuǎn) 9 個(gè)指令
      # 根據(jù)后面的提示,我們看到會(huì)跳轉(zhuǎn)到偏移量為 34 的指令
      # 很明顯,就是當(dāng)前分支的下一個(gè)分支。關(guān)于具體是怎么跳轉(zhuǎn)的,一會(huì)兒說(shuō)
     14 POP_JUMP_IF_FALSE        9 (to 34)
      # 如果走到這里說(shuō)明沒(méi)有跳轉(zhuǎn),當(dāng)前分支的條件為真
      # 那么開(kāi)始執(zhí)行該分支內(nèi)部的邏輯
      # PUSH_NULL 指令做的事情很簡(jiǎn)單,就是往棧里 PUSH 一個(gè) NULL
     16 PUSH_NULL
      # 以下 4 條指令對(duì)應(yīng) print("Good")
     18 LOAD_NAME                1 (print)
     20 LOAD_CONST               2 ('Good')
     22 CALL                     1
     30 POP_TOP
      # if 語(yǔ)句只有一個(gè)分支會(huì)被執(zhí)行
      # 如果執(zhí)行了某個(gè)分支,那么整個(gè) if 語(yǔ)句就結(jié)束了
     32 RETURN_CONST             6 (None)
      
      # 對(duì)應(yīng) score >= 60
>>   34 LOAD_NAME                0 (score)
     36 LOAD_CONST               3 (60)
     38 COMPARE_OP              92 (>=)
      # 如果比較結(jié)果為假,跳轉(zhuǎn)到偏移量為 62 的指令
     42 POP_JUMP_IF_FALSE        9 (to 62)
      # 和上面類(lèi)似
     44 PUSH_NULL
     46 LOAD_NAME                1 (print)
     48 LOAD_CONST               4 ('Normal')
     50 CALL                     1
     58 POP_TOP
     60 RETURN_CONST             6 (None)
      
      # 最后一個(gè)是 else 分支,而 else 分支沒(méi)有判斷條件
      # 邏輯依舊和上面類(lèi)似
>>   62 PUSH_NULL
     64 LOAD_NAME                1 (print)
     66 LOAD_CONST               5 ('Bad')
     68 CALL                     1
     76 POP_TOP
     78 RETURN_CONST             6 (None)

我們看到字節(jié)碼偏移量之前有幾個(gè) >> 這樣的符號(hào),顯然這是 if 語(yǔ)句中的每一個(gè)分支開(kāi)始的地方。

經(jīng)過(guò)分析,整個(gè) if 語(yǔ)句的字節(jié)碼指令還是很簡(jiǎn)單的。就是從上到下依次判斷每一個(gè)分支,如果某個(gè)分支條件成立,就執(zhí)行該分支的代碼,執(zhí)行完畢后結(jié)束整個(gè) if 語(yǔ)句;否則跳轉(zhuǎn)到下一個(gè)分支。

顯然核心就在于 POP_JUMP_IF_FALSE 指令,我們看一下它的邏輯。

POP_JUMP_IF_FALSE

COMPARE_OP 執(zhí)行完之后會(huì)將比較的結(jié)果壓入運(yùn)行時(shí)棧,而該指令則是將結(jié)果從棧頂彈出并判斷真假。如果為假,那么跳到下一個(gè)分支,否則執(zhí)行此分支的代碼。

TARGET(POP_JUMP_IF_FALSE) {
    PREDICTED(POP_JUMP_IF_FALSE);
    // 從棧頂彈出比較結(jié)果,當(dāng)然這里其實(shí)只是獲取
    // 如果再結(jié)合最下面的 STACK_SHRINK(1),那么等價(jià)于彈出
    PyObject *cond = stack_pointer[-1];
    #line 2157 "Python/bytecodes.c"
    // 如果 cond is False,那么 Py_IsFalse(cond) 就是真
    // 此時(shí)會(huì)通過(guò) JUMPBY 跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支,關(guān)于 JUMPBY 一會(huì)兒介紹
    if (Py_IsFalse(cond)) {
        JUMPBY(oparg);
    }
    // 但對(duì)于 if 語(yǔ)句來(lái)說(shuō),除了 False 之外,像 None、0、""、[] 等也表示假
    // 那么當(dāng) cond 也不是 True 的時(shí)候(說(shuō)明不是布爾值),要繼續(xù)判斷
    else if (!Py_IsTrue(cond)) {
        // Py_IsTrue(cond):等價(jià)于 Python 的 cond is True
        // PyObject_IsTrue(cond):等價(jià)于 Python 的 bool(cond) is True
        // 所以 if cond 和 if bool(cond) 是等價(jià)的
        int err = PyObject_IsTrue(cond);
        #line 3047 "Python/generated_cases.c.h"
        Py_DECREF(cond);
        #line 2163 "Python/bytecodes.c"
        // 如果 PyObject_IsTrue 返回 0,說(shuō)明 bool(cond) 不是 True
        // 即 cond 為假,意味著條件不成立,那么要跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支
        if (err == 0) {
            JUMPBY(oparg);
        }
        // 如果返回值小于 0,說(shuō)明出錯(cuò)了(基本不會(huì)發(fā)生)
        // 由于運(yùn)行時(shí)棧里面還有一個(gè)元素
        // 那么跳轉(zhuǎn)到幀評(píng)估函數(shù)中的 pop_1_error
        else {
            if (err < 0) goto pop_1_error;
        }
    }
    #line 3057 "Python/generated_cases.c.h"
    STACK_SHRINK(1);
    DISPATCH();
}

邏輯不難理解,但是里面出現(xiàn)了幾個(gè)判斷布爾值的函數(shù),我們補(bǔ)充一下。

// Objects/object.c

// 等價(jià)于 Python 的 x is y
int Py_Is(PyObject *x, PyObject *y)
{   
    return (x == y);
}

// 等價(jià)于 Python 的 x is None
int Py_IsNone(PyObject *x)
{
    return Py_Is(x, Py_None);
}

// 等價(jià)于 Python 的 x is True
int Py_IsTrue(PyObject *x)
{
    return Py_Is(x, Py_True);
}

// 等價(jià)于 Python 的 x is False
int Py_IsFalse(PyObject *x)
{
    return Py_Is(x, Py_False);
}

// 等價(jià)于 Python 的 bool(v) is True
int
PyObject_IsTrue(PyObject *v)
{
    Py_ssize_t res;
    // 如果 v 本身就是布爾值 True,返回 1
    if (v == Py_True)
        return 1;
    // 如果 v 本身就是布爾值 False,返回 0
    if (v == Py_False)
        return 0;
    // 如果 v 是 None,返回 0
    if (v == Py_None)
        return 0;
    // 如果 v 是數(shù)值型對(duì)象,并且實(shí)現(xiàn)了 nb_bool(對(duì)應(yīng) __bool__)
    // 那么調(diào)用,如果結(jié)果不為 0,返回 1,否則返回 0
    else if (Py_TYPE(v)->tp_as_number != NULL &&
             Py_TYPE(v)->tp_as_number->nb_bool != NULL)
        res = (*Py_TYPE(v)->tp_as_number->nb_bool)(v);
    // 如果 v 是映射型對(duì)象,并且實(shí)現(xiàn)了 mp_length(對(duì)應(yīng) __len__)
    // 那么調(diào)用,返回對(duì)象的長(zhǎng)度
    else if (Py_TYPE(v)->tp_as_mapping != NULL &&
             Py_TYPE(v)->tp_as_mapping->mp_length != NULL)
        res = (*Py_TYPE(v)->tp_as_mapping->mp_length)(v);
    // 如果 v 是序列型對(duì)象,并且實(shí)現(xiàn)了 sq_length(對(duì)應(yīng) __len__)
    // 那么調(diào)用,返回對(duì)象的長(zhǎng)度
    else if (Py_TYPE(v)->tp_as_sequence != NULL &&
             Py_TYPE(v)->tp_as_sequence->sq_length != NULL)
        res = (*Py_TYPE(v)->tp_as_sequence->sq_length)(v);
    // 如果以上條件都不滿(mǎn)足,直接返回 1,比如自定義類(lèi)的實(shí)例對(duì)象(默認(rèn)為真)
    else
        return 1;
    // 如果 res > 0 返回 1,否則返回 0
    return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}

// 等價(jià)于 Python 的 not v
int
PyObject_Not(PyObject *v)
{
    int res;
    res = PyObject_IsTrue(v);
    if (res < 0)
        return res;
    // 如果 v 是真,res == 1,那么 res == 0 結(jié)果是 0
    // 如果 v 是假,res == 0,那么 res == 0 結(jié)果是 1
    // 相當(dāng)于取反
    return res == 0;
}

// Objects/boolobject.c
static PyObject *
bool_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    // <class 'bool'> 是一個(gè) Python 類(lèi),這里的 bool_new 便是它的構(gòu)造函數(shù)
    PyObject *x = Py_False;
    long ok;
    // 不接收關(guān)鍵字參數(shù)
    if (!_PyArg_NoKeywords("bool", kwds))
        return NULL;
    // 只接收 0 ~ 1 個(gè)參數(shù),如果不傳,那么默認(rèn)返回 False
    if (!PyArg_UnpackTuple(args, "bool", 0, 1, &x))
        return NULL;
    // 調(diào)用 PyObject_IsTrue,所以我們說(shuō) if v 和 if bool(v) 是等價(jià)的
    // 因?yàn)楫?dāng) v 不是布爾值時(shí),if v 對(duì)應(yīng)的指令內(nèi)部會(huì)調(diào)用 PyObject_IsTrue
    // 而 bool(v) 也會(huì)調(diào)用 PyObject_IsTrue,所以?xún)烧呤堑葍r(jià)的
    ok = PyObject_IsTrue(x);
    if (ok < 0)
        return NULL;
    // 調(diào)用 PyBool_FromLong 創(chuàng)建布爾值,ok 為 1 返回 True,為 0 返回 False
    return PyBool_FromLong(ok);
}

PyObject *PyBool_FromLong(long ok)
{
    return ok ? Py_True : Py_False;
}

相信你現(xiàn)在明白了為什么 if 后面不跟布爾值也是可以的,因?yàn)橛幸粋€(gè) C 函數(shù) PyObject_IsTrue,可以判斷任意對(duì)象的真假。如果 if 后面跟著的不是布爾值,那么會(huì)自動(dòng)調(diào)用該函數(shù)。另外由于 bool(v) 也會(huì)調(diào)用該函數(shù),所以 if v 和 if bool(v) 是等價(jià)的。

注:沒(méi)有 PyObject_IsFalse。

說(shuō)完了 POP_JUMP_IF_FALSE 指令,再補(bǔ)充一個(gè)和它相似的指令叫 POP_JUMP_IF_TRUE,它表示當(dāng)比較結(jié)果為真時(shí),跳到下一個(gè)分支,否則執(zhí)行當(dāng)前分支的代碼??赡苡腥擞X(jué)得,這不對(duì)吧,比較結(jié)果為真,難道不應(yīng)該執(zhí)行當(dāng)前分支的邏輯嗎?所以 POP_JUMP_IF_TRUE 指令似乎本身就是矛盾的。

仔細(xì)想想你應(yīng)該能夠猜到原因,答案就是使用了 not。

import dis

code_string = """
if 2 > 1:
    print("古明地覺(jué)")
"""
# 只打印部分字節(jié)碼
dis.dis(compile(code_string, "<file>", "exec"))
"""
     2 LOAD_CONST               0 (2)
     4 LOAD_CONST               1 (1)
     6 COMPARE_OP              68 (>)
    10 POP_JUMP_IF_FALSE        9 (to 30)
"""             

code_string = """
if not 2 > 1:
    print("古明地覺(jué)")
"""
dis.dis(compile(code_string, "<file>", "exec"))
"""
     2 LOAD_CONST               0 (2)
     4 LOAD_CONST               1 (1)
     6 COMPARE_OP              68 (>)
    10 POP_JUMP_IF_TRUE         9 (to 30)
"""

正常情況下如果比較結(jié)果為 False,則跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支,所以 POP_JUMP_IF_FALSE 指令是合理的。至于 POP_JUMP_IF_TRUE 指令從邏輯上似乎就不該存在,因?yàn)樗?if 語(yǔ)句本身是相矛盾的。

但現(xiàn)在我們明白了,該指令其實(shí)是為 not 關(guān)鍵字準(zhǔn)備的。如果比較結(jié)果為真,那么 not 取反就是假,于是跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支,所以整個(gè)邏輯依舊是正確的。

當(dāng)然這里只有一個(gè) not,即使有很多個(gè) not 也是可以的,盡管這沒(méi)太大意義。

import dis

# 這里有 4 個(gè) not,因?yàn)槭桥紨?shù)個(gè),兩兩相互抵消
# 所以結(jié)果等價(jià)于 if 2 > 1
code_string = """
if not not not not 2 > 1:
    print("古明地覺(jué)")
"""
# 只打印部分字節(jié)碼
dis.dis(compile(code_string, "<file>", "exec"))
"""
     2 LOAD_CONST               0 (2)
     4 LOAD_CONST               1 (1)
     6 COMPARE_OP              68 (>)
    10 POP_JUMP_IF_FALSE        9 (to 30)
"""             

# 這里有 5 個(gè) not,因?yàn)槭瞧鏀?shù)個(gè),兩兩相互抵消之后還剩下一個(gè)
# 所以結(jié)果等價(jià)于 if not 2 > 1
code_string = """
if not not not not not 2 > 1:
    print("古明地覺(jué)")
"""
dis.dis(compile(code_string, "<file>", "exec"))
"""
     2 LOAD_CONST               0 (2)
     4 LOAD_CONST               1 (1)
     6 COMPARE_OP              68 (>)
    10 POP_JUMP_IF_TRUE         9 (to 30)
"""

然后再看一下 POP_JUMP_IF_TRUE 指令的內(nèi)部邏輯,顯然它和 POP_JUMP_IF_FALSE 是類(lèi)似的。

TARGET(POP_JUMP_IF_TRUE) {
    // 獲取棧頂元素
    PyObject *cond = stack_pointer[-1];
    #line 2173 "Python/bytecodes.c"
    // 如果 cond is True,跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支
    if (Py_IsTrue(cond)) {
        JUMPBY(oparg);
    }
    // 如果 cond 不是 True,那么看 bool(cond) 是否為 True
    else if (!Py_IsFalse(cond)) {
        int err = PyObject_IsTrue(cond);
    #line 3070 "Python/generated_cases.c.h"
        Py_DECREF(cond);
    #line 2179 "Python/bytecodes.c"
        // err > 0,說(shuō)明結(jié)果為真,跳轉(zhuǎn)到 if 語(yǔ)句的下一個(gè)分支
        if (err > 0) {
            JUMPBY(oparg);
        }
        // 否則說(shuō)明比較出錯(cuò)了(基本不會(huì)發(fā)生)
        else {
            if (err < 0) goto pop_1_error;
        }
    }
    #line 3080 "Python/generated_cases.c.h"
    STACK_SHRINK(1);
    DISPATCH();
}

以上就是 POP_JUMP_IF_FALSE 和 POP_JUMP_IF_TRUE 的內(nèi)部邏輯,可以說(shuō)非常簡(jiǎn)單。

JUMPBY

指令跳轉(zhuǎn)是由 JUMPBY 實(shí)現(xiàn)的,它內(nèi)部的邏輯長(zhǎng)啥樣呢?

// Python/ceval_macros.h
#define JUMPBY(x)       (next_instr += (x))

字節(jié)碼指令的遍歷是通過(guò) next_instr 實(shí)現(xiàn)的,如果將指令執(zhí)行的方向代表前進(jìn)的方向,顯然它表示從當(dāng)前指令所在的位置向前跳轉(zhuǎn) x 個(gè)指令。

圖片圖片

POP_JUMP_IF_FALSE 指令的偏移量為 14,向前跳轉(zhuǎn) 9 個(gè)指令,一個(gè)指令的大小是 2 字節(jié),所以結(jié)果是 14 + 18 = 32。咦,不是應(yīng)該跳轉(zhuǎn)到偏移量為 34 的指令嗎,為啥結(jié)果是 32 呢?

很簡(jiǎn)單,TARGET 是一個(gè)宏,它在展開(kāi)之后,還會(huì)對(duì) next_instr 做一次自增操作。

圖片圖片

除了 JUMPBY 之外,還有一個(gè) JUMPTO,它表示從字節(jié)碼的起始位置向前跳轉(zhuǎn) x 個(gè)指令。

// Python/ceval_macros.h

// 從字節(jié)碼的起始位置向前跳轉(zhuǎn) x 個(gè)指令
#define JUMPTO(x)       (next_instr = _PyCode_CODE(frame->f_code) + (x))
// 從 next_instr 處(指向當(dāng)前待執(zhí)行的指令)向前跳轉(zhuǎn) x 個(gè)指令
#define JUMPBY(x)       (next_instr += (x))

所以 JUMPTO 表示絕對(duì)跳轉(zhuǎn),JUMPBY 表示相對(duì)跳轉(zhuǎn)。不難發(fā)現(xiàn),JUMPTO 既可以向前跳轉(zhuǎn)(偏移量增大),也可以向后跳轉(zhuǎn)(偏移量減小);而 JUMPBY 只能向前跳轉(zhuǎn)。

假設(shè)參數(shù)為 n,當(dāng)前指令的偏移量為 m。對(duì)于 JUMPTO 而言,跳轉(zhuǎn)之后的偏移量始終為 2n,如果 m < 2n 就是向前跳轉(zhuǎn),m > 2n 就是向后跳轉(zhuǎn)。但對(duì)于 JUMP_BY 而言,由于它是從當(dāng)前待執(zhí)行的指令開(kāi)始跳轉(zhuǎn)的,所以只能向前跳轉(zhuǎn)(偏移量增大)。

指令預(yù)測(cè)

CPython 3.12 里面引入了計(jì)算跳轉(zhuǎn),可以避免不必要的匹配。因?yàn)檎麄€(gè)指令集合是已知的,這就說(shuō)明某條指令在執(zhí)行時(shí),便可知道它的下一條指令是什么。

所以當(dāng)前指令處理完后,可以直接跳轉(zhuǎn)到下一條指令對(duì)應(yīng)的處理邏輯中,這就是計(jì)算跳轉(zhuǎn)。但如果不使用計(jì)算跳轉(zhuǎn),那么每次讀取到指令后,都要進(jìn)入 switch,順序匹配數(shù)百個(gè) case 分支,找到匹配成功的那一個(gè)。

因此使用計(jì)算跳轉(zhuǎn)可以避免不必要的匹配,既然提前知道下一條指令是啥了,那么直接精確跳轉(zhuǎn)就行,無(wú)需多走一遍 switch。不過(guò)要想實(shí)現(xiàn)計(jì)算跳轉(zhuǎn),需要 GCC 支持標(biāo)簽作為值,即 goto *label_addr 用法,由于 label_addr 是一個(gè)標(biāo)簽地址,那么解引用之后就是標(biāo)簽了。至于具體會(huì)跳轉(zhuǎn)到哪一個(gè)標(biāo)簽,取決于 label_addr 保存了哪一個(gè)標(biāo)簽的地址,因此這種跳轉(zhuǎn)是動(dòng)態(tài)的,在運(yùn)行時(shí)決定跳轉(zhuǎn)目標(biāo)。

goto 標(biāo)簽:靜態(tài)跳轉(zhuǎn),標(biāo)簽需要顯式地定義好,跳轉(zhuǎn)位置在編譯期間便已經(jīng)固定。

goto *標(biāo)簽地址:動(dòng)態(tài)跳轉(zhuǎn)(計(jì)算跳轉(zhuǎn)),跳轉(zhuǎn)位置不固定,可以是已有標(biāo)簽中的任意一個(gè)。至于具體是哪一個(gè),需要在運(yùn)行時(shí)經(jīng)過(guò)計(jì)算才能確定。

虛擬機(jī)為每個(gè)指令的處理邏輯都定義了一個(gè)標(biāo)簽,對(duì)于計(jì)算跳轉(zhuǎn)來(lái)說(shuō),goto 的結(jié)果是 *標(biāo)簽地址,這個(gè)地址是運(yùn)行時(shí)計(jì)算得出的。我們舉個(gè)例子,隨便看一段字節(jié)碼指令集。

比如當(dāng)前正在執(zhí)行 LOAD_NAME 指令,那么下一條指令可以是 STORE_NAME、LOAD_NAME 以及 BUILD_LIST 等。當(dāng)開(kāi)啟計(jì)算跳轉(zhuǎn)時(shí):

  • 如果下一條指令是 STORE_NAME,那么之后就會(huì)跳轉(zhuǎn) STORE_NAME 對(duì)應(yīng)的標(biāo)簽;
  • 如果下一條指令是 LOAD_NAME,那么之后就會(huì)跳轉(zhuǎn)到 LOAD_NAME 對(duì)應(yīng)的標(biāo)簽;
  • 如果下一條指令是 BUILD_LIST,那么之后就會(huì)跳轉(zhuǎn)到 BUILD_LIST 對(duì)應(yīng)的標(biāo)簽;

所以在運(yùn)行時(shí)判斷指令的值,獲取對(duì)應(yīng)的標(biāo)簽,從而實(shí)現(xiàn)精確跳轉(zhuǎn),這就是計(jì)算跳轉(zhuǎn)。當(dāng)然這些內(nèi)容在剖析虛擬機(jī)執(zhí)行字節(jié)碼時(shí)已經(jīng)說(shuō)過(guò)了,這里再回顧一下。

接下來(lái)說(shuō)一說(shuō)指令預(yù)測(cè),不難發(fā)現(xiàn),如果是計(jì)算跳轉(zhuǎn),那么指令預(yù)測(cè)功能貌似沒(méi)啥用,因?yàn)榭偸悄芫_跳轉(zhuǎn)到下一個(gè)指令對(duì)應(yīng)的標(biāo)簽中。沒(méi)錯(cuò),指令預(yù)測(cè)只有在不使用計(jì)算跳轉(zhuǎn)的情況下有用,那什么是指令預(yù)測(cè)呢?

在不使用計(jì)算跳轉(zhuǎn)時(shí),goto 后面必須是一個(gè)靜態(tài)的標(biāo)簽,跳轉(zhuǎn)位置在編譯階段便已經(jīng)固定,換句話(huà)說(shuō)一個(gè)指令執(zhí)行完畢后要跳轉(zhuǎn)到哪一個(gè)標(biāo)簽是寫(xiě)死的,不能保證跳轉(zhuǎn)后的標(biāo)簽正好對(duì)應(yīng)下一條指令的處理邏輯。比如 LOAD_NAME 的下一條指令可以是 STORE_NAME 和 BUILD_LIST,那么應(yīng)該跳轉(zhuǎn)到哪一個(gè)指令對(duì)應(yīng)的標(biāo)簽中呢?

正因?yàn)檫@種不確定性,絕大部分指令在執(zhí)行完畢后都會(huì)直接跳轉(zhuǎn)到 switch 語(yǔ)句所在位置,然后順序匹配 case 分支。

但也有那么幾個(gè)指令,由于彼此的關(guān)聯(lián)性很強(qiáng),很多時(shí)候都是成對(duì)出現(xiàn)的,面對(duì)這樣的指令,虛擬機(jī)會(huì)進(jìn)行預(yù)測(cè)。比如 A 和 B 兩個(gè)指令的關(guān)聯(lián)性很強(qiáng),盡管 A 的下一條指令除了是 B 之外,也有可能是其它指令,但 B 出現(xiàn)的概率是最大的,因此虛擬機(jī)會(huì)預(yù)測(cè)下一條指令是 B 指令。于是在執(zhí)行完 A 指令之后,會(huì)驗(yàn)證自己的預(yù)測(cè)是否正確,即檢測(cè)下一條指令是否是 B 指令。如果預(yù)測(cè)對(duì)了,可以實(shí)現(xiàn)精確跳轉(zhuǎn),如果預(yù)測(cè)錯(cuò)了,就只能回到 switch 語(yǔ)句逐一匹配 case 分支了。

總結(jié)一下:指令在執(zhí)行時(shí),它的下一條指令是已知的,但是不固定,有多種可能。如果不使用計(jì)算跳轉(zhuǎn),由于 goto 后面必須是一個(gè)寫(xiě)死的標(biāo)簽,而下一條指令卻不固定,那么只能跳轉(zhuǎn)到 switch 語(yǔ)句所在的位置,順序匹配 case 分支。但也有那么幾對(duì)指令,關(guān)聯(lián)性很強(qiáng),雖然不能保證百分百,但值得做一次嘗試,這便是指令預(yù)測(cè)。

當(dāng)然啦,如果使用計(jì)算跳轉(zhuǎn),情況則不一樣了,此時(shí)壓根用不到指令預(yù)測(cè)。因?yàn)?goto 后面是 *標(biāo)簽地址,而地址是可以動(dòng)態(tài)獲取的。由于所有標(biāo)簽的地址都保存在了一個(gè)數(shù)組中,不管接下來(lái)要處理哪一條指令,都可以獲取到對(duì)應(yīng)的標(biāo)簽地址,實(shí)現(xiàn)精確跳轉(zhuǎn)。

以上有很多都是之前說(shuō)過(guò)的內(nèi)容,再重復(fù)一遍加深印象。好,關(guān)于指令預(yù)測(cè)我們已經(jīng)知道是啥了,那么在源碼層面又是如何體現(xiàn)的呢?

在 POP_JUMP_IF_FALSE 指令中,我們看到有這么一行邏輯。

圖片圖片

里面有一個(gè)宏 PREDICTED。

// Python/ceval_macros.h

#define PREDICTED(op)           PREDICT_ID(op):
#define PREDICT_ID(op)          PRED_##op

這個(gè)宏展開(kāi)之后又是一個(gè)標(biāo)簽,由于調(diào)用時(shí)結(jié)尾加了分號(hào),所以這還是一個(gè)空標(biāo)簽。整體效果如下:

圖片圖片

那么它有什么用呢?我們?cè)倏匆粋€(gè)指令就明白了。

圖片圖片

MATCH_SEQUENCE 指令是做什么的,我們以后再說(shuō),總之虛擬機(jī)認(rèn)為該指令執(zhí)行完之后,大概率會(huì)執(zhí)行 POP_JUMP_IF_FALSE 指令,所以做了一個(gè)預(yù)測(cè)。而相關(guān)邏輯位于 PREDICT 中,看一下它長(zhǎng)什么樣子。

// Python/ceval_macros.h

#define PREDICT_ID(op)          PRED_##op

// 如果開(kāi)啟計(jì)算跳轉(zhuǎn),那么指令預(yù)測(cè)不生效
// 因?yàn)楸旧砭椭涝撎D(zhuǎn)到哪個(gè)指令對(duì)應(yīng)的標(biāo)簽
#if USE_COMPUTED_GOTOS
#define PREDICT(op)             if (0) goto PREDICT_ID(op)
#else
// 如果不開(kāi)啟計(jì)算跳轉(zhuǎn),那么會(huì)比較預(yù)測(cè)的指令和實(shí)際的指令是否相等
// 所以 MATCH_SEQUENCE 指令處理邏輯里面的 PREDICT(POP_JUMP_IF_FALSE)
// 就是在判斷下一條指令是不是自己預(yù)測(cè)的 POP_JUMP_IF_FALSE
// 如果是,說(shuō)明預(yù)測(cè)成功,那么 goto PRED_POP_JUMP_IF_FALSE
// 否則說(shuō)明預(yù)測(cè)失敗,那么會(huì)執(zhí)行 DISPATCH(),然后 goto 到 switch 語(yǔ)句所在位置
#define PREDICT(next_op) \
    do { \
        _Py_CODEUNIT word = *next_instr; \
        opcode = word.op.code; \
        if (opcode == next_op) { \
            oparg = word.op.arg; \
            INSTRUCTION_START(next_op); \
            goto PREDICT_ID(next_op); \
        } \
    } while(0)
#endif

以上便是指令預(yù)測(cè),說(shuō)白了就是如果指令 A 和指令 B 具有極高的關(guān)聯(lián)性(甚至百分百),那么執(zhí)行完 A 指令后會(huì)判斷下一條指令是不是 B。如果是,那么直接跳轉(zhuǎn)即可,就省去了匹配 case 分支的時(shí)間,如果不是,那就只能挨個(gè)匹配了。

因?yàn)槭庆o態(tài)跳轉(zhuǎn),goto 后面的標(biāo)簽是寫(xiě)死的,編譯階段就確定了,所以只有那種關(guān)聯(lián)度極高的指令才會(huì)開(kāi)啟預(yù)測(cè)功能,因?yàn)轭A(yù)測(cè)成功的概率比較高。但如果指令 A 的下一條指令有多種可能(假設(shè)有 6 種),并且每種指令出現(xiàn)的概率還差不多,那么這時(shí)不管預(yù)測(cè)哪一個(gè),成功的概率都只有 1/6。顯然這就不叫預(yù)測(cè)了,這是在擲骰子,因此對(duì)于這樣的指令,虛擬機(jī)不會(huì)為它開(kāi)啟預(yù)測(cè)功能。

圖片圖片

比如 LOAD_NAME 的下一個(gè)指令可以是 STORE_NAME、LOAD_NAME、BUILD_LIST 等等,不管預(yù)測(cè)哪一種,成功的概率都不是特別高,因此它沒(méi)有進(jìn)行指令預(yù)測(cè)。

所以就一句話(huà):只有 A 和 B 兩個(gè)指令的關(guān)聯(lián)度極高的時(shí)候,執(zhí)行 A 之后才會(huì)預(yù)測(cè)下一條指令是否是 B。預(yù)測(cè)成功直接跳轉(zhuǎn),預(yù)測(cè)失敗執(zhí)行 DISPATCH(),跳轉(zhuǎn)到 switch 語(yǔ)句所在位置,即 dispatch_opcode 標(biāo)簽。

但如果使用了計(jì)算跳轉(zhuǎn),情況就不一樣了,此時(shí)不會(huì)開(kāi)啟指令預(yù)測(cè),或者說(shuō)指令預(yù)測(cè)里的邏輯會(huì)變得無(wú)效。

圖片圖片

很明顯,使用計(jì)算跳轉(zhuǎn)后,PREDICT(op) 不會(huì)產(chǎn)生任何效果,因此也可以理解為沒(méi)有開(kāi)啟指令預(yù)測(cè)。而之所以不用預(yù)測(cè),是因?yàn)閳?zhí)行 DISPATCH() 的時(shí)候,本身就可以精確跳轉(zhuǎn)到指定位置。

小結(jié)

本篇文章我們就分析了 if 語(yǔ)句的實(shí)現(xiàn)原理,總的來(lái)說(shuō)不難理解。依舊是在棧楨中執(zhí)行字節(jié)碼,只是多了一個(gè)指令跳轉(zhuǎn)罷了,至于怎么跳轉(zhuǎn)、跳轉(zhuǎn)到什么地方,全部都體現(xiàn)在字節(jié)碼中。

責(zé)任編輯:武曉燕 來(lái)源: 古明地覺(jué)的編程教室
相關(guān)推薦

2024-11-05 12:59:42

while 循環(huán)迭代字節(jié)碼

2010-05-11 12:53:58

Unix awk

2010-07-19 10:11:58

Perl流程控制語(yǔ)句

2009-09-04 10:42:56

C#流程控制語(yǔ)句

2011-08-24 16:36:00

T-SQL

2015-07-23 15:17:37

JavaScript循環(huán)語(yǔ)句

2017-05-31 17:09:52

LinuxShell命令

2024-06-06 09:09:41

SQL循環(huán)控制命令

2016-08-29 20:51:16

awkLinux開(kāi)源

2011-08-23 13:36:11

T-SQL查詢(xún)流程控制語(yǔ)句

2009-12-15 09:56:51

Ruby流程控制

2011-09-08 13:53:31

Node.js

2010-03-18 16:37:13

Python 程序流程

2020-11-13 10:29:37

流程控制語(yǔ)句

2021-05-27 09:30:51

Java流程控制

2021-05-27 05:27:22

流程控制Rust

2022-07-27 08:31:28

SQL開(kāi)發(fā)控制

2010-05-11 12:17:51

Unix awk

2013-12-13 15:48:52

Lua腳本語(yǔ)言

2021-08-05 06:54:05

流程控制default
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)