Heim > Backend-Entwicklung > PHP7 > Verstehen Sie, was eine virtuelle PHP7-Maschine ist

Verstehen Sie, was eine virtuelle PHP7-Maschine ist

coldplay.xixi
Freigeben: 2023-02-17 16:14:01
nach vorne
3045 Leute haben es durchsucht

Verstehen Sie, was eine virtuelle PHP7-Maschine ist

Der größte Teil des Inhalts dieses Artikels wurde aus Getting into the Zend Execution engine (PHP 5) übersetzt, wobei einige Anpassungen vorgenommen wurden. Der Originaltext basiert auf PHP 5 und diesem Artikel basiert auf PHP 7.

PHP: Eine interpretierte Sprache


PHP wird als Skriptsprache oder interpretierte Sprache bezeichnet. Warum? Die PHP-Sprache wird nicht direkt in Maschinenanweisungen kompiliert, sondern in eine Form von Zwischencode. Offensichtlich kann sie nicht direkt auf der CPU ausgeführt werden. Daher muss die Ausführung von PHP auf einer virtuellen Maschine auf Prozessebene erfolgen (siehe Virtuelle Maschinen verarbeiten in Virtuelle Maschine, im Folgenden als virtuelle Maschine bezeichnet).

Die PHP-Sprache, einschließlich anderer interpretierter Sprachen, ist eigentlich ein plattformübergreifendes Programm, das zur Ausführung abstrakter Anweisungen entwickelt wurde. PHP wird hauptsächlich zur Lösung von Problemen im Zusammenhang mit der WEB-Entwicklung verwendet.

Programme, die in Programmiersprachen wie Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript usw. geschrieben sind, müssen alle auf einer virtuellen Maschine ausgeführt werden. Die virtuelle Maschine kann mithilfe der JIT-Kompilierungstechnologie einige Anweisungen der virtuellen Maschine in Maschinenanweisungen kompilieren, um die Leistung zu verbessern. Bruder Niao entwickelt bereits PHP, um JIT-Unterstützung hinzuzufügen.

Empfohlenes Tutorial: „PHP-Tutorial

Vorteile der Verwendung interpretierter Sprachen:

  • Das Schreiben von Code ist einfach und kann sein schnell erledigt Entwickeln

  • Automatische Speicherverwaltung

  • Abstrakte Datentypen, hohe Programmportabilität

Nachteile :

  • Unfähigkeit, den Speicher direkt zu verwalten und Prozessressourcen zu nutzen

  • Langsamer als in Maschinenanweisungen kompilierte Sprachen: Erfordert normalerweise mehr CPU-Zyklen um die gleiche Aufgabe zu erledigen (JIT versucht, die Lücke zu schließen, kann sie aber nie vollständig beseitigen)

  • Abstraktion so sehr, dass es für viele Programmierer schwierig ist, wenn mit dem Programm etwas schief geht Erklären Sie die Grundursache

Der letzte Mangel ist der Grund, warum der Autor diesen Artikel geschrieben hat. Der Autor ist der Meinung, dass Programmierer einige Dinge auf niedriger Ebene verstehen sollten.

Der Autor möchte den Lesern mit diesem Artikel erklären, wie PHP funktioniert. Das in diesem Artikel erwähnte Wissen über die virtuelle PHP-Maschine kann auch auf andere interpretierte Sprachen angewendet werden. Normalerweise sind die größten Unterschiede zwischen verschiedenen Implementierungen virtueller Maschinen: ob JIT verwendet werden soll, parallele Anweisungen für virtuelle Maschinen (im Allgemeinen Multithreading verwenden, PHP verwendet diese Technologie nicht), Speicherverwaltungs-/Garbage-Collection-Algorithmus.

Die virtuelle Zend-Maschine ist in zwei Teile unterteilt:

  • Kompilierung: Konvertieren Sie PHP-Code in Anweisungen für virtuelle Maschinen (OPCode)

  • Ausführung: Führen Sie die generierten Anweisungen für die virtuelle Maschine aus

Dieser Artikel behandelt nicht den Kompilierungsteil, sondern konzentriert sich hauptsächlich auf die Ausführungs-Engine der virtuellen Zend-Maschine. Die Ausführungs-Engine der PHP7-Version wurde teilweise neu strukturiert, wodurch der Ausführungsstapel des PHP-Codes einfacher und übersichtlicher wurde und auch die Leistung verbessert wurde.

In diesem Artikel wird PHP 7.0.7 als Beispiel verwendet.

OPCode


Wikipedias Erklärung von OPCode:

Opcodes finden sich auch in sogenannten Bytecodes und anderen Darstellungen, die eher für einen Software-Interpreter gedacht sind Diese softwarebasierten Befehlssätze verwenden häufig Datentypen und Operationen auf einer etwas höheren Ebene Die meisten Hardware-Gegenstücke sind jedoch dennoch ähnlich aufgebaut.

OPCode und ByteCode unterscheiden sich konzeptionell.

Mein persönliches Verständnis: OPCode ist eine Anweisung, die angibt, was zu tun ist, während ByteCode aus einer Folge von OPCode/Daten besteht, die angibt, was zu tun ist. Am Beispiel einer Addition weist OPCode die Ausführungs-Engine an, Parameter 1 und Parameter 2 hinzuzufügen, während ByteCode die Ausführungs-Engine anweist, 45 und 56 hinzuzufügen.

Referenz: Unterschied zwischen Opcode und Bytecode und Unterschied zwischen: Opcode, Bytecode, Mnemonik, Maschinencode und Assembly

In PHP listet die Zend/zend_vm_opcodes.h Quellcodedatei alle unterstützten OPCodes auf. Normalerweise beschreibt der Name jedes OPCodes seine Bedeutung, wie zum Beispiel:

  • ZEND_ADD: führt eine Additionsoperation für zwei Operanden aus

  • ZEND_NEW : Erstellen ein Objekt

  • ZEND_FETCH_DIM_R: Lesen Sie den Wert einer bestimmten Dimension im Operanden. Wenn Sie beispielsweise die echo $foo[0]-Anweisung ausführen, müssen Sie den Wert mit Index 0 von $ abrufen foo-Array

OPCode wird durch die zend_op-Struktur dargestellt:

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; /* 结果类型 */
};
Nach dem Login kopieren

Jeder OPcode wird auf die gleiche Weise ausgeführt: OPCode hat seine entsprechende C-Funktion. Sie können 0, 1 oder 2 Operanden (op1, op2) verwenden und das Ergebnis schließlich in result speichern, möglicherweise mit einigen zusätzlichen Informationen, die in Extended_value gespeichert sind.

Sehen Sie sich an, wie der OPCode von ZEND_ADD in der Zend/zend_vm_def.h Quellcodedatei aussieht:

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();
}
Nach dem Login kopieren

可以看出这其实不是一个合法的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
 */
Nach dem Login kopieren

说明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
Nach dem Login kopieren

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 ?) */
}
Nach dem Login kopieren

要知道OPCode Handler在PHP执行过程中是会被调用成千上万次的,所以在Handler中对op1、op2做类型判断,对性能并不好。

重新看下ZEND_ADD的代码模板:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
Nach dem Login kopieren

这说明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)
Nach dem Login kopieren

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();
}
Nach dem Login kopieren

OPArray


OPArray是指一个包含许多要被顺序执行的OPCode的数组,如下图:

OPArray由结构体_zend_op_array表示:

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

    /* 省略 */
    zend_op *opcodes; //<------ 存储着OPCode的数组
    /* 省略 */
};
Nach dem Login kopieren

在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;
Nach dem Login kopieren

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");
}
Nach dem Login kopieren

那么是如何切换到下一个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()
Nach dem Login kopieren

该宏会把当前的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
    }
}
Nach dem Login kopieren

你可能会对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
    }
}
Nach dem Login kopieren

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);
        /* 省略 */
    }

    /* 省略 */
}
Nach dem Login kopieren

这里的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
Nach dem Login kopieren

$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;
Nach dem Login kopieren

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
Nach dem Login kopieren

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

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

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

echo $foo , $bar;
Nach dem Login kopieren

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
Nach dem Login kopieren
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
Nach dem Login kopieren

如果使用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
Nach dem Login kopieren

然而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
Nach dem Login kopieren

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
Nach dem Login kopieren

不同点在于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的类型(字符串、对象、数组)做不同的函数查找 */
}
Nach dem Login kopieren

很显然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
Nach dem Login kopieren

从生成的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
Nach dem Login kopieren

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

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

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

总结


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

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

Das obige ist der detaillierte Inhalt vonVerstehen Sie, was eine virtuelle PHP7-Maschine ist. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
php
Quelle:juejin.im
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage