Статьи

Как выполняется PHP — от исходного кода до рендеринга

Эта статья была рецензирована Юнесом Рафи . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


Вдохновленная недавней статьей о том, как выполняется код Ruby , эта статья описывает процесс выполнения кода PHP.

Блок-схема векторного изображения

Вступление

Когда мы выполняем часть кода PHP, многое происходит под капотом. Вообще говоря, интерпретатор PHP проходит четыре этапа при выполнении кода:

  1. лексический
  2. анализ
  3. компиляция
  4. интерпретация

Эта статья пролистает эти этапы и покажет, как мы можем просмотреть результаты каждого этапа, чтобы реально увидеть, что происходит. Обратите внимание, что хотя некоторые из используемых расширений уже должны быть частью вашей установки PHP (например, tokenizer и OPcache), другие необходимо будет установить и включить вручную (например, php-ast и VLD).

Этап 1 — Лексинг

Лексирование (или токенизация) — это процесс превращения строки (в данном случае исходного кода PHP) в последовательность токенов. Токен — это просто именованный идентификатор значения, которому он соответствует. PHP использует re2c для генерации своего лексера из файла определения zend_language_scanner.l .

Мы можем видеть вывод этапа лексирования через расширение токенизатора :

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

Выходы:

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

Из вышеприведенного вывода есть пара примечательных моментов. Во-первых, не все фрагменты исходного кода называются токенами. Вместо этого некоторые символы считаются токенами сами по себе (например, =;:? Второй момент заключается в том, что на самом деле лексер делает немного больше, чем просто выводит поток токенов. Кроме того, в большинстве случаев хранятся лексема (значение, сопоставленное токену) и номер строки сопоставленного токена (который используется для таких вещей, как трассировка стека).

Этап 2 — Разбор

Парсер также генерируется, на этот раз с Bison через файл грамматики BNF . В PHP используется контекстно-свободная грамматика LALR (1) (смотреть вперед, слева направо). Часть просмотра вперед просто означает, что синтаксический анализатор может просмотреть n Слева направо означает, что он анализирует поток токенов слева направо.

Сгенерированный этап синтаксического анализа принимает поток токенов от лексера в качестве входных данных и имеет два задания. Во-первых, он проверяет правильность порядка токенов, пытаясь сопоставить их с любым из правил грамматики, определенных в его файле грамматики BNF. Это гарантирует, что допустимые языковые конструкции формируются токенами в потоке токенов. Вторая задача синтаксического анализатора — создать абстрактное синтаксическое дерево (AST) — древовидное представление исходного кода, которое будет использоваться на следующем этапе (компиляция).

Мы можем просмотреть форму AST, созданную анализатором, используя расширение php-ast . Внутренний AST не предоставляется напрямую, потому что он не особенно «чист» для работы (с точки зрения согласованности и общего удобства использования), и поэтому расширение php-ast выполняет несколько преобразований, чтобы сделать его более приятным для работы.

Давайте посмотрим на AST для элементарного кода:

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

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

Выход:

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

Узлы дерева (которые обычно имеют тип ast\Node

  • kind у каждого есть соответствующая константа (например, AST_STMT_LISTAST_ASSIGNAST_VAR
  • flagsast\AST_BINARY_OP
  • lineno
  • children

Выход AST на этом этапе удобен для таких инструментов, как статические анализаторы кода (например, Phan ).

Этап 3 — Компиляция

Этап компиляции использует AST, где он генерирует коды операций путем рекурсивного обхода дерева. Этот этап также выполняет несколько оптимизаций. К ним относятся разрешение некоторых вызовов функций с буквальными аргументами (например, strlen("abc")int(3)60 * 60 * 24int(86400)

На этом этапе мы можем проверить вывод кода операции несколькими способами, в том числе с помощью OPcache , VLD и PHPDBG . Я собираюсь использовать VLD для этого, так как я чувствую, что результат будет более дружелюбным.

Посмотрим, что получится для следующего скрипта file.php :

 if (PHP_VERSION === '7.1.0-dev') {
    echo 'Yay', PHP_EOL;
}

Выполнение следующей команды:

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

Наш вывод:

 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

Эти коды операций напоминают исходный код, достаточный для выполнения основных операций. (Я не буду вдаваться в подробности кодов операций в этой статье, так как это займет несколько целых статей само по себе.) На уровне кода операции в приведенном выше сценарии не было применено никаких оптимизаций — но, как мы можем видеть, фаза компиляции сделал некоторые, разрешив постоянное условие ( PHP_VERSION === '7.1.0-dev'true

OPcache делает больше, чем просто кэширует коды операций (таким образом, минуя этапы лексирования, синтаксического анализа и компиляции). Он также включает в себя множество различных уровней оптимизации. Давайте повысим уровень оптимизации до четырех проходов, чтобы посмотреть, что получится:

Команда:

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

Выход:

 line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   4     0  E >   ECHO                                                     'Yay%0A'
   7     1      > RETURN                                                   1

Мы можем видеть, что константное условие было удалено, и две инструкции ECHO Это всего лишь пример многих оптимизаций, которые OPcache применяет при выполнении операций над кодами операций сценария. Я не буду проходить различные уровни оптимизации в этой статье, поскольку это также будет статья сама по себе.

Этап 4 — Интерпретация

Заключительный этап — интерпретация кодов операций. Здесь операционные коды запускаются на виртуальной машине Zend Engine (ZE). На самом деле очень мало что можно сказать об этом этапе (по крайней мере, с точки зрения высокого уровня). Вывод в значительной степени независимо от того, что ваш PHP-скрипт выводит с помощью таких команд, как echoprintvar_dump

Таким образом, вместо того, чтобы углубляться во что-то сложное на этом этапе, вот забавный факт: PHP требует себя в качестве зависимости при создании собственной виртуальной машины. Это связано с тем, что виртуальная машина генерируется сценарием PHP, поскольку ее проще писать и поддерживать.

Вывод

Мы кратко рассмотрели четыре этапа, которые проходит интерпретатор PHP при запуске кода PHP. Это включает использование различных расширений (включая tokenizer, php-ast, OPcache и VLD) для манипулирования и просмотра выходных данных каждого этапа.

Я надеюсь, что эта статья помогла вам лучше понять интерпретатор PHP, а также продемонстрировала важность расширения OPcache (как для его возможностей кэширования, так и для оптимизации).