PHP7 仮想マシンとは何かを理解する

coldplay.xixi
リリース: 2023-02-17 16:14:01
転載
2983 人が閲覧しました

PHP7 仮想マシンとは何かを理解する

この記事の内容の大部分は、「Getting into the Zend Execution Engine (PHP 5)」から翻訳され、いくつかの調整が加えられています。原文は PHP 5 に基づいており、この記事はPHP 7 に基づいています。

PHP: インタプリタ言語


PHP は、スクリプト言語またはインタプリタ言語と呼ばれます。なぜ? PHP言語は機械語命令に直接コンパイルされるのではなく、中間コードの形にコンパイルされるため、CPU上で直接実行することは当然できません。したがって、PHP の実行はプロセス レベルの仮想マシン上で行う必要があります (「仮想マシンのプロセス仮想マシン」を参照。以下、仮想マシンと呼びます)。

PHP 言語は、他のインタープリタ型言語も含めて、実際には抽象命令を実行するように設計されたクロスプラットフォーム プログラムです。 PHP は主に WEB 開発に関する問題を解決するために使用されます。

Java、Python、C#、Ruby、Pascal、Lua、Perl、JavaScript などのプログラミング言語で記述されたプログラムはすべて、仮想マシン上で実行する必要があります。仮想マシンは、パフォーマンスを向上させるために、JIT コンパイル テクノロジを通じて一部の仮想マシン命令をマシン命令にコンパイルできます。 Niao 兄弟は、JIT サポートを追加するためにすでに PHP を開発しています。

推奨チュートリアル: 「PHP チュートリアル

インタープリタ型言語を使用する利点:

  • コードは簡単に記述でき、

  • #自動メモリ管理

  • #抽象データ型、高いプログラム移植性
  • #欠点:

メモリを直接管理できず、プロセス リソースを使用できない
  • 機械語にコンパイルされた言語よりも遅い: 通常、より多くの CPU が必要になります。同じタスクを完了するために複数のサイクルを実行します (JIT はギャップを埋めようとしますが、完全に排除することはできません)
  • 抽象化が多すぎるため、プログラムで何か問題が発生した場合、多くのプログラマーは困難を感じます。根本原因を説明するには
  • 最後の欠点が、著者がこの記事を書いた理由であり、プログラマーは根本的なことを理解する必要があると著者は感じています。
著者は、この記事を通じて PHP がどのように動作するかを読者に説明したいと考えています。この記事で説明した PHP 仮想マシンに関する知識は、他のインタープリタ言語にも適用できます。通常、さまざまな仮想マシン実装における最大の違いは、JIT を使用するかどうか、並列仮想マシン命令 (通常はマルチスレッドを使用します。PHP はこのテクノロジを使用しません)、メモリ管理/ガベージ コレクション アルゴリズムです。

Zend 仮想マシンは 2 つの部分に分かれています:

コンパイル: PHP コードを仮想マシン命令 (OPCode) に変換します
  • 実行: 生成された仮想マシン命令を実行します
  • ##この記事ではコンパイル部分には関与しませんが、主に Zend 仮想マシンの実行エンジンに焦点を当てます。 PHP7 バージョンの実行エンジンは部分的に再構築され、PHP コードの実行スタックがよりシンプルかつ明確になり、パフォーマンスも向上しました。

  • この記事では、例として PHP 7.0.7 を使用します。

OPCode

Wikipedia の OPCode の説明:


オペコードは、いわゆるバイト コードや、ソフトウェア インタプリタを目的としたその他の表現にも見られます。これらのソフトウェア ベースの命令セットでは、多くの場合、ハードウェア デバイスよりもわずかに高いレベルのデータ型と操作が使用されます。 ほとんどのハードウェアは同等ですが、同様の方針に沿って構築されています。

OPCode と ByteCode は概念的に異なります。

私の個人的な理解: OPCode は何をすべきかを示す命令として機能し、ByteCode は何をすべきかを示す OPCode/データのシーケンスで構成されます。加算を例にとると、OPCode は実行エンジンにパラメータ 1 とパラメータ 2 を追加するように指示し、ByteCode は実行エンジンに 45 と 56 を追加するように指示します。

参考: オペコードとバイトコードの違い、およびオペコード、バイトコード、ニーモニック、マシンコード、アセンブリの違い

PHP では、

Zend/zend_vm_opcodes.h

ソース コードこのファイルには、サポートされているすべての OPCode がリストされています。通常、各 OPCode の名前は、次のようにその意味を説明します。

ZEND_ADD: 2 つのオペランドに対して加算演算を実行します。

  • ZEND_NEW : Createオブジェクト

  • ZEND_FETCH_DIM_R: オペランドの特定のディメンションの値を読み取ります。たとえば、

    echo $foo[0]
  • ステートメントを実行するときは、次のことを行う必要があります。 $foo を取得します。配列インデックス 0
  • OPCode の値は、zend_op 構造体で表されます。

    struct _zend_op {
        const void *handler; /* 执行该OPCode的C函数 */
        znode_op op1; /* 操作数1 */
        znode_op op2; /* 操作数2 */
        znode_op result; /* 结果 */
        uint32_t extended_value; /* 额外的信息 */
        uint32_t lineno; /* 该OPCode对应PHP源码所在的行 */
        zend_uchar opcode; /* OPCode对应的数值 */
        zend_uchar op1_type; /* 操作数1类型 */
        zend_uchar op2_type; /* 操作数2类型 */
        zend_uchar result_type; /* 结果类型 */
    };
    ログイン後にコピー
  • 各 OPcode は同じ方法で実行されます。OPCode には、対応する C があります。この C 関数を実行するとき、0、1、または 2 つのオペランド (op1、op2) が使用され、最終的に結果が result に格納され、いくつかの追加情報が extend_value に格納されます。

ソース コード ファイル

Zend/zend_vm_def.h

で、ZEND_ADD の OPCode がどのようなものかを確認してください:

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();
}
ログイン後にコピー

可以看出这其实不是一个合法的C代码,可以把它看成代码模板。稍微解读下这个代码模板:1 就是在Zend/zend_vm_opcodes.h中define定义的ZEND_ADD的值;ZEND_ADD接收两个操作数,如果两个操作数都为IS_LONG类型,那么就调用fast_long_add_function(该函数内部使用汇编实现加法操作);如果两个操作数,都为IS_DOUBLE类型或者1个是IS_DOUBLE类型,另1个是IS_LONG类型,那么就直接执行double的加法操作;如果存在1个操作数不是IS_LONG或IS_DOUBLE类型,那么就调用add_function(比如两个数组做加法操作);最后检查是否有异常接着执行下一条OPCode。

Zend/zend_vm_def.h源码文件中的内容其实是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,实际上包括zend_vm_opcodes.c中的C代码正是从Zend/zend_vm_def.h的代码模板生成的。

操作数类型


每个OPCode最多使用两个操作数:op1和op2。每个操作数代表着OPCode的“形参”。例如ZEND_ASSIGN OPCode将op2的值赋值给op1代表的PHP变量,而其result则没有使用到。

操作数的类型(与PHP变量的类型不同)决定了其含义以及使用方式:

  • IS_CV:Compiled Variable,说明该操作数是一个PHP变量

  • IS_TMP_VAR :虚拟机使用的临时内部PHP变量,不能够在不同OPCode中复用(复用的这一点我并不清楚,还没去研究过)

  • IS_VAR:虚拟机使用的内部PHP变量,能够在不同OPCode中复用(复用的这一点我并不清楚,还没去研究过)

  • IS_CONST:代表一个常量值

  • IS_UNUSED:该操作数没有任何意义,忽略该操作数

操作数的类型对性能优化和内存管理很重要。当一个OPCode的Handler需要读写操作数时,会根据操作数的类型通过不同的方式读写。

以加法例子,说明操作数类型:

$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


我们已经知道每个OPCode Handler最多接收2个操作数,并且会根据操作数的类型读写操作数的值。如果在Handler中,通过switch判断类型,然后再读写操作数的值,那么对性能会有很大损耗,因为存在太多的分支判断了(Why 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执行过程中是会被调用成千上万次的,所以在Handler中对op1、op2做类型判断,对性能并不好。

重新看下ZEND_ADD的代码模板:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
ログイン後にコピー

这说明ZEND_ADD接收op1和op2为CONST或TMPVAR或CV类型的操作数。

前面已经提到zend_vm_execute.h和zend_vm_opcodes.h中的C代码是从Zend/zend_vm_def.h的代码模板生成的。通过查看zend_vm_execute.h,可以看到每个OPCode对应的Handler(C函数),大部分OPCode会对应多个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个Handler,每个Handler的命名规范:ZEND_{OPCODE-NAME}_SPEC_{OP1-TYPE}_{OP2-TYPE}_HANDLER()。在编译阶段,操作数的类型是已知的,也就确定了每个编译出来的OPCode对应的Handler了。

那么这些Handler之间有什么不同呢?最大的不同应该就是获取操作数的方式:

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)) { //<-------- 这部分代码会被编译器优化掉
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (IS_CONST == 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);


    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)) { //<-------- 这部分代码会被编译器优化掉
        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 && 也会被编译器优化掉
        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是指一个包含许多要被顺序执行的OPCode的数组,如下图:

OPArray由结构体_zend_op_array表示:

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

    /* 省略 */
    zend_op *opcodes; //<------ 存储着OPCode的数组
    /* 省略 */
};
ログイン後にコピー

在PHP中,每个PHP用户函数或者PHP脚本、传递给eval()的参数,会被编译为一个OPArray。

OPArray中包含了许多静态的信息,能够帮助执行引擎更高效地执行PHP代码。部分重要的信息如下:

  • 当前脚本的文件名,OPArray对应的PHP代码在脚本中起始和终止的行号

  • /**的代码注释信息

  • refcount引用计数,OPArray是可共享的

  • try-catch-finally的跳转信息

  • break-continue的跳转信息

  • 当前作用域所有PHP变量的名称

  • 函数中用到的静态变量

  • literals(字面量),编译阶段已知的值,例如字符串“foo”,或者整数42

  • 运行时缓存槽,引擎会缓存一些后续执行需要用到的东西

一个简单的例子:

$a = 8;
$b = &#39;foo&#39;;
echo $a + $b;
ログイン後にコピー

OPArray中的部分成员其内容如下:

OPArray包含的信息越多,即在编译期间尽量的将已知的信息计算好存储到OPArray中,执行引擎就能够更高效地执行。我们可以看到每个字面量都已经被编译为zval并存储到literals数组中(你可能发现这里多了一个整型值1,其实这是用于ZEND_RETURN OPCode的,PHP文件的OPArray默认会返回1,但函数的OPArray默认返回null)。OPArray所使用到的PHP变量的名字信息也被编译为zend_string存储到vars数组中,编译后的OPCode则存储到opcodes数组中。

OPCode的执行


OPCode的执行是通过一个while循环去做的:

//删除了预处理语句
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); //执行OPCode对应的C函数
        if (UNEXPECTED(!OPLINE)) { //当前OPArray执行完
            execute_data = orig_execute_data;
            opline = orig_opline;
            return;
        }
    }
    zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn&#39;t happen");
}
ログイン後にコピー

那么是如何切换到下一个OPCode去执行的呢?每个OPCode的Handler中都会调用到一个宏:

#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()
ログイン後にコピー

该宏会把当前的opline+skip(skip通常是1),将opline指向下一条OPCode。opline是一个全局变量,指向当前执行的OPCode。

额外的一些东西


编译器优化

Zend/zend_vm_execute.h中,会看到如下奇怪的代码:

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
    }
}
ログイン後にコピー

你可能会对if (IS_CONST == IS_UNUSED)#if 0 || (IS_CONST != IS_UNUSED)感到奇怪。看下其对应的模板代码:

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时,会把OP1_TYPE替换为op1的类型,从而生成这样子的代码:if (IS_CONST == IS_UNUSED),但C编译器会把这些代码优化掉。

自定义Zend执行引擎的生成

zend_vm_gen.php支持传入参数--without-specializer,当使用该参数时,每个OPCode只会生成一个与之对应的Handler,该Handler中会对操作数做类型判断,然后再对操作数进行读写。

另一个参数是--with-vm-kind=CALL|SWITCH|GOTO,CALL是默认参数。

前面已提到执行引擎是通过一个while循环执行OPCode,每个OPCode中将opline增加1(通常情况下),然后回到while循环中,继续执行下一个OPCode,直到遇到ZEND_RETURN。

如果使用GOTO执行策略:

/* GOTO策略下,execute_ex是一个超大的函数 */
ZEND_API void execute_ex(zend_execute_data *ex)
{
    /* 省略 */

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

    /* 省略 */
}
ログイン後にコピー

这里的goto并没有直接使用符号名,其实是goto一个特殊的用法:Labels as Values。

执行引擎中的跳转

当PHP脚本中出现if语句时,是如何跳转到相应的OPCode然后继续执行的?看下面简单的例子:

$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                                                     &#39;foo&#39;
         4      > JMP                                                      ->6
   6     5    >   ECHO                                                     &#39;bar&#39;
         6    > > RETURN                                                   1
ログイン後にコピー

$a != 9时,JMPZ会使当前执行跳转到第5个OPCode,否则JMP会使当前执行跳转到第6个OPCode。其实就是对当前的opline赋值为跳转目标OPCode的地址。

一些性能Tips


这部分内容将展示如何通过查看生成的OPCode优化PHP代码。

echo a concatenation

示例代码:

$foo = &#39;foo&#39;;
$bar = &#39;bar&#39;;

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, &#39;foo&#39;
   3     1        ASSIGN                                                   !1, &#39;bar&#39;
   5     2        CONCAT                                           ~4      !0, !1
         3        ECHO                                                     ~4
         4      > RETURN                                                   1
ログイン後にコピー

$a和$b的值会被ZEND_CONCAT连接后存储到一个临时变量~4中,然后再echo输出。

CONCAT操作需要分配一块临时的内存,然后做内存拷贝,echo输出后,又要回收这块临时内存。如果把代码改为如下可消除CONCAT:

$foo = &#39;foo&#39;;
$bar = &#39;bar&#39;;

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, &#39;foo&#39;
   3     1        ASSIGN                                                   !1, &#39;bar&#39;
   5     2        ECHO                                                     !0
         3        ECHO                                                     !1
         4      > RETURN                                                   1
ログイン後にコピー
define()和const

PHP 5.3引入了const关键字。

简单地说:

  • define()是一个函数调用

  • conast是关键字,不会产生函数调用,要比define()轻量许多

define(&#39;FOO&#39;, &#39;foo&#39;);
echo FOO;

number of ops:  7
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   INIT_FCALL                                               &#39;define&#39;
         1        SEND_VAL                                                 &#39;FOO&#39;
         2        SEND_VAL                                                 &#39;foo&#39;
         3        DO_ICALL                                                 
   3     4        FETCH_CONSTANT                                   ~1      &#39;FOO&#39;
         5        ECHO                                                     ~1
         6      > RETURN                                                   1
ログイン後にコピー

如果使用const:

const FOO = &#39;foo&#39;;
echo FOO;

number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   DECLARE_CONST                                            &#39;FOO&#39;, &#39;foo&#39;
   3     1        FETCH_CONSTANT                                   ~0      &#39;FOO&#39;
         2        ECHO                                                     ~0
         3      > RETURN                                                   1
ログイン後にコピー

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

  • const关键字定义常量必须处于最顶端的作用区域,这就意味着不能在函数内,循环内以及if语句之内用const 来定义常量

  • const的操作数必须为IS_CONST类型

动态函数调用

尽量不要使用动态的函数名去调用函数:

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                                               &#39;foo&#39;
         2        DO_UCALL                                                 
         3      > RETURN                                                   1
ログイン後にコピー

NOP表示不做任何操作,只是将当前opline指向下一条OPCode,编译器产生这条指令是由于历史原因。为何到PHP7还不移除它呢= =

看看使用动态的函数名去调用函数:

function foo() { }
$a = &#39;foo&#39;;
$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, &#39;foo&#39;
   4     2        INIT_DYNAMIC_CALL                                        !0
         3        DO_FCALL                                      0          
         4      > RETURN                                                   1
ログイン後にコピー

不同点在于INIT_FCALL和INIT_DYNAMIC_CALL,看下两个函数的源码:

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)); /* 看下是否已经在缓存中了 */
    if (UNEXPECTED(fbc == NULL)) {
        func = zend_hash_find(EG(function_table), Z_STR_P(fname)); /* 根据函数名查找函数 */
        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); /* 缓存查找结果 */
    }

    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多行代码,就不贴出来了,会根据CV的类型(字符串、对象、数组)做不同的函数查找 */
}
ログイン後にコピー

很显然INIT_FCALL相比INIT_DYNAMIC_CALL要轻量许多。

类的延迟绑定

简单地说,类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代码在运行时,执行引擎不需要做任何操作。类的定义是比较耗性能的工作,例如解析类的继承关系,将父类的方法/属性添加进来,但编译器已经做完了这些繁重的工作。

如果类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      &#39;Bar&#39;
         1        DECLARE_INHERITED_CLASS                                  &#39;%00foo%2Fhome%2Froketyyang%2Ftest.php0x7fb192b7101f&#39;, &#39;foo&#39;
   3     2        NOP
         3      > RETURN                                                   1
ログイン後にコピー

这里定义了Foo继承自Bar,但当编译器读取到Foo的定义时,编译器并不知道任何关于Bar的情况,所以编译器就生成相应的OPCode,使其定义延迟到执行时。在一些其他的动态类型的语言中,可能会产生错误:Parse error : class not found

除了类的延迟绑定,像接口、traits都存在延迟绑定耗性能的问题。

对于定位PHP性能问题,通常都是先用xhprof或xdebug profile进行定位,需要通过查看OPCode定位性能问题的场景还是比较少的。

总结


希望通过这篇文章,能让你了解到PHP虚拟机大致是如何工作的。具体opcode的执行,以及函数调用涉及到的上下文切换,有许多细节性的东西,限于本文篇幅,在另一篇文章:PHP 7 中函数调用的实现进行讲解。

推荐相关文章:《linux系统教程

以上がPHP7 仮想マシンとは何かを理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
php
ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!