Heim >Backend-Entwicklung >PHP-Tutorial >Analyse der Zend VM-Engine aus PHP-Syntaxzucker

Analyse der Zend VM-Engine aus PHP-Syntaxzucker

巴扎黑
巴扎黑Original
2016-11-21 09:46:141148Durchsuche

## 1.

Lass uns zuerst über den Syntaxzucker von PHP5.3 sprechen. Normalerweise schreiben wir es so:

$a = 0;
$ b = $a ? $a : 1;

Syntax Zucker kann so geschrieben werden:

                                                                                                                                                             b = $a ? 1; ist $b = 1. Die folgende Schreibmethode ist prägnanter, es wird jedoch normalerweise nicht empfohlen, zu viel Syntaxzucker zu verwenden, insbesondere wenn sie leicht zu verstehen und zu verwirren ist. Beispielsweise wurde PHP 7 neu hinzugefügt?? 🎜>
$b = $a ?? 1;

Entspricht:

$b = isset($a ) ? $a : 1;

?: Sind Sie leicht verwirrt?

Syntax Sugar steht nicht im Mittelpunkt dieses Artikels. Unser Ziel ist es, mit Syntax Sugar zu beginnen und über das Parsing-Prinzip von Zend VM zu sprechen.

## 2.

Analysierter PHP-Quellcodezweig => remotes/origin/PHP-5.6.14 Informationen zum Anzeigen von Opcode über vld finden Sie in diesem Artikel, den ich zuvor geschrieben habe:


$a = 0;
$b = $a?: 1; Der entsprechende opcdoe von

lautet wie folgt:

Anzahl der Operationen: 5
kompilierte Variablen: !0 = $a, !1 = $b
Line Fetch Ext Return Operanden
-------------------------------- --------------------------------------------------
2 0 E > ASSIGN !0, 0
3 1 JMP_SET_VAR $1 !0
2 QM_ASSIGN_ VAR                                                                                                    !1, $1
4 4 > RETURN-Zweig: # 0; Zeile: 2- 4 ; sop: 0; eop: 4; out1: -2
Pfad #1: 0,

vim Zend/zend_lingual_parser.y 834

~~~.bash
834 › |› expr '?' ':' { zend_do_jmp_set(&$1, &$2, &$3 TSRMLS_CC); }
835 › › expr { zend_do_jmp_set_else(&$$, &$5, &$2, & $3 TSRMLS_CC); }
~~~

Wenn Sie möchten, können Sie es selbst tun und den syntaktischen Zucker von ?: neu definieren. Befolgen Sie die BNF-Grammatikregeln und verwenden Sie die Bison-Analyse. Wenn Sie interessiert sind, können Sie das relevante Wissen googeln und weiter lernen.

Aus dem Opcode von vld können wir erkennen, dass zend_do_jmp_set_else ausgeführt wird. Der Code ist in Zend/zend_compile.c:

~~~.java
void zend_do_jmp_set_else(znode *result , const znode * false_value, const znode *jmp_token, const znode *colon_token TSRMLS_DC)
{
› zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

› SET_NODE(opline-> ;result, colon_token );
› if (colon_token->op_type == IS_TMP_VAR) {
› › if (false_value->op_type == IS_VAR || false_value->op_type == IS_CV) {
› › CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].opcode = ZEND_JMP_SET_VAR;
› › CG(active_op_array)->opcodes[jmp_token->u.op .opline_num] .result_type = IS_VAR; › › opline->opcode = ZEND_QM_ASSIGN_VAR; › › opline->opcode = ZEND_QM_ASSIGN; } }
› } else {
› › opline->opcode = ZEND_QM_ASSIGN_VAR; 🎜 >› SET_NODE(opline->op1, false_value);
› SET_UNUSED(opline->op2); 🎜>
› GET_NODE(result, opline->result);

› CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].op2.opline_num = get_next_op_number( CG(active_op_array));

› DEC_BPC(CG(active_op_array));
}
~~~

## 3.

Die beiden Schlüssel Opcodes sind ZEND_JMP_SET_VAR und ZEND_QM_ASSIGN_VAR. Wie lese ich den Code weiter? Lassen Sie uns über den Opcode von PHP sprechen.

PHP5.6 verfügt über 167 Opcodes, was bedeutet, dass es 167 verschiedene Berechnungsoperationen ausführen kann. Siehe die offizielle Dokumentation hier 🎜>
PHP verwendet intern die _zend_op-Struktur zur Darstellung von Opcode, vim Zend/zend_compile.h 111

111 struct _zend_op {
112 › opcode_handler_t handler;
113 › z node_op op1;
114 › znode_op2;
116 › ulong advanced_value;
120 › zend_uchar op2_type;
121 › zend_uchar result_type;
122 }

PHP 7.0 ist etwas anders. Der Hauptunterschied besteht darin, dass uint für 64-Bit-Systeme durch uint32_t ersetzt wird und die Anzahl der Bytes klar angegeben ist.

Sie stellen sich Opcode als einen Taschenrechner vor, der nur zwei Operanden (op1, op2) akzeptiert, eine Operation ausführt (Handler wie Addition, Subtraktion, Multiplikation und Division) und dann ein Ergebnis zurückgibt (result ) für Sie, und dann ein wenig Umgang mit dem arithmetischen Überlauf (extended_value).

Zends VM funktioniert für jeden Opcode genauso, wobei ein Handler (Funktionszeiger) auf die Adresse der Verarbeitungsfunktion zeigt. Dies ist eine C-Funktion, die den Code enthält, der der Ausführung von Opcode entspricht und op1 und op2 als Parameter verwendet. Nach Abschluss der Ausführung wird ein Ergebnis (result) zurückgegeben und manchmal eine Information (extended_value) angehängt .

Verwenden Sie den Operanden ZEND_JMP_SET_VAR in unserem Beispiel zur Veranschaulichung, vim Zend/zend_vm_def.h 4995

4942 ZEND_VM_HANDLER(158, ZEND_JMP_SET_VAR, CONST|TMP|VAR|CV, ANY)
494 3 {
4944 ›Use_opline
4945› Zend_Free_op Free_op1; BP_VAR_R );
4950
4951 › if (i_zend_is_true(value)) {
4952 › › if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) {
    4953 ›   ›   ›   Z_ADDREF_P(value); 
    4954 ›   ›   ›   EX_T(opline->result.var).var.ptr = value; 
    4955 ›   ›   ›   EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr; 
    4956 ›   ›   } else { 
    4957 ›   ›   ›   ALLOC_ZVAL(ret); 
    4958 ›   ›   ›   INIT_PZVAL_COPY(ret, value); 
    4959 ›   ›   ›   EX_T(opline->result.var).var.ptr = ret; 
    4960 ›   ›   ›   EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr; 
    4961 ›   ›   ›   if (!IS_OP1_TMP_FREE()) { 
    4962 ›   ›   ›   ›   zval_copy_ctor(EX_T(opline->result.var).var.ptr); 
    4963 ›   ›   ›   } 
    4964 ›   ›   } 
    4965 ›   ›   FREE_OP1_IF_VAR(); 
    4966 #if DEBUG_ZEND>=2 
    4967 ›   ›   printf("Conditional jmp to %dn", opline->op2.opline_num); 
    4968 #endif 
    4969 ›   ›   ZEND_VM_JMP(opline->op2.jmp_addr); 
    4970 ›   } 
    4971 
    4972 ›   FREE_OP1(); 
    4973 ›   CHECK_EXCEPTION(); 
    4974 ›   ZEND_VM_NEXT_OPCODE(); 
    4975 } 

i_zend_is_true 来判断操作数是否为true, 所以ZEND_JMP_SET_VAR是一种条件赋值,相信大家都能看明白,下面讲重点. 

注意`zend_vm_def.h `这并不是一个可以直接编译的C的头文件, 只能说是一个模板, 具体可编译的头为`zend_vm_execute.h`( 45000 多行哦), 它并非手动生成, 而是由`zend_vm_gen.php`这个PHP-Version`zend_vm_def.h`后生成(有意思吧,先有鸡还是先有蛋,没有PHP 哪来的这个脚本?), 猜测这个是后期产物, 早期php版本应该不会用这个. 

Zend_JMP_SET_VAR ist eine Datei mit der Bezeichnung „CONST|TMP|VAR|CV“.能一致的handler函数: 

    static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    static int ZEND_FASTCALL . ZEND_JMP_SET_VAR_SPEC_V AR_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 

这么做的目的是为了在编译期确定handler, 提升运行期的性能C的编译器会进一步优化处理. 

zend_vm_gen.php 也可以接受一些参数,细节在PHP源码中的README文件. `Z end/README.ZEND_VM` 有详细说明. 

## 4. 

讲到这里,我们知道opcode怎么和handler对应了。但是在整体上还有一个过程,就是语法解析,解析后所有的opcode是怎么串联起来的呢? 

语法解析的细节就不说了, Wenn Sie ZEND_VM_NEXT_OPCODE( ),取出下一个opcode,继续执行,直到最后退出,循环的代码 vim Zend/zend_vm_execute.h 337: 

~~~.java 
ZEND_API voidexecute_ ex(zend_execute_data *execute_data TSRMLS_DC) 

›   DCL_OPLINE 
›   zend_bool original_in_execution; 



›   original_in_execution = EG(in_execution); 
›   EG(in_execution) = 1; 

›   if (0) { 
zend_vm_enter: 
›   ›  execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC); 
›   } 

›   LOAD_REGS(); 
›   LOAD_OPLINE(); 

›   while (1) { 
    ›   int ret; 
#ifdef ZEND_WIN32 
›   ›   if (EG(timed_out)) { 
›   ›   ›   zend_timeout(0); 
›   ›   } 
#endif 

›   ›   if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) { 
›   ›   ›   switch (ret) {
›   ›   ›   ›   Fall 1: 
›   ›   ›   ›   ›   EG(in_execution) = original_in_execution; 
›   ›   ›   ›   ›   zurück; 
›   ›   ›   ›   Fall 2: 
›   ›   ›   ›   ›   goto zend_vm_enter; 
›   ›   ›   ›   ›   Pause; 
›   ›   ›   ›   Fall 3: 
›   ›   ›   ›   ›  execute_data = EG(current_execute_data); 
›   ›   ›   ›   ›   Pause; 
›   ›   ›   ›   Standard: 
›   ›   ›   ›   ›   Pause; 
›   ›   ›   } 
›   ›   } 

›   } 
›   zend_error_noreturn(E_ERROR, "Am Ende der Hauptschleife angekommen, was nicht passieren sollte"); 

~~~ 

宏定义, vim Zend/zend_execute.c 1772 

    1772 #define ZEND_VM_NEXT_OPCODE()  
    1773 ›   CHECK_SYMBOL_TABLES()  
    1774 ›   ZEND_VM_INC_OPCODE();  
    1775 ›   ZEND_VM_CONTINUE() 

    329 #define ZEND_VM_CONTINUE()         return 0 
    330 #define ZEND_VM_RETURN()           return 1 
    331 # define ZEND_VM_ENTER()            return 2 
    332 #define ZEND_VM_LEAVE()            Return 3 VM_NEXT_OPCODE() -> ZEND_VM_CONTINUE(),return 0,继续循环。 

> 注:比如 yield 协程是个例外,它会返回1,直接return出循环.以后有机会我们再单独对yield做分析。 

Eine von PHP Zend bereitgestellte Software有个详的了解,下面我们基于原理的分析,再简单聊聊PHP的优化. 

## 5. PHP优化意事项 

### 5.1 echo 输出 

            $foo = 'foo'; 
        $bar = 'bar'; 
        echo $foo . $bar; 

vld 查看opcode: 

    Anzahl der Operationen:  5 
    kompilierte Variablen:  !0 = $foo, !1 = $bar 
    Zeile     #* E I O.                        fetch          ext return Operanden
    ------------------ -------------------------------------- 
       2     0  E >   ASSIGN                                                  !0, 'foo' 
3                        ZUWEISEN                                                                                                                                  

ZEND_CONCAT verbindet die Werte von $a und $b und speichert es in der temporären Variablen ~2. Dann kommt das Echo heraus. Bei diesem Vorgang wird temporären Variablen ein Teil des Speichers zugewiesen, der nach der Verwendung freigegeben werden muss. Außerdem muss die Spleißfunktion aufgerufen werden, um den Spleißvorgang durchzuführen.

Wenn Sie es wie folgt ändern:

$foo = 'foo';
$bar = 'bar'
echo $foo, $ bar;

entsprechender Opcode:

Anzahl der Operationen: 5
kompilierte Variablen: !0 = $foo, !1 = $bar
Zeile #* E I O op ext return Operanden
----------------- -- ----------------------------------------
2 0 E > ASSIGN                                                     0, 'foo'
3 1 1 ASSIGN 1, 'bar'
4 2. ECHO                                                                              !1
5 4 4; out1: -2
Pfad #1: 0,

Es besteht keine Notwendigkeit, Speicher zuzuweisen oder die Spleißfunktion auszuführen. Wenn Sie den Spleißprozess verstehen möchten, können Sie anhand des Inhalts dieses Artikels nach dem Handler suchen, der dem ZEND_CONCAT-Opcode entspricht.

### 5.2 define() und const

Das Schlüsselwort const wurde ab 5.3 eingeführt. Es unterscheidet sich stark von define. Es hat eine ähnliche Bedeutung wie „#define“ in der C-Sprache.

* define() ist ein Funktionsaufruf und verursacht einen Funktionsaufruf-Overhead.
* const ist ein Schlüsselwort, das direkt Opcode generiert, der während der Kompilierung bestimmt werden kann und während der Ausführung nicht dynamisch zugewiesen werden muss. Der Wert von

const ist tot und kann zur Laufzeit nicht geändert werden. Daher ähnelt er #define in der C-Sprache, das während der Kompilierung bestimmt wird und Einschränkungen hinsichtlich numerischer Typen aufweist.

Schauen Sie sich den Code direkt an und vergleichen Sie den Opcode:

define example:

define('FOO', 'foo'); 🎜> echo FOO;

Opcode definieren:

Anzahl der Operationen: 6
kompilierte Variablen: keine
Zeilenrückgabeoperanden
------ --- ----------------------------------------------- --- ---------
2 0 E > SEND_VAL 1 SEND_VAL 'foo'
2 DO_FCALL 2                                                                                                                          
4 5 > RETURN 1

const Beispiel:

const FOO = 'foo'
        echo FOO; 

const opcode: 

    Anzahl der Operationen:  4 
    kompilierte Variablen:  keine 
    Zeile     #* E I O op                         ext  return s 
    -------- -------------------------------------------------- -------------------------- 
       2     0  E >   Declare_Const 'foo', 'foo'
3 1 fetch_constant ~ 0 'foo'
2 echo ~ 0
4 3 & gt; RÜCKKEHR                                             1 

### 5.3 动态函数的代价 

   .         function foo() { } 
        foo(); 

对应opcode: 

    Anzahl der Operationen:  3 
    kompilierte Variablen:  keine 
    Zeile     #* E I O op                         fetch         . ext Rückgabeoperanden 
    -------- -------------------------------------------------- -------------------------- 
       2     0  E >   NOP 
       3     1        DO_FCALL                                     0          'foo' 
       4     2      > RÜCKKEHR                                              function foo() { } 
        $a = 'foo'; 
        $a(); 

opcode: 

    Anzahl der Operationen:  5 
    kompilierte Variablen:  !0 = $a 
    Zeile     #* E I O op                        ext   Operanden 
    ----- -------------------------------------------------- --------------- 
2 0 E > NOP
3 1 ASSIGN !0, 'foo'
4 2 INIT_FCALL_BY_NAME DO_FCALL_BY_NAME !0
3 DO_FCALL_BY_NAME 0
5 4 > RETURN /zend_vm_def.h 2630 bei was INIT_FCALL_BY_NAME tut Der Code ist zu lang und wird hier nicht aufgeführt. Obwohl dynamische Funktionen praktisch sind, beeinträchtigen sie auf jeden Fall die Leistung. Daher müssen Sie die Vor- und Nachteile abwägen, bevor Sie sie verwenden.

### 5.4 Die Kosten einer verzögerten Klassendeklaration

Schauen wir uns zuerst den Code an:

class Bar { }
Klasse Foo erweitert Bar { }

entsprechender Opcode:

Anzahl der Operationen: 4
kompilierte Variablen: keine
Zeile #* E I O op fetch ext return operands
-- --- -------------------------------- --- ---------------------------------
2 0 E > 1 1 NOP
2 NOP
4 3 > RETURN 1
Klasse Foo erweitert Bar { }
Klasse Bar { }

Entsprechender Opcode:

Anzahl von ops: 4
kompilierte Variablen: keine
Zeile #* E I O op fetch ext return operands
------------- ------------ -------------------------------------- ------------ -----------
2 0 E > FETCH_CLASS 0 :0 'Bar'
'

Stellungnahme:
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