Главная » Хабрахабр » Как мы сделали PHP 7 в два раза быстрее PHP 5

Как мы сделали PHP 7 в два раза быстрее PHP 5

В декабре 2015 вышел PHP 7.0. Компании, которые перешли на «семерку» отметили, что увеличилась производительность, а нагрузка на сервера — уменьшилась. Первыми перешли на семерку Vebia и Etsy, а у нас Badoo, Авито и OLX. Для Badoo переход на семёрку обошелся в 1 млн. долларов экономии на серверах. Благодаря PHP 7 в OLX средняя нагрузка на сервер снизилась в 3 раза, повысилась эффективность и экономия ресурсов.

В расшифровке: о внутреннем устройстве PHP, об идеях в основе версии 7. Дмитрий Стогов из Zend Technologies на HighLoad++ рассказал, благодаря чему повысилась производительность. 0, об изменениях в базовых структурах данных и алгоритмах, которые и определили успех.

Доклад Дмитрия от 2016 года про принципы, благодаря которым произошел двукратный скачок производительности между PHP 5 и 7, — актуален и в марте 2019. Disclaimer: На март 2019 года 80% сайтов работают на PHP, и 70% из них — на PHP 5, хотя с 1 января 2019 эта версия не поддерживается. Для половины сайтов — точно.

О спикере: Дмитрий Стогов начал программировать еще в 80-х: «Электроника Б3-34», Basic, ассемблер. В 2002 Дмитрий познакомился с PHP и вскоре, начал работать над его усовершенствованием: разработал Turck MMCache для PHP, руководил проектом PHPNG и играл важную роль в работе над JIT для PHP. Последние 14 лет Principal Engineer в Zend Technologies.

В 1999 её основали израильские программисты Энди Гутманс и Зеев Сураски, которые за два года до этого создали PHP 3. Zend Technologies занимается разработкой PHP и коммерческих решений на его основе. Эти люди стояли у истоков разработки PHP и во многом определили текущий вид языка и успех технологии.

Но самой интересной темой для меня всегда была производительность. Zend Technologies разрабатывает ядро PHP и приложения для него, и за время работы мне приходилось писать расширения, влезать во все подсистемы и даже заниматься коммерческими проектами, иногда с PHP совсем не связанными.

За время работы над проектом я досконально разобрался в языке и понял, что работая не с мейнстримным проектом, можно влиять только на отдельные аспекты исполнения скрипта, а все самое интересное и эффективное можно создать только в ядре. Искать пути ускорения PHP я начал еще до прихода в Zend, работая над своим собственным проектом, который конкурировал с компанией. Это понимание и стечение обстоятельств привели меня в Zend.

Небольшой экскурс в историю PHP

PHP – это не совсем и не только язык программирования. PHP переводится как Personal Home Page — инструмент создания персональных веб-страниц и динамических веб-сайтов. Язык – только одна из его основных частей. PHP — это огромная библиотека функций, множество расширений для работы с другими сторонними библиотеками, например, для доступа к БД или к парсерам XML, а также набор модулей для связи с различными веб-серверами.

На тот момент это был просто набор CGI-скриптов, написанных на Perl. Датский программист Расмус Лердорф представил PHP в июне 1995. 0. В апреле 96 Расмус представил PHP/FI, а уже в июне вышла версия PHP/FI 2. 0. Впоследствии эту версию существенно переработали Энди Гутманс и Зеев Сураски, и в 98-м выпустили PHP 3. К 2000 году язык пришел к тому виду, который мы привыкли видеть сегодня как с точки зрения языка, так и внутренней архитектуры — PHP 4, основанный на Zend Engine.

Переломным моментом был выход PHP 5 в 2004, когда полностью обновилась объектная модель. С 4-й версии PHP развивается эволюционно. Предвидя это, сразу после выхода 5. Именно она открыла эру PHP фреймворков и поставила вопрос о производительности на новый уровень. 0 мы в Zend задумались об ускорении PHP и принялись работать над повышением производительности.

1, которая вышла в ноябре 2016 на синтетических тестах в 25 раз быстрее версии 2002 года. Версия 7. 1 и 7. По графику изменения производительности в разных ветках, основные прорывы видны в 5. 0.

1 мы только запустили работу над производительностью, и все за что брались — получалось, но после 5. В версии 5. 3 — уперлись в стену, все попытки усовершенствовать интерпретатор ни к чему не приводили.

6 на тестах. Тем не менее мы нашли, куда копать, и получили даже больше, чем ожидали, — 2,5-кратное ускорение по сравнению с предыдущей версией 5. Это феномен, потому что предыдущий фактор 2 мы нарабатывали в течении всей жизни пятерки за 10 лет. Но самое интересное, что то же 2,5-кратное ускорение мы получили и на неизменных реальных приложениях.

1 на синтетических тестах, на реальных приложениях не заметен. Огромный скачок в 5. Причина в том, что при разных использованиях производительность PHP упирается в тормоза, связанные с разными подсистемами.

Тогда мы поняли, что не можем больше увеличивать производительность мелкими усовершенствованиями нашего интерпретатора и обратились в сторону JIT. История PHP 7 начинается с трехлетнего застоя, который начался в 2012, а закончился в 2015 с релизом седьмой версии.

Блуждание около JIT

Почти два года мы потратили на прототип JIT для PHP-5.5. Сначала мы генерировали очень простой код – последовательность вызовов для стандартных обработчиков, что-то наподобие сшитого кода Форта. Затем написали собственный Runtime Assembler, инлайнили отдельный код для обходов, но поняли, что такие низкоуровневые оптимизации не дают практического эффекта даже на тестах.

Реализовав вывод, сразу же получили 2-кратное ускорение на тестах. Воодушевленные, попытались написать глобальные register alocator, но потерпели неудачу. Тогда мы задумались о выводе типов переменных, используя методы статического анализа. Мы использовали достаточно высокоуровневое представление, а для распределения регистров применять его было практически невозможно.

Кроме того, компиляция реальных приложений теперь занимала минуты, например, первый реквест к WordPress занимал 2 минуты и не давал ускорения. Конечно, это совершенно не подходило для реальной практики. Чтобы избежать проблем с низким уровнем, решили попробовать LLVM, и через год у нас получилось 10-кратное ускорение для bench.php, а на реальных приложениях — ничего.

Хороший код возможен при правильном предсказании типов, которое в реальных приложениях работает плохо, а использование структур данных РНР делает генерируемый код неэффективным.

Что же тормозит?

Мы переосмыслили причины неудач и решили еще раз посмотреть, почему тормозит PHP. На картинке результат профилирования нескольких запросов к домашней странице WordPress.

На интерпретацию байт-кода тратится меньше 30%, 20% — это накладные расходы memory-менеджера, 13% — это работа с хэш-таблицами, и 5% — работа с регулярными выражениями.

Практически везде мы были вынуждены использовать стандартные структуры данных PHP, которые влекли за собой накладные расходы: распределение памяти, подсчет ссылок, и т.п. Работая на JIT, мы избавлялись только от первых 30%, а все остальное лежало мертвым грузом. С этой подмены фундамента и начался проект PHPNG. Это понимание и привело к выводу о необходимости замены ключевых структур данных в PHP.

PHPNG. New Generation

Проект получил развитие после безрезультатных попыток создать JIT для PHP. Основная цель — достичь нового уровня производительности и заложить базу для будущих улучшений.

Реальные приложения, наоборот, подвержены тормозам, связанным с подсистемной памяти, и одно чтение из памяти может стоить 100 вычислительных инструкций. Мы обещали себе какое-то время больше не использовать для измерения производительности синтетические тесты — это как правило небольшие вычислительные программы, которые используют ограниченный объем данных, полностью помещающийся в кэш процессора. Никаких нововведений, 100% совместимость с PHP 5. Проект PHPNG — это рефакторинг ключевых структур данных PHP для оптимизации обращения к памяти.

Но объем зависимых изменений был огромен, потому что само ядро PHP – это 150 000 строк, и почти каждая третья нуждалась в изменении. Как менять эти структуры было понятно. Прибавьте ещё сотню расширений, которые входят в base distribution, десяток модулей для разных веб-серверов, и вы поймете грандиозность проекта.

Поэтому запустили проект в тайне и открыли его, только когда появились первые оптимистичные результаты. Мы даже не были уверены, что доведем проект до конца. Еще через две недели заработал bench.php. Две недели ушло на то, чтобы просто скомпилировать ядро. Еще через месяц мы открыли проект — это был май 2014 года. Полтора месяца потратили для обеспечения работы WordPress. Это уже казалось грандиозным событием. На тот момент у нас было ускорение на 30% на WordPress.

Это был уже другой проект, с другим набором целей, где производительность была только одной из них. PHPNG сразу вызвал волну интереса, и в августе 2014 принят, как основа для будущего PHP 7.

PHP 7.0

Сам номер версии 7 был под вопросом. Предыдущая версия была пятая. А шестая разрабатывалась несколько лет назад и была полностью посвящена нативной поддержке Unicode, но неудачные решения, принятые на ранних этапах разработки, привели к чрезмерному усложнению кода ядра и каждого расширения. В конце концов было принято решение о заморозке проекта.

Чтобы никого не путать, мы назвали проект PHP 7, пропустив PHP 6. К этому времени уже было накоплено много материала, посвящённого PHP 6: выступления на конференциях, опубликованные книги. Этой версии повезло куда больше — PHP 7 вышел в декабре 2015, почти по плану.

Кроме производительности, в PHP 7 появились некоторые давно востребованные нововведения:

  • Возможность определять скалярные типы параметров и возвращаемых значений.
  • Исключения вместо ошибок — теперь мы можем их ловить и обрабатывать.
  • Появились Zero-cost assert(), анонимные классы, чистка неконсистентностей, новые операторы и функции (<=>, ??).

Нововведения это хорошо, но вернемся ко внутренним изменениям. Поговорим о пути, который прошел PHP 7, и о том, куда этот путь нас может завести.

zval

Это основная структура данных PHP. Она используется для представления любого значения в PHP. Так как язык у нас динамически типизированный и тип переменных может меняться во выполнения программы, нам необходимо хранить поле типа (zend_uchar type), которое может принимать значения IS_NULL, IS_BOOL, IS_LONG, IS_DOUBLE, IS_ARRAY, IS_OBJECT и т.д., и собственно значение, представленное union-ом (value), где может храниться целое, вещественное число, строка, массив или объект.

zval в PHP 5

Память под каждую такую структуру выделялась отдельно в Heap. Помимо типа и значения в ней же хранился счетчик ссылок на структуру. Так структура занимала 24 байта, не считая накладные расходы memory-менеджера и указателя на нее.

На картинке справа сверху показаны структуры данных, которые создавались в памяти PHP 5 для простого скрипта.

Сами же значения (zval) лежат в куче. На стеке выделилась память под 4 переменные, представленные указателями. В нашем случае это всего два zval, на каждый из которых ссылаются две переменные, и соответственно их счетчики ссылок установлены равными 2.

Если же надо прочитать не скалярное значение, а например, часть строки или массива, то потребуется как минимум еще на одно чтение больше. Для доступа к типу или к скалярному значению нужно как минимум два чтения: сначала прочитать значение указателя, а потом значение структуры.

zval в PHP 7

Там, где раньше мы использовали указатели, в семерке мы стали встраивать zval. Мы ушли от подсчета ссылок для скалярных типов. Поля тип и значение остались без существенных изменений, но добавились еще некоторые флаги и зарезервированное место, про которые расскажу чуть позже.

Слева — как это выглядело в PHP 5, а справа — в PHP 7.

Для чтения типов и скалярных значений достаточно всего одной машинной инструкции. Теперь на стеке лежат сами zval. Но настоящая мощь нового представления включается при необходимости копирования. Все значения сгруппированы в одной области памяти, а это значит, что при работе с локальными переменными у нас практически не будет потерь из-за промахов кэша процессора.

Копирование записи

В верхней строчке скрипта добавилось еще одно присваивание.

В PHP5 мы выделяли из кучи память под новый zval, инициализировали его int(2), изменяли значение указателя переменной b и уменьшали reference counter того значения, на которое b ссылалось раньше.

Так zval выглядит сейчас в памяти. В PHP 7 мы просто инициализировали переменную b прямо по месту с помощью нескольких инструкций, в то время как в PHP 5 это требовало сотен инструкций.

Первое слово — значение: целое число, вещественное или указатель. Это два 64-битных слова. Но оно не пропадает, а используется разными подсистемами для хранения косвенно связанных значений. Во втором слове тип (он говорит, как интерпретировать значение), флаги, и зарезервированное место, которое все равно добавилось бы при выравнивании.

Например, если стоит IS_TYPE_REFCOUNTED, то при работе с данным zval, engine должен заботиться о значении счетчика ссылок. Флаги — это набор битов, где каждый бит говорит о том, поддерживает ли zval какой-то протокол. При присваивании — увеличивать, при выходе из области видимости — уменьшать, если reference counter достиг нуля — уничтожать зависимую структуру.

Из типов, по сравнению с PHP 5, появилось несколько новых.

  • IS_UNDEF — маркер неинициализированной переменной.
  • На смену единому IS_BOOL пришли раздельные IS_FALSE и IS_TRUE.
  • Добавился отдельный тип для ссылок и еще несколько магических типов.

Типы от IS_UNDEF до IS_DOUBLE — скалярные, и не требуют дополнительной памяти. Для их копирования достаточно скопировать первое машинное 64-битное слово со значением и половину второго с типом и флагами.

Refcounted

С другими типами сложнее. Все они представлены подчиненной структурой, и в zval хранится просто ссылка на эту структуру. Для каждого из типов эта структура своя, но в терминах ООП все они имеют общего абстрактного предка или структуру zend_refcounted. Она определяет формат первого 64-битного слова, где хранится счетчик ссылок и другая информация для сборщика мусора.

Это слово можно рассматривать просто как информацию для сборщика мусора, а структуры для конкретных типов добавляют свои поля вслед за этим первым словом.

Строки

В семёрке для строки мы храним вычисленное значение хэш-функции, её длину и сами символы. Размер такой структуры переменный и зависит от длины строки. Хэш-функция вычисляется для строки один раз, при первой необходимости. В PHP 5 она заново вычислялась при каждой потребности.

Теперь строки стали reference countable, и если в PHP 5 мы копировали сами символы, то теперь достаточно увеличить счетчик ссылок на данную структуру.

Они обычно существуют в одном экземпляре, живут до конца запроса и могут вести себя как скалярные значения. Так же как и в PHP 5 у нас осталось понятие неизменяемых или interned-строк. Нам незачем заботиться о счетчике ссылок на них, и для копирования достаточно скопировать только сам zval с помощью четырех машинных инструкций.

Массивы

Массивы представлены встроенной хэш-таблицей и мало чем отличаются от PHP 5. Сама хэш-таблица изменилась, но об этом отдельно.

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

Так хэш-таблица выглядит в PHP 5.

Каждый элемент представлен Bucket. Это классическая реализация хэш-таблицы с разрешением коллизий с помощью линейных списков (показана в правом верхнем углу). Значения под каждый zval выделяются отдельно — в Bucket мы храним только ссылку на него. Все Buckets связаны двусвязными списками для разрешения коллизий, и связаны еще другим двусвязным списком для итерации по порядку. Также строковые ключи могут выделяться отдельно.

Каждый такой переход может вызвать cahce miss и задержку на ~10-100 циклов процессора. Таким образом, под каждую хэш-таблицу нужно выделять очень много мелких блоков памяти, а чтобы потом что-то найти, приходится бегать по указателям.

Вот что получилось в PHP 7.

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

Для плоских или упакованных массивов, когда мы храним только элементы с числовыми индексами, верхняя часть вообще не выделяется, и мы адресуемся к Bucket-ам непосредственно по номеру. На картинке, внизу от базового указателя лежат элементы, а вверху — хэш-массив, который адресуется по хэш-функции.

Значения встроены в Buckets, а вот зарезервированное место в них как раз используется для разрешения коллизий. Для обхода элементов мы последовательно перебираем их сверху вниз или снизу вверх, что современные процессоры делают безупречно. Там хранится индекс другого Bucket с тем же значением хэш-функции либо маркер конца списка.

При вставке в массив достаточно увеличить reference counter строки, хотя раньше нам приходилось копировать непосредственно символы, а при поиске мы теперь можем сравнивать не символы, а сами указатели на строки. Память под строковые значения ключей выделяется отдельно, но это все те же zend_string.

Неизменяемые массивы

Раньше у нас были неизменяемые строки, а теперь появились еще и неизменяемые массивы. Как и строки они не используют счетчик ссылок и не уничтожаются до конца запроса. Это простой скрипт, который создает массив из миллиона элементов, а каждый элемент — это один и тот же массив с единственным элементом «hello».

В PHP 7 на этапе компиляции мы создаем всего один неизменяемый массив, который ведет себя как скаляр, и добавляем его в результирующий. В PHP 5 на каждой итерации цикла создавался новый пустой массив, в него записывалось «hello», и все это добавлялось в результирующий массив. На представленном примере это позволяет добиться более чем 10-кратного уменьшения потребления памяти и почти 10-кратного ускорения.

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

Объекты

Ссылки на все объекты в PHP 5 лежали в отдельном хранилище, а в zval был только handle — уникальному ID объекта.

Кроме того, память под значение каждого свойства объекта распределялась отдельно, и нам требовалось еще как минимум 2 чтения, чтобы прочитать его. Чтобы добраться до объекта, мы производили как минимум 3 чтения.

В PHP 7 мы смогли перейти к прямой адресации.

А Property встроены и для их чтения нужно всего одно дополнительное чтение. Теперь адрес zend_object доступен с помощью одной машинной инструкции. Также они сгруппированы вместе, что улучшает data locality и помогает современным процессорам не спотыкаться.

В PHP к любому объекту можно добавить property, которые изначально не были определены, и если для доступа к предопределенным property достаточно нескольких машинных инструкций, то для не предопределенных придется обращаться к хэш-таблице, что потребует десятков машинных инструкций. Кроме предопределенных property здесь же хранится ссылка на класс данного объекта, некие handlers — аналог таблиц виртуальных методов, и хэш-таблица для property, которые не были определены. Конечно, это намного дороже.

Reference

Наконец, нам пришлось ввести отдельный тип для представления PHP ссылок.

Он не виден PHP скриптам. Это абсолютно прозрачный тип. Подразумевается, что на одну такую структуру у нас ссылаются как минимум из двух мест, и reference counter этой структуры всегда больше 1. Скрипты видят другой zval, который встроен в структуру zend_reference. Встроенный в ссылку zval копируется в последний ссылающийся на него zval, а сама структура удаляется. Как только счетчик падает до 1, ссылка превращается в обычное скалярное значение.

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

IS_FALSE и IS_TRUE

Я уже говорил, что единый тип IS_BOOL был разбит на отдельные IS_FALSE и IS_TRUE. Эта идея была подсмотрена в реализации LuaJIT, а сделана для ускорения одной из наиболее частых операций — условного перехода.

Если в PHP 5 требовалось прочитать тип, проверить на boolean, прочитать значение, узнать true оно или false и сделать переход на основании этого, то теперь достаточно просто проверить тип и сравнить его с true:

  • если он равен true, то идем по одной ветке;
  • если он меньше true, идем по другой ветке;
  • если он больше true, идем на так называемый slow path (медленный путь) и там проверяем, что это за тип пришел и что с ним делать: если это integer, то мы должны сравнить его значение с 0, если float — опять с 0 (но вещественным), и т.д.

Calling Convention

Изменение в Calling Convention или соглашении о вызовах функций — важная оптимизация, которая затрагивает не только структуры данных, но и в базовые алгоритмы. На картинке слева небольшой скрипт, состоящий из функции foo() и ее вызова. Ниже — байт-код, в который этот скрипт был скомпилирован PHP 5.

Сначала расскажу, как это работало в PHP 5.

Calling Convention в PHP 5

Первая инструкция SEND_VAL должна была отправить значение «3» в функцию foo. Для этого она была вынуждена аллоцировать новый zval на куче, копировать туда значение (3) и записать на стек значение указателя на эту структуру.

Дальше DO_FCALL инициализировал CALL FRAME, резервировал место под локальные и временные переменные, и передавал управление на вызываемую функцию. Аналогично со второй инструкцией.

Тут мы обошлись без копирования и просто увеличили счетчик ссылок соответствующего параметра (zval со значением 3). Первый оператор RECV проверял первый аргумент и инициализировал на стеке слот соответствующей локальной переменной ($a). Аналогично второй оператор RECV устанавливал связь между переменной $b и параметром 5.

Произошло сложение 3 + 5 — получилось 8. Дальше тело функции. Это временная переменная и ее значение хранилось непосредственно на стеке.

RETURN и мы возвращаемся из функции.

Для этого проходим по всем zval на которые ссылаются слоты из освобождаемого фрейма, и для каждого уменьшаем счетчик ссылок. При возврате освобождаем все переменные и аргументы, которые вышли из области видимости. Если он достиг 0, то уничтожаем соответствующую структуру.

Как видно, даже такая простая операция, как посылка константы в функцию требует распределения новой памяти, копирования и увеличения счетчика ссылок, а потом еще и двойного уменьшения и удаления.

Calling Convention в PHP 7

В PHP 7 эти проблемы исправили — теперь на стеке храним не указатели на zval-ы, а сами zval-ы.

Также мы ввели новую инструкцию INIT_FCALL, которая теперь отвечает за инициализацию и выделение памяти под CALL FRAME, и резервацию места под аргументы и временные переменные.

Следующий SEND_VAL 5 во второй слот. SEND_VAL 3 теперь просто копирует аргумент в первый слот за CALL FRAME.

Казалось бы, DO_FCALL должна передать управление на первую инструкцию вызываемой функции. Дальше самое интересное. Поэтому можно их просто пропустить. Но аргументы уже попали в слоты, которые зарезервированы для переменных параметров $a и $b, и инструкциям RECV просто ничего делать. Если бы посылали три — пропустили бы три. Мы посылали два параметра, поэтому пропускаем две инструкции.

Так что мы переходим непосредственно на тело функции, производим сложение и возвращаемся.

При возврате чистим все локальные переменные, но теперь только для двух слотов, а поскольку там у нас скаляры, нам опять ничего не требуется делать.

Мой рассказ чуть-чуть упрощен, он не учитывает функции с переменным числом аргументов и необходимость проверки типов и некоторые другие моменты.

В PHP есть такие функции, как func_get_arg и func_get_args. Новый Calling Convention немного сломал совместимость. Так же как делают отладчики C. Если раньше они возвращали оригинальное значение посланного параметра, то теперь возвращают текущее значение соответствующей локальной переменной, потому что мы просто не храним оригинальные значения.

Смысла в этом не было и раньше, но такой PHP код foo($_, $_) я встречал. Кроме того, функция теперь не может иметь несколько параметров с одинаковым именем. (Я узнал Prolog) На что это похоже?

Новый менеджер памяти

Закончив с оптимизацией структур данных и базовых алгоритмов, мы еще раз обратили внимание на все тормозящие подсистемы. Менеджер памяти в PHP 5 занимал почти 20% процессорного времени на WordPress.

Происходило это из-за того, что мы использовали классический алгоритм Doug Lea's malloc, который подразумевал поиск подходящих свободных участков памяти с помощью путешествия по ссылкам и деревьям, а все эти путешествия неминуемо вызывали промахи кэша. После того, как мы избавились от множества аллокаций, его накладные расходы стали меньше, но все равно существенны — и не потому что он делал какую-то существенную работу, а потому, что спотыкался на кэше.

Например: jemalloc и ptmalloc от Google. Сегодня существуют новые алгоритмы управления памятью, которые учитывают особенности современных процессоров. В итоге мы отказались от dlmalloc и написали что-то свое, скомбинировав идеи из старого memory manager и jemalloc. Сначала, мы попытались использовать их в неизменном виде, но не получили выигрыша, поскольку отсутствие специфичного для PHP функционала удорожало полное освобождение памяти в конце реквеста.

Подходящие блоки памяти теперь ищутся по битовым картам, память под блоки небольшого размера выделяется из отдельных страниц и кэшируется при освобождении, добавлены специализированные функции для часто используемых размеров блоков. Мы сократили накладные расходы Memory Manager до 5%, уменьшили издержки памяти на служебную информацию и улучшили использование кэша CPU.

Множество мелких усовершенствований

Я рассказал только о самых главных усовершенствованиях, но мелких было куда больше. Могу отметить некоторые из них.

  • Быстрый API для разбора параметров внутренних функций и новый API для итерации по HashTable.
  • Новые инструкции VM: конкатенация строк, специализация, супер-инструкции.
  • Некоторые внутренние функции были превращены в инструкции VM: strlen, is_int.
  • Использование регистров CPU под регистры VM: IP и FP.
  • Оптимизация функций дублирования и удаления массивов.
  • Использование счетчиков ссылок вместо копирования везде, где можно.
  • PCRE JIT.
  • Оптимизация внутренних функций и serialize().
  • Уменьшение размера кода и обрабатываемых данных.

Одни были очень простыми, например, потребовалось всего три строчки кода, чтобы включить JIT в регулярных перловских выражениях, и это сразу принесло видимое (2-3%) ускорение почти всем приложениям. Другие оптимизации затрагивали какие-то узкие аспекты определенных PHP функций, и не особо интересны, хотя суммарный вклад всех этих мелких усовершенствований вполне значим.

К чему пришли

Это вклад различных подсистем на WordPress/PHP 7.0.

Memory Manager потребляет уже меньше 5% — и в основном не за счет оптимизаций самого Memory Manager, а за счет уменьшения количества обращений к нему. Вклад виртуальной машины увеличен до 50%. раз, то сейчас только 10 млн. Если раньше на этом же тесте память выделялась 130 млн. Может показаться, что все основное ускорение достигнуто за счет уменьшения накладных расходов Memory Manager и уменьшения количества обращений к нему за счет улучшения структур данных, но на самом деле все подсистемы были существенно усовершенствованы.


Основные источники ускорения:

  • Интерпретатор стал работать лучше в 2 раза.
  • Накладные расходы MM уменьшились в 17 раз.
  • Хэш-таблицы стали работать быстрее в 4 раза.
  • Общая производительность на WordPress выросла в 3,5 раза.

В начале статьи мы говорили о 2,5-кратном реальном ускорении, а сейчас цифры другие. Почему так? Дело в том, что реальную скорость мы измеряли в запросах в секунду, а здесь скорость измерена профилировщиком в терминах CPU time, по сути — тактах процессора, когда он не простаивает. Когда PHP ждет ответа от базы данных, процессор стоит и это время здесь не учитывается.

Производительность PHP 7

WordPress 3.6 был для нас основным бенчмарком — на нём мы мониторили производительность с первых дней работы. В какой-то момент, когда из PHP 7 выкинули расширение mysql, нам пришлось его специально поддерживать, просто чтобы продолжить этот график.

К августу было набрано 2/3 улучшений. На графике видно, что основные прорывы произошли в первые месяцы работы над PHPNG. Дальше мы двигались маленькими шажками, и набрали оставшуюся треть.

Разумеется, мы измеряли производительность не только на WordPress, но и на других популярных приложениях, и практически везде мы видим — от 1,5 до 2-кратное ускорение.

PHP 7 и HHVM

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

Всегда выигрыш в пользу измеряющего. Но сравнение со сторонним продуктом — неблагодарное занятие. На графике HHVM везде пропорционально быстрее. Версия команды Facebook показывает другие результаты. Возможно, это связано с разными процедурами замера, тестированием на разных аппаратных платформах, разницы в тонких настройках, а может повлияли и субъективные факторы.

Пионерами были китайский Vebia, американский Etsy и Badoo. Апофеоз PHP 7 — начало использования крупными сайтами. Highload-проверка вскрыла несколько существенных проблем, но они были быстро локализованы и пофикшены.

0 для Etsy и Badoo позволил выключить практически половину серверов в веб-фермах. Переход на PHP 7. Badoo оценил экономию в миллион долларов.

Показательны графики, что на момент перехода суммарная загрузка процессоров уменьшилась в 2 раза, а потребление памяти — аж в 7 раз.

0. На этой радостной ноте закончим сегодняшний разговор о PHP 7. 1, в оптимизации которого пошли существенно дальше структур данных. Но продолжим его совсем скоро с PHP 7.

Если и ваш опыт во многом связан с PHP, вы знаете, как его правильно готовить, и готовы поделиться своими наработками — присылайте заявки до 1 апреля. В мае на PHP Russia Дмитрий Стогов выступит с докладом о самых интересных новых технологиях разрабатываемых для PHP 8. И помните, главное, что мы ищем — живой полезный опыт, а с докладом поможем, зададим правильные вопросы и подскажем, куда двигаться.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] История транзистора, часть 2: из горнила войны

Другие статьи цикла: История реле История электронных компьютеров История транзистора Горнило войны подготовило почву для появления транзистора. С 1939 по 1945 года технические знания из области полупроводников невероятно сильно разрослись. И тому была одна простая причина: радар. Самая важная технология ...

Что можно сделать через разъем OBD в автомобиле

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