Maison > développement back-end > PHP7 > le corps du texte

Comprendre ce qu'est une machine virtuelle PHP7

coldplay.xixi
Libérer: 2023-02-17 16:14:01
avant
2992 Les gens l'ont consulté

Comprendre ce qu'est une machine virtuelle PHP7

La majeure partie du contenu de cet article est traduite de Getting into the Zend Execution Engine (PHP 5), avec quelques ajustements apportés. Le texte original est basé sur PHP 5, et cet article. est basé sur PHP 7.

PHP : Un langage interprété


PHP est appelé langage de script ou langage interprété. Pourquoi? Le langage PHP n'est pas directement compilé en instructions machine, mais est compilé sous une forme de code intermédiaire. Il ne peut évidemment pas être exécuté directement sur le CPU. Par conséquent, l'exécution de PHP doit se faire sur une machine virtuelle au niveau du processus (voir Traiter les machines virtuelles dans Machine virtuelle, ci-après dénommée machine virtuelle).

Le langage PHP, y compris d'autres langages interprétés, est en fait un programme multiplateforme conçu pour exécuter des instructions abstraites. PHP est principalement utilisé pour résoudre des problèmes liés au développement WEB.

Les programmes écrits dans des langages de programmation tels que Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript, etc. doivent tous être exécutés sur une machine virtuelle. La machine virtuelle peut compiler certaines instructions de machine virtuelle en instructions machine via la technologie de compilation JIT pour améliorer les performances. Brother Niao développe déjà PHP pour ajouter le support JIT.

Tutoriel recommandé : "Tutoriel PHP"

Avantages de l'utilisation de langages interprétés :

  • L'écriture de code est simple et peut être fait rapidement Développer

  • Gestion automatique de la mémoire

  • Types de données abstraits, portabilité élevée du programme

Inconvénients :

  • Incapacité de gérer directement la mémoire et d'utiliser les ressources du processus

  • Plus lent que les langages compilés en instructions machine : nécessite généralement plus de cycles CPU multiples pour accomplir la même tâche (JIT essaie de combler l'écart, mais ne peut jamais l'éliminer complètement)

  • Résume tellement que lorsque quelque chose ne va pas avec le programme, de nombreux programmeurs ont du mal à expliquer la cause profonde

La dernière lacune est la raison pour laquelle l'auteur a écrit cet article. L'auteur estime que les programmeurs doivent comprendre certaines choses de bas niveau.

L'auteur espère expliquer aux lecteurs comment fonctionne PHP à travers cet article. Les connaissances sur la machine virtuelle PHP mentionnées dans cet article peuvent également être appliquées à d'autres langages interprétés. Habituellement, les plus grandes différences entre les différentes implémentations de machines virtuelles sont : l'utilisation ou non de JIT, les instructions de machine virtuelle parallèles (utilisant généralement le multi-threading, PHP n'utilise pas cette technologie), l'algorithme de gestion de la mémoire/garbage collection.

La machine virtuelle Zend est divisée en deux parties :

  • Compilation : Convertir le code PHP en instructions de machine virtuelle (OPCode)

  • Exécution : Exécuter les instructions de la machine virtuelle générées

Cet article ne couvrira pas la partie compilation, mais se concentrera principalement sur le moteur d'exécution de la machine virtuelle Zend. Le moteur d'exécution de la version PHP7 a été partiellement restructuré, rendant la pile d'exécution du code PHP plus simple et plus claire, et ses performances ont également été améliorées.

Cet article utilise PHP 7.0.7 comme exemple.

OPCode


Explication Wikipédia d'OPCode :

Les opcodes peuvent également être trouvés dans ce qu'on appelle les codes d'octet et d'autres représentations destinées à un interpréteur logiciel plutôt qu'un périphérique matériel, ces jeux d'instructions logiciels utilisent souvent des types de données et des opérations de niveau légèrement supérieur à celui-ci. La plupart des homologues matériels, mais sont néanmoins construits selon des lignes similaires.

OPCode et ByteCode sont conceptuellement différents.

Ma compréhension personnelle : OPCode est une instruction indiquant quoi faire, tandis que ByteCode consiste en une séquence d'OPCode/données indiquant quoi faire. En prenant un ajout comme exemple, OPCode indique au moteur d'exécution d'ajouter le paramètre 1 et le paramètre 2, tandis que ByteCode indique au moteur d'exécution d'ajouter 45 et 56.

Référence : Différence entre Opcode et Bytecode et Différence entre : Opcode, byte code, mnémoniques, code machine et assembly

En PHP, le Zend/zend_vm_opcodes.h fichier de code source répertorie tous les OPCodes pris en charge. Habituellement, le nom de chaque OPCode décrit sa signification, comme par exemple :

  • ZEND_ADD : effectue une opération d'addition sur deux opérandes

  • ZEND_NEW : Créer un objet

  • ZEND_FETCH_DIM_R : Lisez la valeur d'une certaine dimension dans l'opérande. Par exemple, lors de l'exécution de l'instruction echo $foo[0], vous devez obtenir la valeur d'index 0 du $. foo array

OPCode est représenté par la structure 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; /* 结果类型 */
};
Copier après la connexion

Chaque OPcode est exécuté de la même manière : OPCode a sa fonction C correspondante. Lors de l'exécution de la fonction C, vous pouvez utiliser 0, 1 ou 2 opérandes (op1, op2), et enfin stocker le résultat dans result, éventuellement avec des informations supplémentaires stockées dans extend_value.

Regardez à quoi ressemble l'OPCode de ZEND_ADD, dans le Zend/zend_vm_def.h fichier de code source :

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();
}
Copier après la connexion

可以看出这其实不是一个合法的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
 */
Copier après la connexion

说明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
Copier après la connexion

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 ?) */
}
Copier après la connexion

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

重新看下ZEND_ADD的代码模板:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
Copier après la connexion

这说明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)
Copier après la connexion

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();
}
Copier après la connexion

OPArray


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

OPArray由结构体_zend_op_array表示:

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

    /* 省略 */
    zend_op *opcodes; //<------ 存储着OPCode的数组
    /* 省略 */
};
Copier après la connexion

在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;
Copier après la connexion

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");
}
Copier après la connexion

那么是如何切换到下一个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()
Copier après la connexion

该宏会把当前的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
    }
}
Copier après la connexion

你可能会对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
    }
}
Copier après la connexion

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

    /* 省略 */
}
Copier après la connexion

这里的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
Copier après la connexion

$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;
Copier après la connexion

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
Copier après la connexion

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

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

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

echo $foo , $bar;
Copier après la connexion

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
Copier après la connexion
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
Copier après la connexion

如果使用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
Copier après la connexion

然而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
Copier après la connexion

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
Copier après la connexion

不同点在于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的类型(字符串、对象、数组)做不同的函数查找 */
}
Copier après la connexion

很显然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
Copier après la connexion

从生成的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
Copier après la connexion

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

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

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

总结


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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
php
source:juejin.im
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!