溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

PHP的虛擬機(jī)是什么

發(fā)布時(shí)間:2020-06-22 20:35:09 來源:億速云 閱讀:598 作者:Leah 欄目:編程語言

這篇文章將為大家詳細(xì)講解有關(guān)PHP的虛擬機(jī),小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

PHP被稱為腳本語言或解釋型語言。為何? PHP語言沒有被直接編譯為機(jī)器指令,而是編譯為一種中間代碼的形式,很顯然它無法直接在CPU上執(zhí)行。 所以PHP的執(zhí)行需要在進(jìn)程級(jí)虛擬機(jī)上(見Virtual machine中的Process virtual machines,下文簡(jiǎn)稱虛擬機(jī))。

PHP語言,包括其他的解釋型語言,其實(shí)是一個(gè)跨平臺(tái)的被設(shè)計(jì)用來執(zhí)行抽象指令的程序。PHP主要用于解決WEB開發(fā)相關(guān)的問題。

諸如Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript等編程語言所編寫的程序,都需要在虛擬機(jī)上執(zhí)行。虛擬機(jī)可以通過JIT編譯技術(shù)將一部分虛擬機(jī)指令編譯為機(jī)器指令以提高性能。鳥哥已經(jīng)在進(jìn)行PHP加入JIT支持的開發(fā)了。

使用解釋型語言的優(yōu)點(diǎn):

  • 代碼編寫簡(jiǎn)單,能夠快速開發(fā)
  • 自動(dòng)的內(nèi)存管理
  • 抽象的數(shù)據(jù)類型,程序可移植性高

缺點(diǎn):

  • 無法直接地進(jìn)行內(nèi)存管理和使用進(jìn)程資源
  • 比編譯為機(jī)器指令的語言速度慢:通常需要更多的CPU周期來完成相同的任務(wù)(JIT試圖縮小差距,但永遠(yuǎn)不能完全消除)
  • 抽象了太多東西,以至于當(dāng)程序出問題時(shí),許多程序員難以解釋其根本原因

最后一條缺點(diǎn)是作者之所以寫這篇文章的原因,作者覺得程序員應(yīng)該去了解一些底層的東西。

作者希望能夠通過這篇文章向讀者講明白PHP是如何運(yùn)行的。本文所提到的關(guān)于PHP虛擬機(jī)的知識(shí)同樣可以應(yīng)用于其他解釋型語言。通常,不同虛擬機(jī)實(shí)現(xiàn)上的最大不同點(diǎn)在于:是否使用JIT、并行的虛擬機(jī)指令(一般使用多線程實(shí)現(xiàn),PHP沒有使用這一技術(shù))、內(nèi)存管理/垃圾回收算法。

Zend虛擬機(jī)分為兩大部分:

  • 編譯:將PHP代碼轉(zhuǎn)換為虛擬機(jī)指令(OPCode)
  • 執(zhí)行:執(zhí)行生成的虛擬機(jī)指令

本文不會(huì)涉及到編譯部分,主要關(guān)注Zend虛擬機(jī)的執(zhí)行引擎。PHP7版本的執(zhí)行引擎做了一部分重構(gòu),使得PHP代碼的執(zhí)行堆棧更加簡(jiǎn)單清晰,性能也得到了一些提升。

本文以PHP 7.0.7為示例。

OPCode

維基百科對(duì)于OPCode的解釋:

Opcodes can also be found in so-called byte codes and other representations intended for a software interpreter rather than a hardware device. These software based instruction sets often employ slightly higher-level data types and operations than        most hardware counterparts, but are nevertheless constructed along similar lines.

OPCode與ByteCode在概念上是不同的。

我的個(gè)人理解:OPCode作為一條指令,表明要怎么做,而ByteCode由一序列的OPCode/數(shù)據(jù)組成,表明要做什么。以一個(gè)加法為例子,OPCode是告訴執(zhí)行引擎將參數(shù)1和參數(shù)2相加,而ByteCode則告訴執(zhí)行引擎將45和56相加。

參考:Difference between Opcode and Bytecode和Difference between: Opcode, byte code, mnemonics, machine code and assembly

在PHP中,Zend/zend_vm_opcodes.h源碼文件列出了所有支持的OPCode。通常,每個(gè)OPCode的名字都描述了其含義,比如:

  • ZEND_ADD:對(duì)兩個(gè)操作數(shù)執(zhí)行加法操作
  • ZEND_NEW:創(chuàng)建一個(gè)對(duì)象
  • ZEND_FETCH_DIM_R:讀取操作數(shù)中某個(gè)維度的值,比如執(zhí)行echo $foo[0]語句時(shí),需要獲取$foo數(shù)組索引為0的值

OPCode以zend_op結(jié)構(gòu)體表示:

struct _zend_op {
    const void *handler; /* 執(zhí)行該OPCode的C函數(shù) */
    znode_op op1; /* 操作數(shù)1 */
    znode_op op2; /* 操作數(shù)2 */
    znode_op result; /* 結(jié)果 */
    uint32_t extended_value; /* 額外的信息 */
    uint32_t lineno; /* 該OPCode對(duì)應(yīng)PHP源碼所在的行 */
    zend_uchar opcode; /* OPCode對(duì)應(yīng)的數(shù)值 */
    zend_uchar op1_type; /* 操作數(shù)1類型 */
    zend_uchar op2_type; /* 操作數(shù)2類型 */
    zend_uchar result_type; /* 結(jié)果類型 */
};

每一條OPcode都以相同的方式執(zhí)行:OPCode有其對(duì)應(yīng)的C函數(shù),執(zhí)行該C函數(shù)時(shí),可能會(huì)用到0、1或2個(gè)操作數(shù)(op1,op2),最后將結(jié)果存儲(chǔ)在result中,可能還會(huì)有一些額外的信息存儲(chǔ)在extended_value。

看下ZEND_ADD的OPCode長(zhǎng)什么樣子,在Zend/zend_vm_def.h源碼文件中:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)                                                                                      
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;
    zval *op1, *op2, *result;

    op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
    op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
    if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
        if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
            result = EX_VAR(opline->result.var);
            fast_long_add_function(result, op1, op2);
            ZEND_VM_NEXT_OPCODE();
        } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
            result = EX_VAR(opline->result.var);
            ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
            ZEND_VM_NEXT_OPCODE();
        }    
    } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
        if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
            result = EX_VAR(opline->result.var);
            ZVAL_DOUBLE(result, Z_DVAL_P(op1) + Z_DVAL_P(op2));
            ZEND_VM_NEXT_OPCODE();
        } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
            result = EX_VAR(opline->result.var);
            ZVAL_DOUBLE(result, Z_DVAL_P(op1) + ((double)Z_LVAL_P(op2)));
            ZEND_VM_NEXT_OPCODE();
        }    
    }

    SAVE_OPLINE();
    if (OP1_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) {
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (OP2_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) {
        op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
    }
    add_function(EX_VAR(opline->result.var), op1, op2);
    FREE_OP1();
    FREE_OP2();
    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

可以看出這其實(shí)不是一個(gè)合法的C代碼,可以把它看成代碼模板。稍微解讀下這個(gè)代碼模板:1 就是在Zend/zend_vm_opcodes.h中define定義的ZEND_ADD的值;ZEND_ADD接收兩個(gè)操作數(shù),如果兩個(gè)操作數(shù)都為IS_LONG類型,那么就調(diào)用fast_long_add_function(該函數(shù)內(nèi)部使用匯編實(shí)現(xiàn)加法操作);如果兩個(gè)操作數(shù),都為IS_DOUBLE類型或者1個(gè)是IS_DOUBLE類型,另1個(gè)是IS_LONG類型,那么就直接執(zhí)行double的加法操作;如果存在1個(gè)操作數(shù)不是IS_LONG或IS_DOUBLE類型,那么就調(diào)用add_function(比如兩個(gè)數(shù)組做加法操作);最后檢查是否有異常接著執(zhí)行下一條OPCode。

Zend/zend_vm_def.h源碼文件中的內(nèi)容其實(shí)是OPCode的代碼模板,在該源文件的開頭處可以看到這樣一段注釋:

/* If you change this file, please regenerate the zend_vm_execute.h and
 * zend_vm_opcodes.h files by running:
 * php zend_vm_gen.php
 */

說明zend_vm_execute.h和zend_vm_opcodes.h,實(shí)際上包括zend_vm_opcodes.c中的C代碼正是從Zend/zend_vm_def.h的代碼模板生成的。

操作數(shù)類型

每個(gè)OPCode最多使用兩個(gè)操作數(shù):op1和op2。每個(gè)操作數(shù)代表著OPCode的“形參”。例如ZEND_ASSIGN OPCode將op2的值賦值給op1代表的PHP變量,而其result則沒有使用到。

操作數(shù)的類型(與PHP變量的類型不同)決定了其含義以及使用方式:

  • IS_CV:Compiled Variable,說明該操作數(shù)是一個(gè)PHP變量
  • IS_TMP_VAR :虛擬機(jī)使用的臨時(shí)內(nèi)部PHP變量,不能夠在不同OPCode中復(fù)用(復(fù)用的這一點(diǎn)我并不清楚,還沒去研究過)
  • IS_VAR:虛擬機(jī)使用的內(nèi)部PHP變量,能夠在不同OPCode中復(fù)用(復(fù)用的這一點(diǎn)我并不清楚,還沒去研究過)
  • IS_CONST:代表一個(gè)常量值
  • IS_UNUSED:該操作數(shù)沒有任何意義,忽略該操作數(shù)

操作數(shù)的類型對(duì)性能優(yōu)化和內(nèi)存管理很重要。當(dāng)一個(gè)OPCode的Handler需要讀寫操作數(shù)時(shí),會(huì)根據(jù)操作數(shù)的類型通過不同的方式讀寫。

以加法例子,說明操作數(shù)類型:

$a + $b;  // IS_CV + IS_CV
1 + $a;   // IS_CONST + IS_CV
$$b + 3   // IS_VAR + IS_CONST
!$a + 3;  // IS_TMP_VAR + IS_CONST
OPCode Handler

我們已經(jīng)知道每個(gè)OPCode Handler最多接收2個(gè)操作數(shù),并且會(huì)根據(jù)操作數(shù)的類型讀寫操作數(shù)的值。如果在Handler中,通過switch判斷類型,然后再讀寫操作數(shù)的值,那么對(duì)性能會(huì)有很大損耗,因?yàn)榇嬖谔嗟姆种袛嗔耍╓hy is it good to avoid instruction branching where possible?),如下面的偽代碼所示:

int ZEND_ADD(zend_op *op1, zend_op *op2)
{
    void *op1_value;
    void *op2_value;

    switch (op1->type) {
        case IS_CV:
            op1_value = read_op_as_a_cv(op1);
        break;
        case IS_VAR:
            op1_value = read_op_as_a_var(op1);
        break;
        case IS_CONST:
            op1_value = read_op_as_a_const(op1);
        break;
        case IS_TMP_VAR:
            op1_value = read_op_as_a_tmp(op1);
        break;
        case IS_UNUSED:
            op1_value = NULL;
        break;
    }
    /* ... same thing to do for op2 .../

    /* do something with op1_value and op2_value (perform a math addition ?) */
}

要知道OPCode Handler在PHP執(zhí)行過程中是會(huì)被調(diào)用成千上萬次的,所以在Handler中對(duì)op1、op2做類型判斷,對(duì)性能并不好。

重新看下ZEND_ADD的代碼模板:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)

這說明ZEND_ADD接收op1和op2為CONST或TMPVAR或CV類型的操作數(shù)。

前面已經(jīng)提到zend_vm_execute.h和zend_vm_opcodes.h中的C代碼是從Zend/zend_vm_def.h的代碼模板生成的。通過查看zend_vm_execute.h,可以看到每個(gè)OPCode對(duì)應(yīng)的Handler(C函數(shù)),大部分OPCode會(huì)對(duì)應(yīng)多個(gè)Handler。以ZEND_ADD為例:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

ZEND_ADD的op1和op2的類型都有3種,所以一共生成了9個(gè)Handler,每個(gè)Handler的命名規(guī)范:ZEND_{OPCODE-NAME}_SPEC_{OP1-TYPE}_{OP2-TYPE}_HANDLER()。在編譯階段,操作數(shù)的類型是已知的,也就確定了每個(gè)編譯出來的OPCode對(duì)應(yīng)的Handler了。

那么這些Handler之間有什么不同呢?最大的不同應(yīng)該就是獲取操作數(shù)的方式:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *op1, *op2, *result;

    op1 = EX_CONSTANT(opline->op1);
    op2 = EX_CONSTANT(opline->op2);
    if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
       /* 省略 */
    } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
        /* 省略 */
    }

    SAVE_OPLINE();
    if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { //<-------- 這部分代碼會(huì)被編譯器優(yōu)化掉
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { //<-------- 這部分代碼會(huì)被編譯器優(yōu)化掉
        op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
    }
    add_function(EX_VAR(opline->result.var), op1, op2);


    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}


static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *op1, *op2, *result;

    op1 = EX_CONSTANT(opline->op1);
    op2 = _get_zval_ptr_cv_undef(execute_data, opline->op2.var);    //<-------- op2的獲取方式與上面的CONST不同
    if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
        /* 省略 */
    } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
        /* 省略 */
    }

    SAVE_OPLINE();
    if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { //<-------- 這部分代碼會(huì)被編譯器優(yōu)化掉
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { //<-------- IS_CV == IS_CV && 也會(huì)被編譯器優(yōu)化掉
        op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
    }
    add_function(EX_VAR(opline->result.var), op1, op2);

    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
OPArray

OPArray是指一個(gè)包含許多要被順序執(zhí)行的OPCode的數(shù)組,如下圖:

PHP的虛擬機(jī)是什么

OPArray由結(jié)構(gòu)體_zend_op_array表示:

struct _zend_op_array {
    /* Common elements */
    /* 省略 */
    /* END of common elements */

    /* 省略 */
    zend_op *opcodes; //<------ 存儲(chǔ)著OPCode的數(shù)組
    /* 省略 */
};

在PHP中,每個(gè)PHP用戶函數(shù)或者PHP腳本、傳遞給eval()的參數(shù),會(huì)被編譯為一個(gè)OPArray。

OPArray中包含了許多靜態(tài)的信息,能夠幫助執(zhí)行引擎更高效地執(zhí)行PHP代碼。部分重要的信息如下:

  • 當(dāng)前腳本的文件名,OPArray對(duì)應(yīng)的PHP代碼在腳本中起始和終止的行號(hào)
  • /**的代碼注釋信息
  • refcount引用計(jì)數(shù),OPArray是可共享的
  • try-catch-finally的跳轉(zhuǎn)信息
  • break-continue的跳轉(zhuǎn)信息
  • 當(dāng)前作用域所有PHP變量的名稱
  • 函數(shù)中用到的靜態(tài)變量
  • literals(字面量),編譯階段已知的值,例如字符串“foo”,或者整數(shù)42
  • 運(yùn)行時(shí)緩存槽,引擎會(huì)緩存一些后續(xù)執(zhí)行需要用到的東西

一個(gè)簡(jiǎn)單的例子:

$a = 8;
$b = 'foo';
echo $a + $b;

OPArray中的部分成員其內(nèi)容如下:

PHP的虛擬機(jī)是什么

OPArray包含的信息越多,即在編譯期間盡量的將已知的信息計(jì)算好存儲(chǔ)到OPArray中,執(zhí)行引擎就能夠更高效地執(zhí)行。我們可以看到每個(gè)字面量都已經(jīng)被編譯為zval并存儲(chǔ)到literals數(shù)組中(你可能發(fā)現(xiàn)這里多了一個(gè)整型值1,其實(shí)這是用于ZEND_RETURN OPCode的,PHP文件的OPArray默認(rèn)會(huì)返回1,但函數(shù)的OPArray默認(rèn)返回null)。OPArray所使用到的PHP變量的名字信息也被編譯為zend_string存儲(chǔ)到vars數(shù)組中,編譯后的OPCode則存儲(chǔ)到opcodes數(shù)組中。

OPCode的執(zhí)行

OPCode的執(zhí)行是通過一個(gè)while循環(huán)去做的:

//刪除了預(yù)處理語句
ZEND_API void execute_ex(zend_execute_data *ex)
{
    DCL_OPLINE

    const zend_op *orig_opline = opline;
    zend_execute_data *orig_execute_data = execute_data;
    execute_data = ex; 


    LOAD_OPLINE();

    while (1) {
        ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); //執(zhí)行OPCode對(duì)應(yīng)的C函數(shù)
        if (UNEXPECTED(!OPLINE)) { //當(dāng)前OPArray執(zhí)行完
            execute_data = orig_execute_data;
            opline = orig_opline;
            return;
        }
    }
    zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
}

那么是如何切換到下一個(gè)OPCode去執(zhí)行的呢?每個(gè)OPCode的Handler中都會(huì)調(diào)用到一個(gè)宏:

#define ZEND_VM_NEXT_OPCODE_EX(check_exception, skip) \
    CHECK_SYMBOL_TABLES() \
    if (check_exception) { \
        OPLINE = EX(opline) + (skip); \
    } else { \
        OPLINE = opline + (skip); \
    } \
    ZEND_VM_CONTINUE()

該宏會(huì)把當(dāng)前的opline+skip(skip通常是1),將opline指向下一條OPCode。opline是一個(gè)全局變量,指向當(dāng)前執(zhí)行的OPCode。

額外的一些東西

編譯器優(yōu)化

Zend/zend_vm_execute.h中,會(huì)看到如下奇怪的代碼:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_ARRAY_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    /* 省略 */

    if (IS_CONST == IS_UNUSED) {
        ZEND_VM_NEXT_OPCODE();
#if 0 || (IS_CONST != IS_UNUSED)
    } else {
        ZEND_VM_TAIL_CALL(ZEND_ADD_ARRAY_ELEMENT_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
#endif
    }
}

你可能會(huì)對(duì)if (IS_CONST == IS_UNUSED)#if 0 || (IS_CONST != IS_UNUSED)感到奇怪??聪缕鋵?duì)應(yīng)的模板代碼:

ZEND_VM_HANDLER(71, ZEND_INIT_ARRAY, CONST|TMP|VAR|UNUSED|CV, CONST|TMPVAR|UNUSED|CV)
{
    zval *array;
    uint32_t size;
    USE_OPLINE

    array = EX_VAR(opline->result.var);
    if (OP1_TYPE != IS_UNUSED) {
        size = opline->extended_value >> ZEND_ARRAY_SIZE_SHIFT;
    } else {
        size = 0;
    }
    ZVAL_NEW_ARR(array);
    zend_hash_init(Z_ARRVAL_P(array), size, NULL, ZVAL_PTR_DTOR, 0);

    if (OP1_TYPE != IS_UNUSED) {
        /* Explicitly initialize array as not-packed if flag is set */
        if (opline->extended_value & ZEND_ARRAY_NOT_PACKED) {
            zend_hash_real_init(Z_ARRVAL_P(array), 0);
        }
    }

    if (OP1_TYPE == IS_UNUSED) {
        ZEND_VM_NEXT_OPCODE();
#if !defined(ZEND_VM_SPEC) || (OP1_TYPE != IS_UNUSED)
    } else {
        ZEND_VM_DISPATCH_TO_HANDLER(ZEND_ADD_ARRAY_ELEMENT);
#endif
    }
}

php zend_vm_gen.php在生成zend_vm_execute.h時(shí),會(huì)把OP1_TYPE替換為op1的類型,從而生成這樣子的代碼:if (IS_CONST == IS_UNUSED),但C編譯器會(huì)把這些代碼優(yōu)化掉。

自定義Zend執(zhí)行引擎的生成

zend_vm_gen.php支持傳入?yún)?shù)--without-specializer,當(dāng)使用該參數(shù)時(shí),每個(gè)OPCode只會(huì)生成一個(gè)與之對(duì)應(yīng)的Handler,該Handler中會(huì)對(duì)操作數(shù)做類型判斷,然后再對(duì)操作數(shù)進(jìn)行讀寫。

另一個(gè)參數(shù)是--with-vm-kind=CALL|SWITCH|GOTO,CALL是默認(rèn)參數(shù)。

前面已提到執(zhí)行引擎是通過一個(gè)while循環(huán)執(zhí)行OPCode,每個(gè)OPCode中將opline增加1(通常情況下),然后回到while循環(huán)中,繼續(xù)執(zhí)行下一個(gè)OPCode,直到遇到ZEND_RETURN。

如果使用GOTO執(zhí)行策略:

/* GOTO策略下,execute_ex是一個(gè)超大的函數(shù) */
ZEND_API void execute_ex(zend_execute_data *ex)
{
    /* 省略 */

    while (1) {
        /* 省略 */
        goto *(void**)(OPLINE->handler);
        /* 省略 */
    }

    /* 省略 */
}

這里的goto并沒有直接使用符號(hào)名,其實(shí)是goto一個(gè)特殊的用法:Labels as Values。

執(zhí)行引擎中的跳轉(zhuǎn)

當(dāng)PHP腳本中出現(xiàn)if語句時(shí),是如何跳轉(zhuǎn)到相應(yīng)的OPCode然后繼續(xù)執(zhí)行的?看下面簡(jiǎn)單的例子:

$a = 8;
if ($a == 9) {
    echo "foo";
} else {
    echo "bar";
}

number of ops:  7
compiled vars:  !0 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 8
   3     1        IS_EQUAL                                         ~2      !0, 9
         2      > JMPZ                                                     ~2, ->5
   4     3    >   ECHO                                                     'foo'
         4      > JMP                                                      ->6
   6     5    >   ECHO                                                     'bar'
         6    > > RETURN                                                   1

當(dāng)$a != 9時(shí),JMPZ會(huì)使當(dāng)前執(zhí)行跳轉(zhuǎn)到第5個(gè)OPCode,否則JMP會(huì)使當(dāng)前執(zhí)行跳轉(zhuǎn)到第6個(gè)OPCode。其實(shí)就是對(duì)當(dāng)前的opline賦值為跳轉(zhuǎn)目標(biāo)OPCode的地址。

一些性能Tips

這部分內(nèi)容將展示如何通過查看生成的OPCode優(yōu)化PHP代碼。

echo a concatenation

示例代碼:

$foo = 'foo';
$bar = 'bar';

echo $foo . $bar;

OPArray:

number of ops:  5
compiled vars:  !0 = $foo, !1 = $bar
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 'foo'
   3     1        ASSIGN                                                   !1, 'bar'
   5     2        CONCAT                                           ~4      !0, !1
         3        ECHO                                                     ~4
         4      > RETURN                                                   1

$a和$b的值會(huì)被ZEND_CONCAT連接后存儲(chǔ)到一個(gè)臨時(shí)變量~4中,然后再echo輸出。

CONCAT操作需要分配一塊臨時(shí)的內(nèi)存,然后做內(nèi)存拷貝,echo輸出后,又要回收這塊臨時(shí)內(nèi)存。如果把代碼改為如下可消除CONCAT:

$foo = 'foo';
$bar = 'bar';

echo $foo , $bar;

OPArray:

number of ops:  5
compiled vars:  !0 = $foo, !1 = $bar
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 'foo'
   3     1        ASSIGN                                                   !1, 'bar'
   5     2        ECHO                                                     !0
         3        ECHO                                                     !1
         4      > RETURN                                                   1
define()和const

PHP 5.3引入了const關(guān)鍵字。

簡(jiǎn)單地說:

  • define()是一個(gè)函數(shù)調(diào)用
  • conast是關(guān)鍵字,不會(huì)產(chǎn)生函數(shù)調(diào)用,要比define()輕量許多
define('FOO', 'foo');
echo FOO;

number of ops:  7
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   INIT_FCALL                                               'define'
         1        SEND_VAL                                                 'FOO'
         2        SEND_VAL                                                 'foo'
         3        DO_ICALL                                                 
   3     4        FETCH_CONSTANT                                   ~1      'FOO'
         5        ECHO                                                     ~1
         6      > RETURN                                                   1

如果使用const:

const FOO = 'foo';
echo FOO;

number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   DECLARE_CONST                                            'FOO', 'foo'
   3     1        FETCH_CONSTANT                                   ~0      'FOO'
         2        ECHO                                                     ~0
         3      > RETURN                                                   1

然而const在使用上有一些限制:

  • const關(guān)鍵字定義常量必須處于最頂端的作用區(qū)域,這就意味著不能在函數(shù)內(nèi),循環(huán)內(nèi)以及if語句之內(nèi)用const 來定義常量
  • const的操作數(shù)必須為IS_CONST類型
動(dòng)態(tài)函數(shù)調(diào)用

盡量不要使用動(dòng)態(tài)的函數(shù)名去調(diào)用函數(shù):

function foo() { }
foo();

number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP                                                      
   3     1        INIT_FCALL                                               'foo'
         2        DO_UCALL                                                 
         3      > RETURN                                                   1

NOP表示不做任何操作,只是將當(dāng)前opline指向下一條OPCode,編譯器產(chǎn)生這條指令是由于歷史原因。為何到PHP7還不移除它呢= =

看看使用動(dòng)態(tài)的函數(shù)名去調(diào)用函數(shù):

function foo() { }
$a = 'foo';
$a();

number of ops:  5
compiled vars:  !0 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP                                                      
   3     1        ASSIGN                                                   !0, 'foo'
   4     2        INIT_DYNAMIC_CALL                                        !0
         3        DO_FCALL                                      0          
         4      > RETURN                                                   1

不同點(diǎn)在于INIT_FCALL和INIT_DYNAMIC_CALL,看下兩個(gè)函數(shù)的源碼:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *fname = EX_CONSTANT(opline->op2);
    zval *func;
    zend_function *fbc;
    zend_execute_data *call;

    fbc = CACHED_PTR(Z_CACHE_SLOT_P(fname)); /* 看下是否已經(jīng)在緩存中了 */
    if (UNEXPECTED(fbc == NULL)) {
        func = zend_hash_find(EG(function_table), Z_STR_P(fname)); /* 根據(jù)函數(shù)名查找函數(shù) */
        if (UNEXPECTED(func == NULL)) {
            SAVE_OPLINE();
            zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname));
            HANDLE_EXCEPTION();
        }
        fbc = Z_FUNC_P(func);
        CACHE_PTR(Z_CACHE_SLOT_P(fname), fbc); /* 緩存查找結(jié)果 */
    }

    call = zend_vm_stack_push_call_frame_ex(
        opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
        fbc, opline->extended_value, NULL, NULL);
    call->prev_execute_data = EX(call);
    EX(call) = call;

    ZEND_VM_NEXT_OPCODE();
}

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_DYNAMIC_CALL_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    /* 200多行代碼,就不貼出來了,會(huì)根據(jù)CV的類型(字符串、對(duì)象、數(shù)組)做不同的函數(shù)查找 */
}

很顯然INIT_FCALL相比INIT_DYNAMIC_CALL要輕量許多。

類的延遲綁定

簡(jiǎn)單地說,類A繼承類B,類B最好先于類A被定義。

class Bar { }
class Foo extends Bar { }

number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP
   3     1        NOP
         2        NOP
         3      > RETURN                                                   1

從生成的OPCode可以看出,上述PHP代碼在運(yùn)行時(shí),執(zhí)行引擎不需要做任何操作。類的定義是比較耗性能的工作,例如解析類的繼承關(guān)系,將父類的方法/屬性添加進(jìn)來,但編譯器已經(jīng)做完了這些繁重的工作。

如果類A先于類B被定義:

class Foo extends Bar { }
class Bar { }

number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   FETCH_CLASS                                   0  :0      'Bar'
         1        DECLARE_INHERITED_CLASS                                  '%00foo%2Fhome%2Froketyyang%2Ftest.php0x7fb192b7101f', 'foo'
   3     2        NOP
         3      > RETURN                                                   1

這里定義了Foo繼承自Bar,但當(dāng)編譯器讀取到Foo的定義時(shí),編譯器并不知道任何關(guān)于Bar的情況,所以編譯器就生成相應(yīng)的OPCode,使其定義延遲到執(zhí)行時(shí)。在一些其他的動(dòng)態(tài)類型的語言中,可能會(huì)產(chǎn)生錯(cuò)誤:Parse error : class not found

除了類的延遲綁定,像接口、traits都存在延遲綁定耗性能的問題。

對(duì)于定位PHP性能問題,通常都是先用xhprof或xdebug profile進(jìn)行定位,需要通過查看OPCode定位性能問題的場(chǎng)景還是比較少的。

關(guān)于PHP的虛擬機(jī)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI