How PHP Executes
Bài đăng này đã không được cập nhật trong 7 năm
Mở đầu
Có rất nhiều điều xảy ra khi chúng ta thực hiện một đoạn mã PHP. Nói chung, trình thông dịch PHP đi qua bốn giai đoạn khi thực hiện 1 đoạn code:
- Lexing
- Parsing
- Compilation
- Interpretation
Chúng ta sẽ đi qua các giai đoạn này và thực hiện các ví dụ để thấy kết quả từ mỗi giai đoạn, xem điều gì đang xảy ra. Lưu ý rằng mặc dù một số phần mở rộng đã được sử dụng trong các ví dụ như là một phần của PHP (như tokenizer và OPcache) nhưng những phần khác cần phải được cài đặt và bật theo cách thủ công (như php-ast và VLD)
Bước 1 – Lexing
Lexing (hoặc tokenizing) là quá trình biến một chuỗi (trong trường hợp này mã nguồn PHP) thành một chuỗi mã thông báo liên tục (sequence of tokens). Một mã thông báo (token) chỉ đơn giản là một tên nhận dạng được đặt tên cho giá trị mà nó đã khớp. PHP sử dụng re2c
để tạo ra lexer của nó từ tập tin định nghĩa zend_language_scanner.l
.
Chúng ta có thể thấy đầu ra của giai đoạn lexing thông qua phần mở rộng tokenizer
:
$code = <<<'code'
<?php
$a = 1;
code;
$tokens = token_get_all($code);
foreach ($tokens as $token) {
if (is_array($token)) {
echo "Line {$token[2]}: ", token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
} else {
var_dump($token);
}
}
Đầu ra:
Line 1: T_OPEN_TAG ('<?php
')
Line 2: T_VARIABLE ('$a')
Line 2: T_WHITESPACE (' ')
string(1) "="
Line 2: T_WHITESPACE (' ')
Line 2: T_LNUMBER ('1')
string(1) ";"
Không phải tất cả các phần của mã nguồn được đổi thành các mã thông báo. Thay vào đó, một số biểu tượng được xem là các mã thông báo và thay bằng chính chúng (ví dụ =
, ;
, :
, ?
,...)
Bước 2 – Parsing
Bộ phân tích cú pháp cũng được tạo ra, lần này với Bison
thông qua BNF grammar file
. PHP sử dụng ngữ pháp không ngữ cảnh LALR (1) (look ahead, left-to-right). Phần look ahead
chỉ đơn giản có nghĩa là trình phân tích cú pháp có thể tìm kiếm n
tokens trước (trong trường hợp này là 1) để giải quyết sự mơ hồ mà nó có thể gặp khi phân tích cú pháp. Phần left-to-right
có nghĩa là nó phân tách dòng mã thông báo từ trái sang phải.
Giai đoạn phân tích cú pháp được tạo ra lấy mã thông báo từ lexer làm đầu vào và có hai công việc cần làm.
Trước tiên, kiểm tra tính hợp lệ của trật tự token bằng cách cố gắng so khớp chúng với bất kỳ một trong các quy tắc ngữ pháp được định nghĩa trong BNF grammar file
của nó. Điều này đảm bảo rằng cấu trúc ngôn ngữ hợp lệ được hình thành bởi các mã thông báo trong luồng mã thông báo.
Công việc thứ hai của trình phân tích cú pháp là tạo cây cú pháp trừu tượng (AST - abstract syntax tree) - một cây khung nhìn của mã nguồn,và sẽ được sử dụng trong giai đoạn tiếp theo (compilation - biên dịch)
Chúng ta có thể xem một ví dụ về AST được tạo ra bởi trình phân tích cú pháp sử dụng phần mở rộng php-ast
. Chúng ta xem AST cho một đoạn mã thô sơ:
$code = <<<'code'
<?php
$a = 1;
code;
print_r(ast\parse_code($code, 30));
Đầu ra
ast\Node Object (
[kind] => 132
[flags] => 0
[lineno] => 1
[children] => Array (
[0] => ast\Node Object (
[kind] => 517
[flags] => 0
[lineno] => 2
[children] => Array (
[var] => ast\Node Object (
[kind] => 256
[flags] => 0
[lineno] => 2
[children] => Array (
[name] => a
)
)
[expr] => 1
)
)
)
)
Các nút cây (thường là loại ast\Node
) có một số thuộc tính:
kind
Giá trị số nguyên để miêu tả loại nút, mỗi cái lại tương ứng với 1 hằng số (AST_STMT_LIST
=> 132,AST_ASSIGN
=> 517,AST_VAR
=> 256)flags
Một số nguyên quy định quá tải hành vi (overloaded behaviour) (ví dụ nốtast\AST_BINARY_OP
sẽ có flags khác nhau để phân biệtbinary operation
nào đang xảy ra)lineno
Số dòng, giống như ví dụ về token ở bên trênchildren
Các nút phụ, thường là các phần của nút được chia nhỏ hơn nữa (ví dụ nút chức năng sẽ có các con: các thông số, loại trả về, nội dung,...)
Bước 3 - Compilation
Giai đoạn biên dịch sử dụng đầu ra của AST, nơi mà nó phát ra các mã opcodes bằng cách đệ quy đi qua cây. Giai đoạn này cũng thực hiện một vài tối ưu hóa. Chúng bao gồm giải quyết một số cuộc gọi hàm với các đối số kiểu chữ (ví dụ strlen("abc")
thành int(3)
) và xếp lại biểu thức toán học liên tục (ví dụ 60 * 60 * 24
thành int(86400)
)
Chúng ta có thể kiểm tra đầu ra opcode ở giai đoạn này theo một số cách, bao gồm OPcache
, VLD
, và PHPDBG
. Ở đây chúng ta sử dụng VLD
. Ví dụ ta có file tên là file.php
if (PHP_VERSION === '7.1.0-dev') {
echo 'Yay', PHP_EOL;
}
Chạy lệnh sau:
php -dopcache.enable_cli=1 -dopcache.optimization_level=0 -dvld.active=1 -dvld.execute=0 file.php
Đầu ra:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > > JMPZ <true>, ->3
4 1 > ECHO 'Yay'
2 ECHO '%0A'
7 3 > > RETURN 1
Các opcodes sắp xếp giống với mã nguồn gốc, đủ để chạy theo với các hoạt động cơ bản. Không có tối ưu hóa nào được áp dụng ở cấp độ opcode trong kịch bản trên - nhưng như chúng ta có thể thấy, giai đoạn biên soạn đã giải quyết các điều kiện không thay đổi (constant condition) (PHP_VERSION === '7.1.0-dev'
thành true
)
OPcache làm nhiều việc hơn chỉ đơn giản là bộ nhớ đệm opcodes (do đó bỏ qua lexing, phân tích cú pháp, và giai đoạn biên dịch). Nó cũng đóng gói với nhiều mức độ tối ưu khác nhau. Bây giờ bật lên mức độ tối ưu hóa 4 lần để xem chúng ta có gì:
php -dopcache.enable_cli=1 -dopcache.optimization_level=1111 -dvld.active=-1 -dvld.execute=0 file.php
Đầu ra
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
4 0 E > ECHO 'Yay%0A'
7 1 > RETURN 1
Chúng ta có thể thấy rằng constant condition đã bị bỏ đi, và hai ECHO
đã được nén thành một. Đây chỉ là một phần nhỏ trong nhiều optimizations OPcache được áp dụng.
Bước 4 - Interpretation
Giai đoạn cuối cùng là việc biên dịch các opcodes. Đây là nơi mà các opcodes được chạy trên máy ảo Zend Engine (ZE). Có rất ít thứ để nói về giai đoạn này. Đầu ra là bất cứ thứ gì thông qua các lệnh như echo
, print
, var_dump
,... nằm trong file php của chúng ta.
Kết luận
All rights reserved