PHP chạy như nào? Từ Source code đến render

Giới thiệu

Khi chúng ta chạy một đoạn code PHP, có rất nhiều thứ xảy ra sâu bên dưới mà ta không nhìn thấy. Một cách khái quát, bộ thông dịch PHP trải qua 4 giai đoạn khi nó thực thi 1 đoạn code:

  • Lexing

  • Parsing

  • BIên dịch (compilation)

  • Thông dịch (intepretation)

Bài viết này sẽ tập trung vào các giai đoạn này, biểu diễn các output trong mỗi giai đoạn để hiểu bản chất. Cần chú ý rằng một số extension được cài đặt mặc định cùng PHP ( tokenizer, OPcache, …), trong khi đó nhiều extension bắt buộc phải cài đặt và khởi động 1 cách thủ công ( php-ast và VLD ).

Giai đoạn 1: LEXING

Lexing (tokenizing) là quá trình chuyển một đoạn string (mã nguồnPHP) thành 1 chuỗi các token. Một token đơn giản là một định danh gắn với một giá trị. PHP sử dụng re2c để sinh ra các lexer của nó từ file khai báo zend_language_scanner.

Hãy xem output trong giai đoạn Lexing thông qua tokenizer extension:

$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);
    }
}

Outputs:

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) ";"

Có một vài điểm cần chú ý khi nhìn vào những dòng output trên. Đầu tiên, không phải tất cả các dòng code đều được định nghĩa (tokenize). Các kí hiệu như =, ;, :, ? vẫn được giữ nguyên. Thứ hai, lexing không đơn giản chỉ output ra một dãy các token. Trong phần lớn trường hợp, nó cũng lưu trữ các lexeme (giá trị tương ứng của mỗi định danh) và số thứ tự hàng (sẽ được ứng dụng với các stack trace).

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

Giai đoạn 2: PARSING

Bộ phân tích cú phsp (parser) được tạo ra với Bison qua file grammar BNF. PHP sử dụng một cấu trúc gọi là LALR (Look Ahead Left to Right - từ trên xuống, từ trái sang). Nó có nghĩa là bộ parser có khả năng tìm các token n (trong trường hợp này, là 1) để giải quyết các vấn đề không rõ ràng trong khi phân tích cú pháp. Phần Left-to-Right có nghĩa đơn giản là bộ parser sẽ phân tích từ trái sang phải.

 

Giai đoạn phân tích cú pháp này sẽ nhận các luồng token từ lexer như các biến đầu vào và thực hiện 2 công việc. Đầu tiên, nó sẽ xác định tính hợp lệ của các token bằng việc cố gắng so khớp chúng với từng quy tắc ngữ pháp định nghĩa trong tập tin ngữ pháp BNF của nó. Việc này sẽ làm đảm bảo cấu trúc ngôn ngữ hợp lệ theo dạng của nó trong các luồng stream. Công việc thứ hai của bộ parser là để tạo ra cây cú pháp trừu tượng (AST) - mã nguồn sẽ hiển thị dưới dạng cây, dùng trong giai đoạn kế tiếp ( biên dịch ).

Chúng ta có thể xem 1 form dạng AST tạo ra bởi bộ parser sử dụng php - ast extension. AST bên trong không thấy được 1 cách trực tiếp vì nó không phải thực sự " clean" để làm việc ( liên quan đến tính nhất quán và tính dễ sử dụng ), và vì thế php - ast extension thực hiện một vài biến đổi để làm nó trở nên dễ dàng hơn.

Dưới đây là một AST:

$code = <<<'code'
<?php
$a = 1;
code;

print_r(ast\parse_code($code, 30));

Outputs:

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 (ast/Nodes) có một số thuộc tính:

  • Kind: giá trị integer để mô tả loại nút ; mỗi cái có hằng số tương ứng

  • Flag: số nguyên xác định hành vi quá tải ( chẳng hạn như nút ast\AST_BINARY_OP sẽ có flag để phân biệt phép toán nhị phân nào màng xảy ra )

  • Lineno - số dòng

  • Childredn: nhóm nút.

 

Giai đoạn 3: COMPILING - Biên dịch

Giai đoạn biên dịch sử dụng AST, nó sẽ phát ra các mã tác vụ bằng cách duyệt cây theo phương pháp đệ quy. Giai đoạn này cũng thực hiện một vài tối ưu hoá. Những việc này bao gồm giải quyết một số lời gọi hàm với đối số tường minhtrựcư là strlen("abc") hay int(3),..

Chúng ta có thể kiểm tra đầu ra của các đoạn mã được tối ưu hóa bằng nhiều cách, với OPcache, VLD, và PHPDBG. Bài viết này sử dụng VLD, vì nó tạo ra các đoạn mã output dễ đọc hơn.

Hãy xem output trong đoạn file.php:

if (PHP_VERSION === '7.1.0-dev') { 

echo 'Yay', PHP_EOL; 

} 

Đoạn mã lệnh để thực thi:

php -dopcache.enable_cli=1 -dopcache.optimization_level=0 -dvld.active=1 -dvld.execute=0 file.php

Output:

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

Mã tác vụ sắp xếp giống mã nguồn gốc, có thể thực hiện các thao tác cơ bản.Ở đây không sử dụng phương pháp tối ưu hoá Không được áp dụng tại mức mã tác vụ trong kịch bản bên trên - nhưng như đã thấy, giai đoạn biên dịch đã gán cho hằng số điều kiện ( PHP_VERSION = = ' 7.1.0 - dev ' ) thành true.

OPcache thực hiện nhiều tác vụ hơn là chỉ lưu trữ tạm vào bộ nhớ cache mã tác vụ ( do đó bỏ qua các giai đoạn tránhg, phân tích cú pháp, và biên dịch ). Nó cũng được tối ưu hóa theo nhiều cấp độ khác nhau.

Câu lệnh:

php -dopcache.enable_cli=1 -dopcache.optimization_level=1111 -dvld.active=-1 -dvld.execute=0 file.php 

 

Output:

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 hằng số điều kiện đã bị loại bỏ, và 2 dòng ECHO đã hợp thành một lệnh. Đây chỉ là một trong nhiều cách mà OPcache thực hiện tối ưu hoá khi thực hiện chuyển qua mã tác vụ của kịch bản.

Giai đoạn 4: INTEPRETER - Thông dịch

Giai đoạn cuối là thông dịch mã tác vụ. Đây là nơi mã tác vụ đang chạy trên Zen Engine VM. Giai đoạn này hầu như rất ngắn. Đầu ra gần như tương tự với kết quả khi ta sử dụng các câu lệnh PHP như echo, var_dump,…

Thay vì đào sâu thêm vào những thứ phức tạp trong giai đoạn này, hãy cùng xem một sự thật thú vị: PHP yêu cầu chính nó như 1 dependency khi sản sinh ra chính VM của nó. Bởi vì VM được sinh ra bởi mã PHP, do đó nó trở nên đơn giản hơn để viết và dễ dàng hơn để bảo trì.

Kết luận

Như vậy thì chúng ta đã có một cách nhìn thoáng qua về 4 giai đoạn hoạt động của PHP interpreter khi nó biên dịch code PHP. Chúng ta cũng đã sử dụng các extendsions từ bên ngoài như tokenizer, php-ast, OPcache, và VLD để xem các trạng thái của nó khi chạy.

Tôi hy vọng bài viết này đã giúp cung cấp cho bạn một sự hiểu biết toàn diện hơn về PHP interpreter, cũng như tầm quan trọng của việc mở rộng OPcache (cho cả bộ nhớ đệm của nó và khả năng tối ưu hóa).

Nguồn: Sitepoint và bản dịch Đinh Thiên Phúc - học viên trung tâm Techmaster.vn

PHP vs Node.js - Cuộc chiến giữa hai công nghệ lập trình web PHP vs Node.js - Cuộc chiến giữa hai công nghệ lập trình web Hồ Sỹ Hùng Blog Home Hướng dẫn học lập trình web bằng PHP - phần 1 Hướng dẫn học lập trình web bằng PHP - phần 1 Techmaster team
Thân Văn Nghĩa

Lập trình viên PHP, Laravel Trợ giảng tại Techmaster