PHP

Розуміння JIT в PHP 8

Alex Alex 06 липня 2020
Розуміння JIT в PHP 8

TL;DR

Компілятор Just In Time в PHP 8 реалізований як частина розширення Opcache і покликаний компілювати операційний код в інструкції процесора в рантаймі.

Це означає, що з JIT деякі операційні коди не повинні інтерпретуватися Zend VM, такі інструкції будуть виконуватися безпосередньо як інструкції рівня процесора.

JIT в PHP 8

Однією з найбільш обговорюваних фіч PHP 8 є компілятор Just In Time (JIT). Він на слуху у багатьох блогів і спільнот — навколо нього багато шуму, але поки я знайшов не дуже багато подробиць про роботу JIT в деталях.

Після багаторазових спроб і розчарувань знайти корисну інформацію, я вирішив вивчити вихідний код PHP. Поєднуючи свої невеликі пізнання мови та всю розкидану інформацію, яку я зміг зібрати досі, я зумів підготувати цю статтю і сподіваюся, що вона допоможе вам краще зрозуміти JIT PHP.

Спрощуючи речі: коли JIT працює належним чином, ваш код не буде виконуватися через Zend VM, замість цього він буде виконуватися безпосередньо як набір інструкцій рівня процесора.

У цьому вся ідея.

Але щоб краще це зрозуміти, нам потрібно подумати про те, як php працює всередині. Це не дуже складно, але вимагає деякого роз'яснення.

Я вже писав статтю з коротким оглядом того, як працює php. Якщо вам здасться, що ця стаття стає надто складною, просто прочитайте її попередницю і повертайтеся. Це повинно трохи полегшити ситуацію.

Як виконується PHP-код?

Ми всі знаємо, що php —  мова що інтерпретується. Але що це насправді означає?


Всякий раз, коли ви хочете виконати код PHP, будь то фрагмент або цілий веб застосунок, вам доведеться пройти через інтерпретатор php. Найчастіше використовуються — PHP-FPM і інтерпретатор CLI. Їх робота дуже проста: отримати код php, інтерпретувати його і видати назад результат.

Це звичайна картина для кожної мови що інтерпретується. Деякі кроки можуть змінюватись, але загальна ідея та ж сама. В PHP це відбувається так:
  1. PHP Код читається і перетворюється на набір ключових слів, відомих як токени (Tokens). Цей процес дозволяє інтерпретатору зрозуміти, в якій частині програми написаний кожен фрагмент коду. Цей перший крок називається лексування (Lexing) або токенізація (Tokenizing).
  2. Маючи на руках токени, інтерпретатор PHP проаналізує цю колекцію токенів і постарається знайти в них сенс. В результаті абстрактне синтаксичне дерево (Abstract Syntax Tree — AST) генерується з допомогою процесу, званого синтаксичним аналізом (parsing). AST являє собою набір вузлів, що вказують, які дії повинні бути виконані. Наприклад, «echo 1 + 1» має фактично означати «вивести результат 1 + 1» або, більш реалістично, «вивести операцію, операція — 1 + 1».
  3. Маючи AST, наприклад, набагато простіше зрозуміти операції і їх пріоритет. Перетворення цього дерева в щось, що може бути виконано, вимагає проміжного представлення (Intermediate Representation IR), яке в PHP ми називаємо операційний код (Opcode). Процес перетворення AST в операційний код називається компіляцією.
  4. Тепер, коли у нас є опкоди, відбувається найцікавіше: виконання коду! В PHP є рушій під назвою Zend VM, який здатний отримувати список опкодов і виконувати їх. Після виконання всіх опкодів програма завершується.
Щоб зробити це трохи наочніше, я склав діаграму:

Розуміння JIT в PHP 8Спрощена схема процесу інтерпретації PHP.
Досить прямолінійно, як ви можете помітити. Але тут є і вузьке місце: який сенс лексуваті та парсити код кожного разу, коли ви його виконуєте, якщо ваш php-код може навіть не змінюється так часто?

Зрештою, нас цікавлять тільки опкоди, вірно? Правильно! Ось навіщо існує розширення Opcache.

Розширення Opcache

Розширення Opcache поставляється з PHP, і, як правило, немає особливих причин його вимкнути. Якщо ви використовуєте PHP, вам, ймовірно, слід включити Opcache.

Що він робить, так це додає шар оперативного загального кеша для опкодів. Його завдання полягає в тому, щоб отримувати опкоди, нещодавно згенеровані з нашого AST, і кешувати їх, щоб при подальших виконаннях можна було легко пропустити фази лексування та синтаксичного аналізу.

Ось схема того ж процесу з урахуванням розширення Opcache:Розуміємо JIT в PHP 8Потік інтерпретації PHP з Opcache. Якщо файл вже був проаналізований, php отримує для нього параметр операційний код, а не аналізує його заново.
 
Це просто заворожує, як красиво пропускаються кроки лексування, синтаксичного аналізу та компіляції.
Примітка: саме тут краще всього себе проявляє функція попереднього завантаження PHP 7.4! Це дозволяє вам сказати PHP-FPM аналізувати вашу кодову базу, перетворювати її в опкоди та кешувати їх навіть до того, як ви виконаєте.
Ви можете почати замислюватися, а куди сюди можна приліпити JIT, вірно?! Принаймні я на це сподіваюся, саме тому я і пишу цю статтю...

Що робить компілятор Just In Time?

Прослухавши пояснення Зіва в епізоді подкастів PHP і JIT від PHP Internals News, мені вдалося отримати деяке уявлення про те, що насправді повинен робити JIT...

Якщо Opcache дозволяє швидше отримувати операційний код, щоб він міг переходити безпосередньо до Zend VM, JIT призначити змусити його працювати взагалі без Zend VM.

Zend VM — це програма, написана на C, яка діє як шар між операційним кодом і самим процесором. JIT генерує скомпільований код під час виконання, тому php може пропустити Zend VM і перейти безпосередньо до процесора. Теоретично ми повинні виграти в продуктивності від цього.

Спочатку це звучало дивно, тому що для компіляції машинного коду вам потрібно написати дуже специфічну реалізацію для кожного типу архітектури. Але насправді це цілком реально.

Реалізація JIT в PHP використовує бібліотеку DynASM (Dynamic Assembler), яка створює набір інструкцій для ЦП в одному конкретному форматі і код збірки для різних ЦП. Таким чином, компілятор Just In Time перетворює операційний код в машинний код для конкретної архітектури, використовуючи DynASM.

Хоча одна думка все-таки не давала мені спокою...

Якщо попереднє завантаження здатне парсити php-код в операційний перед виконанням, а DynASM може компілювати операційний код в машинний (компіляція Just In Time), чому ми, чорт візьми, не компілюємо PHP відразу ж на місці, використовуючи Ahead of Time компіляцію?!

Одна з думок, на які мене наштовхнув один з епізодів подкасту з Зівом Сураскі, полягала в тому, що PHP слабо типізований, тобто часто PHP не знає, який тип має змінна, поки Zend VM не спробує виконати певний опкод.

Це можна зрозуміти, подивившись на тип об'єднання zend_value, який має багато покажчиків на різні представлення типів для змінної. Всякий раз, коли віртуальна машина Zend намагається отримати значення з zend_value, вона використовує макроси, подібні ZSTR_VAL, які намагаються отримати доступ до покажчика рядку з об'єднання значень.

Наприклад, цей обробник Zend VM повинен обробляти вираз «менше або дорівнює» (<=). Подивіться, як він розгалужується на безліч різних шляхів коду, щоб вгадати типи операндів.

Дублювання такої логіки виведення типів з допомогою машинного коду нездійсненно і потенційно може зробити роботу ще повільніше.

Фінальна компіляція після того, як типи були оцінені, також не є хорошим варіантом, тому що компіляція в машинний код є трудомістким завданням ЦП. Так що компіляція ВСЬОГО під час виконання — погана ідея.

Як поводитися компілятор Just In Time?

Тепер ми знаємо, що не можемо вивести типи, щоб генерувати достатньо хорошу  компіляцію на випередження. Ми також знаємо, що компіляція під час виконання коштує дорого. Чим може бути корисний JIT для PHP?

Щоб збалансувати це рівняння, JIT PHP намагається зібрати лише кілька опкодів, які, на його думку, того варті. Для цього він профілює коди операцій, що виконуються віртуальною машиною Zend, і перевіряє, які з них має сенс компілювати. (залежно від конфігурації).

Коли певний опкод компілюється, він потім делегує виконання цього скомпільованого коду замість делегування на Zend VM. Це виглядає як на діаграмі нижче:

Розуміємо JIT в PHP 8
Потік інтерпретації PHP з JIT. Якщо вони вже скомпільовані, опкоди не виконуються через Zend VM.

Таким чином, у розширенні Opcache є пара інструкцій, які визначають, чи має певний операційний код бути скомпільований чи ні. Якщо так, то компілятор перетворює його в машинний код з допомогою DynASM і виконує цей новий згенерований машинний код.

Цікаво, що, оскільки в поточній реалізації є обмеження в мегабайтах для скомпільованого коду (також настроюється), виконання коду повинна мати можливість безперешкодного перемикання між JIT і  кодом що інтерпретується.

До речі, ця бесіда Бенуа Жакемона про JIT від php ДУЖЕ допомогла мені розібратися у всьому цьому.

Я досі не впевнений в тому, в яких конкретних випадках відбувається компіляція, але я думаю, що поки не дуже хочу це знати.

Так що, ймовірно, ваш приріст продуктивності не буде колосальним

Я сподіваюся, що зараз набагато зрозуміліше, ЧОМУ всі кажуть, що більшість застосунків php не отримають великих переваг в продуктивності від використання компілятора Just In Time. І чому рекомендація Зіва для профілювання та експерименту з різними конфігураціями JIT для вашого застосунку — найкращий шлях.

Скомпільовані опкоди зазвичай будуть розподілені між декількома запитами, якщо ви використовуєте PHP-FPM, але це все одно не змінить правила гри.

Це тому, що JIT оптимізує операції з процесором, а нині більшість php-застосунків більшою мірою зав'язані на вводі/виводі, ніж на чому-небудь ще. Не має значення, скомпільовані операції обробки, якщо вам все одно доведеться звертатися до диска або мережі. Таймінги будуть дуже схожі.

Якщо тільки...

Ви робите щось не зав'язане на введення/виведення, наприклад, обробку зображень або машинне навчання. Все, що не стосується введення/виводу, отримає користь від компілятора Just In Time. Це також причина, по якій люди зараз кажуть, що вони схиляються більше до написання нативних функцій PHP, написаних на PHP, а не на C. Накладні витрати не будуть разюче відрізнятися, якщо такі функції будуть скомпільовані в будь-якому випадку.

Переклад статті Understanding PHP 8's JIT

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.