Введение
Данный пост нацелен на неопытных PHP-специалистов. От этой информации лучше программировать вы не станете. Ожидаемая польза:
Мне когнитивно и морально легче, когда уменьшается «магия» того, с чем работаешь. Может тебе тоже
Возможно чуть-чуть реже статьи на хабре будут тебя отпугивать
Объясню на 4 примерах — каждый лишь немного сложнее предыдущего.
Пример 1: Запуск программы, написанной на компилируемом языке
Пойдем с основ. Ты знаешь, что языки программирования бывают компилируемыми и интерпретируемыми (такое разделение весьма условное, и даже в рамках этого поста условность будет видна в последнем примере).
PHP — интерпретируемый.
Тем не менее рассмотрим в первом примере работу приложения написанного на компилируемом языке.
Компилируемый язык — означает, что программа написанная на этом языке, может компилироваться сразу в машинный код. Этот машинный код может быть непосредственно выполнен процессором компьютера.
Вот простая схема — работа 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 (каждый из них по отдельности рассмотрим в следующих двух примерах).
У нас привычный «исходный код» PHP (файл hello-world.php
):
<?php
echo "Hello world";
Опять идем по порядку, рассмотрим какие процессы происходят запустив команду:
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, но можно установить и на более ранние версии.
Сравним с предыдущей схемой:
Появились 2 дополнительных действия (посмотреть в кэш и записать в кэш)
Появилась развилка сразу после первого действия
Видно, что такой подход позволяет нам проскочить этап компиляции для конкретных участков кода, которые уже были в кэше.
Конечно, подобное кэширование под капотом реализуется совсем непросто — спасибо ребятам большое.
Очень подробно о расширении в статье на хабре.
Пример 4: Запуск скрипта PHP 8
Наконец добрались до свежих версий PHP!
Если всмотреться в логику предыдущей схемы — может возникнуть вопрос: а почему участки исходного кода закешировать не в байт-код, а сразу в машинный код? Именно этим и занимается JIT.
Неочевидная особенность: JIT — это надстройка над OPCache. Т.е. без включенного OPCache (по дефолту включен) JIT работать не будет. Это можно понять из представленной последней схемы:
Рассмотрим самый оптимистичный путь выполнения нашего скрипта, когда участок исходного кода обнаружился сначала в OpCode Cache, а затем и в буфере JIT (т.е. самый левый путь на схеме) — мы приблизились к принципу запуска скрипта из самого первого примера на Go. Другими словами на момент запуска участка кода — машинный код для него уже был скомпилирован.
Может показаться, что это что-то вроде панацеи — мы приблизились к производительности компилируемых языков — но это не так.
Есть эффективный предел объема JIT буфера — поэтому выполняется профилирование (т.е. анализ выполнения) работы опкодов (на схеме действие №5) — виртуальная машина Zend принимает решение, имеет ли смысл хранить этот машинный код в буфере.
Подробнее о JIT на php.watch
Заключение
Поздравляю, ты дошел до конца! Надеюсь, теперь процесс исполнения PHP стал для тебя более понятным, и следующие статьи для прочтения на эту тему не будут казаться такими запутанными.
Мне кажется этих представлений вполне достаточно, чтобы быстрее сориентироваться как работают большинство других популярных языков.
Был бы рад увидеть кого-нибудь в моем маленьком PHP телеграмм-блоге
Комментарии (12)
unreal_undead2
20.09.2024 06:33+1Подробнее о JIT на php.watch
По ссылке одна куцая картинка - может, имелось в виду это? А так спасибо - когда то возился с исходниками hhvm , где JIT уже давно, не знал, что в стандартный PHP его тоже завезли.
ionov-e Автор
20.09.2024 06:33Исправил, спасибо большое!! Именно эта ссылка и была, при смене картинок я ошибся
Viacheslav01
20.09.2024 06:33+1Лет 20 назад неплохо зарабатывал устанавливая внешние реалищации opcode cache )))
lehha
20.09.2024 06:33+1Есть ли возможность вытаскивать, хранить и запускать opcode? Например, скомпилировать скрипт заранее и распространять в таком виде? И такой же вопрос про машинный код (JIT).
В версиях 5.x было интересное расширение bcompiler, которое позволяло делать это и сохранять в php-файл и нативно запускать php-скрипт (по факту opcode), но при условии что расширение bcompiler присутствует в среде исполнения и скрипт был "скомпилирован" в такой же версии и в такой же архитектуре. Изменение любого условия приводит скрипт в нерабочее состояние.
FanatPHP
20.09.2024 06:33+1Это опять история "как бы мне скрыть свой позорный код от заказчика"?
lehha
20.09.2024 06:33Программы компилируют не для этого.
FanatPHP
20.09.2024 06:33+1Так она и так компилируется. Вы же спрашиваете не "как скомпилировать", а как отдать заказчику байткод вместо исходного.
lehha
20.09.2024 06:33+1Вопрос про распространение - это не передача заказчику, а условно размещение кода в репозитории и дальнейшее использование (внутри системы, организации или публично).
В частности, проблема с компилированием и использованием на разных архитектурах, версиях и окружении явно должна проявиться. Возможно, есть решение этого вопроса.
Компилируется код в версиях >5.5 под капотом и только в кэш, то есть не хранится на диске рядом с исходником.
FanatPHP
20.09.2024 06:33+1У вас логика вверх тормашками
проблема с компилированием и использованием на разных архитектурах, версиях и окружении явно должна проявиться
Разумеется, она и проявится, если класть в репу скомпилированный для определенной версии и архитектуры код, который, действительно не будет работать на других архитектурах :)))
А если класть исходник, то вообще никаких проблем с архитектурами не будет. Как их никогда и не было.
clerik_r
Спасибо за статью! До 7 версии ещё давно все знал, когда писал на PHP, а вот всё что после уже не следил, спасибо что собрали вcе воедино.
semendyaevanton
Примерно такая же история, а тут все хорошо расписано и собрано вместе. Автору спасибо!