>  기사  >  백엔드 개발  >  PHP 구문 설탕에서 Zend VM 엔진 분석

PHP 구문 설탕에서 Zend VM 엔진 분석

巴扎黑
巴扎黑원래의
2016-11-21 09:46:141089검색

## 1.

먼저 PHP5.3+의 설탕 구문에 대해 이야기해 보겠습니다. 일반적으로 다음과 같이 작성합니다.

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

실행 결과 $b = 1, 나중에 작성하는 것이 더 간결하지만 일반적으로 너무 많은 구문을 사용하지 않는 것이 좋습니다. 설탕, 특히 다음과 같이 PHP 7에 새로 추가된 것처럼 이해하고 혼동하기 쉬운 경우:

$b = $a ?? 1;
다음과 동일:

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

이 글의 목적은 Syntactic Sugar로 시작하여 Zend VM의 구문 분석 원리에 대해 이야기하는 것입니다.

## 2.

분석된 PHP 소스 브랜치 => Remotes/origin/PHP-5.6.14 vld를 통해 opcode를 보는 방법은 이전에 작성한 이 기사를 참조하세요. :


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

의 해당 opcdoe는 다음과 같습니다:

ops 수: 5
컴파일된 vars: !0 = $a, !1 = $b
line fetch ext return Operands
--------------------- - -------------
2 0 E > ASSIGN !0 , 0
3 1 JMP_SET_VAR $1 !0
2 QM_ASSIGN_ VAR                                                                                                         !1, $1
4 4 > 라인: 2- 4; out1; -2
경로 #1: 0,

vim Zend/zend_언어_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) }
~~~

원한다면 직접 ?: 의 구문 설탕을 재정의할 수도 있습니다. BNF 문법 규칙을 따르고 들소 분석을 사용하십시오. 관심이 있으시면 Google에서 관련 지식을 검색하고 계속해서 자세히 알아볼 수 있습니다.

vld의 opcode에서 zend_do_jmp_set_else가 실행되었음을 알 수 있습니다. 코드는 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-> ;결과, 콜론_토큰 ; › 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->result_type = IS_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(결과, 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.

두 가지 주요 opcode는 ZEND_JMP_SET_VAR 및 ZEND_QM_ASSIGN_VAR입니다. 코드를 계속 읽는 방법은 무엇입니까? PHP의 opcode에 대해 이야기해 봅시다.

PHP5.6에는 167개의 opcode가 있습니다. 즉, 여기에서 공식 문서를 참조하세요 🎜>
PHP는 내부적으로 _zend_op 구조를 사용하여 opcode를 나타냅니다. vim Zend/zend_compile.h +111

111 struct _zend_op {
112 › opcode_handler_t handler
113 › znode_op op1; 🎜> 114 › znode_op op2; 115 › znode_op 결과;
116 › zend_uchar opcode;
119 › zend_uchar op2_type; char 결과_유형;
122 }

PHP 7.0은 약간 다릅니다. 64비트 시스템에서는 uint가 uint32_t로 바뀌고 바이트가 명시적으로 지정된다는 점입니다.

opcode를 두 개의 피연산자(op1, op2)만 받아들이고 연산(덧셈, 뺄셈, 곱셈, 나눗셈과 같은 핸들러)을 수행한 다음 결과(result)를 반환하는 계산기라고 생각하시면 됩니다. ), 그리고 산술 오버플로(extended_value)를 약간 처리합니다.

Zend의 VM은 처리 함수의 주소를 가리키는 핸들러(함수 포인터)를 사용하여 각 opcode에 대해 정확히 동일한 방식으로 작동합니다. op1, op2를 매개변수로 사용하여 opcode 실행에 해당하는 코드를 포함하는 C 함수입니다. 실행이 완료된 후 결과(result)가 반환되고, 때로는 정보(extended_value)가 추가되기도 합니다. . 4944 › USE_OPLINE
4945 › zend_free_op free_op1;
4946 › zval *value, *ret;
4947
4948 › S AVE_OPLINE()
4949 › value = GET_OP1_ZVAL _PTR ( BP_VAR_R);
4950
4951 › if (i_zend_is_true(값)) {
4952 › › if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) {
    4953 ›   ›   ›   Z_ADDREF_P(값); 
    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("%dn에 대한 조건부 jmp", 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脚本解析`zend_vm_def.h`后生成(유의미思吧,先有鸡还是先有蛋,没有PHP 哪来的这个脚本?),猜测这个是后期产物,早期php版本应该不会用这个。 

상면 ZEND_JMP_SET_VAR적 대표성, 根据不同参数 `CONST|TMP|VAR|CV` 最终会生成不同类型的, 但功能一致적 핸들러函数: 

    정적 정수 ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    정적 정수 ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    정적 정수 ZEND_FASTCALL  ZEND_JMP_SET_VAR_SP EC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 
    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 

期确정 핸들러는 提升运行期性能.이 없습니다.些垃圾代码(看似无用),不用担心,C的编译器会进一步优化处理. 

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

## 4.

저희가 알고 있는 opcode와 처리기의 기능을 알려주세요.么串联起来的呢? 

语链表可能更准确代) ,从上面码我们可以>看到,每个handler执行完后,city会调用 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) { 
>   ›   ›   스위치(ret) {
옥션   ›   ›   ›   사례 1: 
**   ›   ›   ›   ›   EG(in_execution) = original_in_execution; 
옥션   ›   ›   ›   ›   반품; 
옥션   ›   ›   ›   사례 2: 
옥션 2: 
옥션   ›   ›   ›   ›   goto zend_vm_enter; 
옥션   ›   ›   ›   ›   휴식; 
옥션   ›   ›   ›   사례 3: 
옥션 3: 
옥션   ›   ›   ›   ›   Execute_data = EG(current_execute_data); 
옥션   ›   ›   ›   ›   휴식; 
옥션   ›   ›   ›   기본값: 
옥션   ›   ›   ›   ›   휴식; 
옥션   ›   ›   } 
옥션   ›   } 

옥션   } 
>   zend_error_noreturn(E_ERROR, "발생해서는 안되는 메인 루프의 끝에 도착했습니다."); 

~~~ 

宏定义, vim Zend/zend_execute.c +1772 

    1772 #define ZEND_VM_NEXT_OPCODE()  
    1773 ›   CHECK_SYMBOL_TABLES() 🎜> 1774 ›   ZEND_VM_INC_OPCODE();  
    1775 신문 331 #define ZEND_VM_ENTER()            2 반환 
    332 #define ZEND_VM_LEAVE()            3을 반환

동안 是一个死循环,执行一个handler函数,除个别情况,多数handler函数末尾TU调用ZEND_VM_NEXT_OPCODE() -> ; ZEND_VM_CONTINUE(), 0 반환, 继续循环。 

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

希望你看完상면内容,对PHP Zend 引擎的解析过程有个详细的了解,下面我们基于原理的分析,再简单聊聊PHP的优化。 

## 5. PHP优化注의사项 

### 5.1 echo 输流 

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

vld 查看opcode: 

    작업 수:  5 
    컴파일된 변수:  !0 = $foo, !1 = $bar 
    라인     #* E I O op                        가져오기          ext  return  피연산자
    ---------------------------------- -------------------------------------- 
       2     0  E >   할당                                               !0, 'foo' 
3                       할당                                                                                

ZEND_CONCAT은 다음의 가치를 연결합니다. $a와 $b를 임시변수 ~2에 저장하면 에코가 나옵니다. 이 프로세스에는 사용 후 해제되어야 하는 임시 변수에 대한 메모리 조각이 할당되며, 스플라이싱 프로세스를 수행하려면 스플라이싱 함수를 호출해야 합니다.

이렇게 바꾸면:

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

해당 opcode:

ops 수: 5
컴파일된 vars: !0 = $foo, !1 = $bar
line #* E I O op ext return 피연산자
--------------------------------- -- --------------
2 0 E > 할당                                                     0, 'foo'
3 1 1 할당 1, 'bar'
4 2 ECHO                                                                               !1
5 4 4; out1: -2
경로 #1: 0,

메모리를 할당하거나 스플라이싱 기능을 실행할 필요가 없어 더 효율적이지 않나요? 스플라이싱 프로세스를 이해하려면 이 기사의 내용에 따라 ZEND_CONCAT opcode에 해당하는 핸들러를 검색하면 됩니다.

### 5.2 define() 및 const

const 키워드는 5.3부터 도입되었습니다. 이는 C 언어의 `#define`과 유사한 의미를 갖습니다.

* 정의()는 함수 호출이며 함수 호출 오버헤드가 있습니다.
* const는 opcode를 직접 생성하는 키워드로 컴파일 중에 결정될 수 있으며 실행 중에 동적으로 할당할 필요가 없습니다.

const의 값은 죽었고 런타임 중에 변경할 수 없으므로 컴파일 중에 결정되고 숫자 유형에 제한이 있는 C 언어의 #define과 유사합니다.

코드를 직접 보고 opcode를 비교하세요:

예제 정의:

정의('FOO', 'foo')
echo FOO;

opcode 정의:

ops 수: 6
컴파일된 vars: none
줄 반환 피연산자
------ --- --------------------- --- --------------------------
2 0 E > SEND_VAL 'foo'
2 DO_FCALL 2                                                                         > ; RETURN 1

const 예:

const FOO = 'foo'
        에코 FOO; 

const opcode: 

    연산 수:  4 
    컴파일된 변수:  없음 
    line     #* 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      > 반환                                              1 

### 5.3 动态函数的代价 

            function foo() { } 
        foo(); 

对应opcode: 

    작업 수:  3 
    컴파일된 변수:  없음 
    line     #* E I O op                        가져오기         반환  피연산자 
    -------- ------------------------------------- --------------------------- 
       2     0  E >   NOP 
       3     1        DO_FCALL                                 0          'foo' 
       4     2      > 반환                                              1 

动态调用代码: 

            함수 foo( ) { } 
        $a = 'foo'; 
        $a(); 

opcode: 

    작업 수:  5 
    컴파일된 변수:  !0 = $a 
    line     #* E I O op                       가져오기          ext  return  피연산자 
    ----- ------------------------------------- ----------------- 
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 INIT_FCALL_BY_NAME의 기능을 살펴보세요. 코드가 너무 길어서 여기에 나열되지 않습니다. 동적 기능은 편리하지만 성능이 저하되는 것이 분명하므로 사용하기 전에 장단점의 균형을 잘 맞춰야 합니다.

### 5.4 클래스 선언 지연 비용

먼저 코드를 살펴보겠습니다.

class Bar { }
class Foo 확장 Bar { }

해당 opcode:

ops 수: 4
컴파일된 vars: none
line #* E I O op fetch ext return Operands
-- --- ---------------------------------- --- ----------------------------------
2 0 E > NOP
3 1 1 NOP
2 NOP
4 3 > RETURN 1
class Foo 확장 Bar { }
class Bar { }

해당 opcode:

개수 ops: 4
컴파일된 vars: none
line #* E I O op fetch ext return 피연산자
------------- ------------ ------------------------- ------------ -----------
2 0 E > FETCH_CLASS 0:0 '바'
1 DECLARE_INHERITED_CLASS '%00foo%2FUusers%2Fqisen%2Ftmp%2Fvld.php0x103d58020', 'foo'
3 2 NOP
4 3 > RETURN 컴파일 오류가 발생하지만 PHP와 같은 동적 언어는 클래스 선언을 다음까지 연기합니다. 주의를 기울이지 않으면 이 함정에 빠질 가능성이 높습니다.

그러므로 Zend VM의 원리를 이해한 후에는 덜 동적인 기능을 사용하는 데 더 주의를 기울여야 합니다.

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.