成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

流程控制語句 if 是怎么實現的?

開發 前端
本篇文章我們就分析了 if 語句的實現原理,總的來說不難理解。依舊是在棧楨中執行字節碼,只是多了一個指令跳轉罷了,至于怎么跳轉、跳轉到什么地方,全部都體現在字節碼中。?

楔子

前面我們分析了虛擬機執行字節碼的原理,并且也介紹了不少指令,但這些指令都是從上往下順序執行的,不涉及任何的跳轉。而像流程控制語句,比如 if、for、while、try 等等,它們在執行時會發生跳轉,因此 Python 底層一定還存在相應的跳轉指令。

那么從現在開始,就來分析一下這些流程控制語句的實現原理,本文先來介紹 if 語句。

if 字節碼

if 語句應該是最簡單也是最常用的流程控制語句,那么它的字節碼是怎么樣的呢?當然這里的 if 語句指的是 if elif else 整體,里面的某個條件叫做該 if 語句的分支。

我們看一下 if 語句的字節碼長什么樣子。

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"))

反編譯得到的字節碼指令比較多,我們來慢慢分析。另外為了閱讀方便,源代碼行號就不顯示了。

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

我們看到字節碼偏移量之前有幾個 >> 這樣的符號,顯然這是 if 語句中的每一個分支開始的地方。

經過分析,整個 if 語句的字節碼指令還是很簡單的。就是從上到下依次判斷每一個分支,如果某個分支條件成立,就執行該分支的代碼,執行完畢后結束整個 if 語句;否則跳轉到下一個分支。

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

POP_JUMP_IF_FALSE

COMPARE_OP 執行完之后會將比較的結果壓入運行時棧,而該指令則是將結果從棧頂彈出并判斷真假。如果為假,那么跳到下一個分支,否則執行此分支的代碼。

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

邏輯不難理解,但是里面出現了幾個判斷布爾值的函數,我們補充一下。

// Objects/object.c

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

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

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

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

// 等價于 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 是數值型對象,并且實現了 nb_bool(對應 __bool__)
    // 那么調用,如果結果不為 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 是映射型對象,并且實現了 mp_length(對應 __len__)
    // 那么調用,返回對象的長度
    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 是序列型對象,并且實現了 sq_length(對應 __len__)
    // 那么調用,返回對象的長度
    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);
    // 如果以上條件都不滿足,直接返回 1,比如自定義類的實例對象(默認為真)
    else
        return 1;
    // 如果 res > 0 返回 1,否則返回 0
    return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}

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

// Objects/boolobject.c
static PyObject *
bool_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    // <class 'bool'> 是一個 Python 類,這里的 bool_new 便是它的構造函數
    PyObject *x = Py_False;
    long ok;
    // 不接收關鍵字參數
    if (!_PyArg_NoKeywords("bool", kwds))
        return NULL;
    // 只接收 0 ~ 1 個參數,如果不傳,那么默認返回 False
    if (!PyArg_UnpackTuple(args, "bool", 0, 1, &x))
        return NULL;
    // 調用 PyObject_IsTrue,所以我們說 if v 和 if bool(v) 是等價的
    // 因為當 v 不是布爾值時,if v 對應的指令內部會調用 PyObject_IsTrue
    // 而 bool(v) 也會調用 PyObject_IsTrue,所以兩者是等價的
    ok = PyObject_IsTrue(x);
    if (ok < 0)
        return NULL;
    // 調用 PyBool_FromLong 創建布爾值,ok 為 1 返回 True,為 0 返回 False
    return PyBool_FromLong(ok);
}

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

相信你現在明白了為什么 if 后面不跟布爾值也是可以的,因為有一個 C 函數 PyObject_IsTrue,可以判斷任意對象的真假。如果 if 后面跟著的不是布爾值,那么會自動調用該函數。另外由于 bool(v) 也會調用該函數,所以 if v 和 if bool(v) 是等價的。

注:沒有 PyObject_IsFalse。

說完了 POP_JUMP_IF_FALSE 指令,再補充一個和它相似的指令叫 POP_JUMP_IF_TRUE,它表示當比較結果為真時,跳到下一個分支,否則執行當前分支的代碼。可能有人覺得,這不對吧,比較結果為真,難道不應該執行當前分支的邏輯嗎?所以 POP_JUMP_IF_TRUE 指令似乎本身就是矛盾的。

仔細想想你應該能夠猜到原因,答案就是使用了 not。

import dis

code_string = """
if 2 > 1:
    print("古明地覺")
"""
# 只打印部分字節碼
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("古明地覺")
"""
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)
"""

正常情況下如果比較結果為 False,則跳轉到 if 語句的下一個分支,所以 POP_JUMP_IF_FALSE 指令是合理的。至于 POP_JUMP_IF_TRUE 指令從邏輯上似乎就不該存在,因為它和 if 語句本身是相矛盾的。

但現在我們明白了,該指令其實是為 not 關鍵字準備的。如果比較結果為真,那么 not 取反就是假,于是跳轉到 if 語句的下一個分支,所以整個邏輯依舊是正確的。

當然這里只有一個 not,即使有很多個 not 也是可以的,盡管這沒太大意義。

import dis

# 這里有 4 個 not,因為是偶數個,兩兩相互抵消
# 所以結果等價于 if 2 > 1
code_string = """
if not not not not 2 > 1:
    print("古明地覺")
"""
# 只打印部分字節碼
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 個 not,因為是奇數個,兩兩相互抵消之后還剩下一個
# 所以結果等價于 if not 2 > 1
code_string = """
if not not not not not 2 > 1:
    print("古明地覺")
"""
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 指令的內部邏輯,顯然它和 POP_JUMP_IF_FALSE 是類似的。

TARGET(POP_JUMP_IF_TRUE) {
    // 獲取棧頂元素
    PyObject *cond = stack_pointer[-1];
    #line 2173 "Python/bytecodes.c"
    // 如果 cond is True,跳轉到 if 語句的下一個分支
    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,說明結果為真,跳轉到 if 語句的下一個分支
        if (err > 0) {
            JUMPBY(oparg);
        }
        // 否則說明比較出錯了(基本不會發生)
        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 的內部邏輯,可以說非常簡單。

JUMPBY

指令跳轉是由 JUMPBY 實現的,它內部的邏輯長啥樣呢?

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

字節碼指令的遍歷是通過 next_instr 實現的,如果將指令執行的方向代表前進的方向,顯然它表示從當前指令所在的位置向前跳轉 x 個指令。

圖片圖片

POP_JUMP_IF_FALSE 指令的偏移量為 14,向前跳轉 9 個指令,一個指令的大小是 2 字節,所以結果是 14 + 18 = 32。咦,不是應該跳轉到偏移量為 34 的指令嗎,為啥結果是 32 呢?

很簡單,TARGET 是一個宏,它在展開之后,還會對 next_instr 做一次自增操作。

圖片圖片

除了 JUMPBY 之外,還有一個 JUMPTO,它表示從字節碼的起始位置向前跳轉 x 個指令。

// Python/ceval_macros.h

// 從字節碼的起始位置向前跳轉 x 個指令
#define JUMPTO(x)       (next_instr = _PyCode_CODE(frame->f_code) + (x))
// 從 next_instr 處(指向當前待執行的指令)向前跳轉 x 個指令
#define JUMPBY(x)       (next_instr += (x))

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

假設參數為 n,當前指令的偏移量為 m。對于 JUMPTO 而言,跳轉之后的偏移量始終為 2n,如果 m < 2n 就是向前跳轉,m > 2n 就是向后跳轉。但對于 JUMP_BY 而言,由于它是從當前待執行的指令開始跳轉的,所以只能向前跳轉(偏移量增大)。

指令預測

CPython 3.12 里面引入了計算跳轉,可以避免不必要的匹配。因為整個指令集合是已知的,這就說明某條指令在執行時,便可知道它的下一條指令是什么。

所以當前指令處理完后,可以直接跳轉到下一條指令對應的處理邏輯中,這就是計算跳轉。但如果不使用計算跳轉,那么每次讀取到指令后,都要進入 switch,順序匹配數百個 case 分支,找到匹配成功的那一個。

因此使用計算跳轉可以避免不必要的匹配,既然提前知道下一條指令是啥了,那么直接精確跳轉就行,無需多走一遍 switch。不過要想實現計算跳轉,需要 GCC 支持標簽作為值,即 goto *label_addr 用法,由于 label_addr 是一個標簽地址,那么解引用之后就是標簽了。至于具體會跳轉到哪一個標簽,取決于 label_addr 保存了哪一個標簽的地址,因此這種跳轉是動態的,在運行時決定跳轉目標。

goto 標簽:靜態跳轉,標簽需要顯式地定義好,跳轉位置在編譯期間便已經固定。

goto *標簽地址:動態跳轉(計算跳轉),跳轉位置不固定,可以是已有標簽中的任意一個。至于具體是哪一個,需要在運行時經過計算才能確定。

虛擬機為每個指令的處理邏輯都定義了一個標簽,對于計算跳轉來說,goto 的結果是 *標簽地址,這個地址是運行時計算得出的。我們舉個例子,隨便看一段字節碼指令集。

比如當前正在執行 LOAD_NAME 指令,那么下一條指令可以是 STORE_NAME、LOAD_NAME 以及 BUILD_LIST 等。當開啟計算跳轉時:

  • 如果下一條指令是 STORE_NAME,那么之后就會跳轉 STORE_NAME 對應的標簽;
  • 如果下一條指令是 LOAD_NAME,那么之后就會跳轉到 LOAD_NAME 對應的標簽;
  • 如果下一條指令是 BUILD_LIST,那么之后就會跳轉到 BUILD_LIST 對應的標簽;

所以在運行時判斷指令的值,獲取對應的標簽,從而實現精確跳轉,這就是計算跳轉。當然這些內容在剖析虛擬機執行字節碼時已經說過了,這里再回顧一下。

接下來說一說指令預測,不難發現,如果是計算跳轉,那么指令預測功能貌似沒啥用,因為總是能精確跳轉到下一個指令對應的標簽中。沒錯,指令預測只有在不使用計算跳轉的情況下有用,那什么是指令預測呢?

在不使用計算跳轉時,goto 后面必須是一個靜態的標簽,跳轉位置在編譯階段便已經固定,換句話說一個指令執行完畢后要跳轉到哪一個標簽是寫死的,不能保證跳轉后的標簽正好對應下一條指令的處理邏輯。比如 LOAD_NAME 的下一條指令可以是 STORE_NAME 和 BUILD_LIST,那么應該跳轉到哪一個指令對應的標簽中呢?

正因為這種不確定性,絕大部分指令在執行完畢后都會直接跳轉到 switch 語句所在位置,然后順序匹配 case 分支。

但也有那么幾個指令,由于彼此的關聯性很強,很多時候都是成對出現的,面對這樣的指令,虛擬機會進行預測。比如 A 和 B 兩個指令的關聯性很強,盡管 A 的下一條指令除了是 B 之外,也有可能是其它指令,但 B 出現的概率是最大的,因此虛擬機會預測下一條指令是 B 指令。于是在執行完 A 指令之后,會驗證自己的預測是否正確,即檢測下一條指令是否是 B 指令。如果預測對了,可以實現精確跳轉,如果預測錯了,就只能回到 switch 語句逐一匹配 case 分支了。

總結一下:指令在執行時,它的下一條指令是已知的,但是不固定,有多種可能。如果不使用計算跳轉,由于 goto 后面必須是一個寫死的標簽,而下一條指令卻不固定,那么只能跳轉到 switch 語句所在的位置,順序匹配 case 分支。但也有那么幾對指令,關聯性很強,雖然不能保證百分百,但值得做一次嘗試,這便是指令預測。

當然啦,如果使用計算跳轉,情況則不一樣了,此時壓根用不到指令預測。因為 goto 后面是 *標簽地址,而地址是可以動態獲取的。由于所有標簽的地址都保存在了一個數組中,不管接下來要處理哪一條指令,都可以獲取到對應的標簽地址,實現精確跳轉。

以上有很多都是之前說過的內容,再重復一遍加深印象。好,關于指令預測我們已經知道是啥了,那么在源碼層面又是如何體現的呢?

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

圖片圖片

里面有一個宏 PREDICTED。

// Python/ceval_macros.h

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

這個宏展開之后又是一個標簽,由于調用時結尾加了分號,所以這還是一個空標簽。整體效果如下:

圖片圖片

那么它有什么用呢?我們再看一個指令就明白了。

圖片圖片

MATCH_SEQUENCE 指令是做什么的,我們以后再說,總之虛擬機認為該指令執行完之后,大概率會執行 POP_JUMP_IF_FALSE 指令,所以做了一個預測。而相關邏輯位于 PREDICT 中,看一下它長什么樣子。

// Python/ceval_macros.h

#define PREDICT_ID(op)          PRED_##op

// 如果開啟計算跳轉,那么指令預測不生效
// 因為本身就知道該跳轉到哪個指令對應的標簽
#if USE_COMPUTED_GOTOS
#define PREDICT(op)             if (0) goto PREDICT_ID(op)
#else
// 如果不開啟計算跳轉,那么會比較預測的指令和實際的指令是否相等
// 所以 MATCH_SEQUENCE 指令處理邏輯里面的 PREDICT(POP_JUMP_IF_FALSE)
// 就是在判斷下一條指令是不是自己預測的 POP_JUMP_IF_FALSE
// 如果是,說明預測成功,那么 goto PRED_POP_JUMP_IF_FALSE
// 否則說明預測失敗,那么會執行 DISPATCH(),然后 goto 到 switch 語句所在位置
#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

以上便是指令預測,說白了就是如果指令 A 和指令 B 具有極高的關聯性(甚至百分百),那么執行完 A 指令后會判斷下一條指令是不是 B。如果是,那么直接跳轉即可,就省去了匹配 case 分支的時間,如果不是,那就只能挨個匹配了。

因為是靜態跳轉,goto 后面的標簽是寫死的,編譯階段就確定了,所以只有那種關聯度極高的指令才會開啟預測功能,因為預測成功的概率比較高。但如果指令 A 的下一條指令有多種可能(假設有 6 種),并且每種指令出現的概率還差不多,那么這時不管預測哪一個,成功的概率都只有 1/6。顯然這就不叫預測了,這是在擲骰子,因此對于這樣的指令,虛擬機不會為它開啟預測功能。

圖片圖片

比如 LOAD_NAME 的下一個指令可以是 STORE_NAME、LOAD_NAME、BUILD_LIST 等等,不管預測哪一種,成功的概率都不是特別高,因此它沒有進行指令預測。

所以就一句話:只有 A 和 B 兩個指令的關聯度極高的時候,執行 A 之后才會預測下一條指令是否是 B。預測成功直接跳轉,預測失敗執行 DISPATCH(),跳轉到 switch 語句所在位置,即 dispatch_opcode 標簽。

但如果使用了計算跳轉,情況就不一樣了,此時不會開啟指令預測,或者說指令預測里的邏輯會變得無效。

圖片圖片

很明顯,使用計算跳轉后,PREDICT(op) 不會產生任何效果,因此也可以理解為沒有開啟指令預測。而之所以不用預測,是因為執行 DISPATCH() 的時候,本身就可以精確跳轉到指定位置。

小結

本篇文章我們就分析了 if 語句的實現原理,總的來說不難理解。依舊是在棧楨中執行字節碼,只是多了一個指令跳轉罷了,至于怎么跳轉、跳轉到什么地方,全部都體現在字節碼中。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-11-05 12:59:42

while 循環迭代字節碼

2010-05-11 12:53:58

Unix awk

2009-09-04 10:42:56

C#流程控制語句

2010-07-19 10:11:58

Perl流程控制語句

2015-07-23 15:17:37

JavaScript循環語句

2011-08-24 16:36:00

T-SQL

2017-05-31 17:09:52

LinuxShell命令

2024-06-06 09:09:41

SQL循環控制命令

2016-08-29 20:51:16

awkLinux開源

2011-08-23 13:36:11

T-SQL查詢流程控制語句

2009-12-15 09:56:51

Ruby流程控制

2011-09-08 13:53:31

Node.js

2010-03-18 16:37:13

Python 程序流程

2021-05-27 09:30:51

Java流程控制

2020-11-13 10:29:37

流程控制語句

2021-05-27 05:27:22

流程控制Rust

2025-07-01 08:20:00

JUC包Java并發

2022-07-27 08:31:28

SQL開發控制

2010-05-11 12:17:51

Unix awk

2021-08-05 06:54:05

流程控制default
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: a级性视频 | 欧美激情精品久久久久 | 国产成人精品一区二区三区四区 | 91一区| 欧美视频一区二区三区 | 91久久久久久久 | 日韩欧美在线不卡 | 欧美高清视频 | 亚洲一区二区三区免费在线 | 成人国产精品免费观看视频 | 成人在线看片 | 91视频大全 | 成人影院在线 | 99久久精品免费看国产四区 | 日本三级全黄三级a | 久久综合久久久 | 国产精品久久视频 | 日本字幕在线观看 | 国产欧美在线 | 成人三区 | 午夜视频一区二区 | 久久久精彩视频 | 中文字幕二区 | 麻豆久久 | 国产香蕉视频 | 国产精品毛片久久久久久 | 国产精品国产三级国产aⅴ原创 | 久久精品国产亚洲夜色av网站 | www.久久 | 精品国产一区二区三区久久久四川 | 99精品国产在热久久 | 中文字幕亚洲一区二区三区 | 日韩精品在线观看免费 | 日本精品一区二区三区在线观看 | 免费激情 | 精品久久久久久亚洲综合网 | 97精品超碰一区二区三区 | 日本一二三区高清 | 操久久| 国产精品久久久久久久久久久久冷 | jlzzjlzz欧美大全 |