Схематично, простыми словами о внутреннем устройстве PHP (Zend Engine, OPCache, JIT)

Простыми словами о внутреннем устройстве PHP

Простыми словами о внутреннем устройстве PHP

Введение

Данный пост нацелен на неопытных PHP-специалистов. От этой информации лучше программировать вы не станете. Ожидаемая польза:

  • Мне когнитивно и морально легче, когда уменьшается «магия» с тем с чем работаешь. Может тебе тоже

  • Возможно чуть-чуть реже статьи на хабре будут тебя отпугивать

Объясню на 4 примерах — каждый лишь немного сложнее предыдущего.

Пример 1: запуск программы, написанном на компилируемом языке

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

PHP — интерпретируемый.

Тем не менее рассмотрим в первом примере работу приложения написанного на компилируемом языке.

Если язык компилируемый — это означает, что написанная программа может компилироваться сразу в машинный код, который может непосредственно выполнен процессором компьютера.

Вот простая схема — работа Go программы (двоичный код приведен выдуманный, лишь для иллюстрации):

Пример 1: Go

Пример 1: Go

Сначала у нас «исходный код» Go — файл hello-world.go:

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

Процессор самостоятельно такой код запустить не может — прежде нам его нужно преобразовать в машинный.

Процесс №0 — Компиляция в машинный код

Номер ноль, потому что запускать скрипт будем лишь в следующем действии. Но без текущего никак.

Чтобы получить машинный код программы запускаем компилятор Go выполнив команду:

go build hello-world.go

Компилятор — это программа-преобразователь из одного языка программирования в другой. На практике почти всегда, из языка более высокого уровня (т.е. обычно ближе к пониманию человеком) в низко-уровненный (т.е. ближе к пониманию компьютером).

В результате компиляции в той же папке появляется файл hello-world, в нем двоичный код (нули и единицы) — это и есть машинный код.

В машинном коде уже собраны команды и данные для выполнения на конкретном процессоре. Так, например, машинный код одной и той же программы для x86 и ARM будут отличаться.

Процесс №1 — Выполнение машинного кода

Готовый машинный код уже можно выполнять непосредственно. Вводим команду:

./hello-world

Выходит, что для запуска нашего скрипта после единоразовой компиляции достаточно лишь одного действия — запуска машинного кода.

Пример 2: Запуск скрипта PHP без OPCache и JIT (т.е. работа PHP до версии 5.5)

Вернемся к PHP — в интерпретируемых языках подразумевается, что выполнение программы будет осуществляться не сразу машинного кода. В случае PHP — запускается именно исходной код.

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

Вот схематично представил всю последовательность работы PHP скрипта без включенных OPCache и JIT (каждый из них по отдельности рассмотрим в следующих двух примерах).

Пример 2: PHP (no OpCache & no JIT)

Пример 2: PHP (no OpCache & no JIT)

У нас привычный «исходный код» PHP (файл hello-world.php):

Опять идем по порядку, рассмотрим какие процессы происходят запустив команду:

php hello-world.php

Процесс №1 — Компиляция в байт-код
Сначала исходный код обрабатывается Zend Compiler — это PHP компилятор. Первый из двух основных компонентов Zend Virtual Machine.

В отличие от рассмотренного выше компилятора Go:

  • задача PHP компилятора — преобразовать исходный код не в машинный код, а в код-посредник — байт-код;

  • процесс компиляции происходит при каждом запуске программы (вместо лишь единоразового — до запуска программы, как в примере с Go)

Подробнее о процессе компиляции PHP можно почитать в посте на хабре.

В случае PHP этот байт-код назвали PHP OPCode.

Байт-код — более низкоуровненный чем исходный код, содержащий набор команд для интерпретатора (об интерпретаторе в следующем пункте). Байт-код не может выполняться процессором напрямую.

Чтобы посмотреть результат работы компилятора — сам байт-код — выполним команду (подробная статья о получении PHP байткода):

php -d opcache.opt_debug_level=0x20000 -d opcache.enable_cli=1 hello-world.php

Получим:

$_main:
; (lines=3, args=0, vars=0, tmps=1)
; (after optimizer)
; /hello-world.php:1-2
0000 EXT_STMT
0001 ECHO string("Hello World")
0002 RETURN int(1)
  • Вначале видим $_main: — обозначает, что следующие строки относятся к функции main. Появление такой функции в байт-коде для глобальной области видимости PHP — занятная историческая особенность, дошедшая из других языков;

  • Следующие 3 строки начинаются на ; — так обозначаются комментарии. Одна из целей — для дебаг-информации;

  • Последние 3 строки — непосредственно код нашего приложения, который будет выполняться виртуальной машиной в следующем шаге.

Процесс №2 — Выполнение байт-кода

Выполняет Zend Executor. Это — PHP интерпретатор, второй из двух основных компонентов Zend Virtual Machine.

Он преобразовывает байт-код в машинный код.

Процесс №3 — Выполнение машинного кода

Интерпретатор порциями передает на выполнение процессору машинный код. Получаем заветное Hello world.

Общая картина

Конечно же будет разница в худшую сторону в производительности между этим подходом и подходом из предыдущего примера на Go, но есть и плюсы — и то, и другое разбираться в этом посте не будет.

Уже на этом примере видим, что каждый запуск PHP-скрипта сопряжен с использованием виртуальной машины Zend Virtual Machine (или Zend Engine). Он отвечает за процессы №2 и №3 в схеме (компиляция байткода и выполнение байткода). Подробнее: по ссылке.

Пример 3: Запуск скрипта PHP с OPCache, но без JIT (т.е. работа PHP 5.5 — 7.4)

Для повышения производительности PHP-ребята позаботились вот о чем:

Чтобы при каждом запуске скрипта для всех участков кода снова и снова заново не компилировать исходный код в байт-код — скомпилированный байт-код единожды помещается в отдельный кэш — OPCache (он же — OPCode Cache). Все участки кода наших программ хранить в OPCache по разным причинам, к сожалению, не удастся.

Расширение включено по дефолту начиная с PHP 5.5, но можно установить и на более ранние версии.

Пример 3: PHP 5.5 (OpCache)

Пример 3: PHP 5.5 (OpCache)

Сравним с предыдущей схемой:

  • Появились 2 дополнительных действия (посмотреть в кэш и записать в кэш)

  • Появилась развилка сразу после первого действия

  • Видно, что такой подход позволяет нам проскочить этап компиляции для конкретных участков кода, которые уже были в кэше.

Конечно, подобное кэширование под капотом реализуется совсем непросто — спасибо ребятам большое.

Очень подробно о расширении в статье на хабре.

Пример 4: Запуск скрипта PHP 8

Наконец добрались до свежих версий PHP!

Если всмотреться в логику предыдущей схемы — может возникнуть вопрос:, а почему участки исходного кода закешировать не в байт-код, а сразу в машинный код? Именно этим и занимается JIT.

Неочевидная особенность: JIT — это надстройка над OPCache. Т.е. без включенного OPCache (по дефолту включен) JIT работать не будет. Это можно понять из представленной последней схемы:

Пример 4: PHP 8 (OpCache & JIT)

Пример 4: PHP 8 (OpCache & JIT)

Рассмотрим самый оптимистичный путь выполнения нашего скрипта, когда участок исходного кода обнаружился сначала в OpCode Cache, а затем и в буфере JIT (т.е. самый левый путь на схеме) — мы приблизились к принципу запуска скрипта из самого первого примера на Go. Другими словами на момент запуска участка кода — машинный код для него уже был скомпилирован.

Может показаться, что это что-то вроде панацеи — мы приблизились к производительности компилируемых языков —, но это не так.
Есть эффективный предел объема JIT буфера — поэтому выполняется профилирование (т.е. анализ выполнения) работы опкодов (на схеме действие №5) — виртуальная машина Zend принимает решение, имеет ли смысл хранить этот машинный код в буфере.

Подробнее о JIT на php.watch

Заключение

Поздравляю, ты дошел до конца! Надеюсь, теперь процесс исполнения PHP стал для тебя более понятным, и следующие статьи для прочтения на эту тему не будут казаться такими запутанными.

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

Был бы рад увидеть кого-нибудь в моем маленьком PHP телеграмм-блоге

© Habrahabr.ru