## 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의 원리를 이해한 후에는 덜 동적인 기능을 사용하는 데 더 주의를 기울여야 합니다.