company_banner

Как мы сделали PHP 7 в два раза быстрее PHP 5. Часть 2: оптимизация байт-кода в PHP 7.1

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

    Накопленный опыт работы над JIT до семёрки теперь можно интерпретировать, смотря на результаты в 7.0 без JIT и на результаты HHVM с JIT. В PHP 7.1 было решено c JIT не работать, а опять обратиться к интерпретатору. Если раньше оптимизации касались интрепретатора, то в этой статье посмотрим на оптимизацию байт-кода, с использованием вывода типов, который реализовали для нашего JIT.



    Под катом Дмитрий Стогов покажет, как это все работает, на простом примере.

    Оптимизация байт-кода


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


    Оптимизации OPcache


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


    Они могут быть объединены в одну операцию compound assignment, которая выполняет сложение непосредственно над результатом: ASSIGN_ADD $sum, $i. Другой пример пост-инкремент переменной, которая теоретически может вернуть какой-то результат.


    Он может быть не скалярным значением и должен быть удален. Для этого используется следующая за ним инструкция FREE. Но если его изменить на пре-инкремент, то инструкции FREE не потребуется.


    В конце — два оператора RETURN: первый — прямое отражение оператора RETURN в исходном тексте, а второй добавился тупым компилятором по закрывающей скобке. Этот код никогда не будет достигнут и его можно удалить.
    В цикле осталось всего четыре инструкции. Кажется, что дальше нечего оптимизировать, но не для нас.
    Посмотрите на код $i++ и соответствующую ей инструкцию — пре-инкремент PRE_INC. Каждый раз, когда она выполняется:

    • нужно проверить, какой тип переменной пришел;
    • is_long ли это;
    • выполнить инкремент;
    • проверить, не произошло ли переполнение;
    • перейти на следующий;
    • возможно, проверить исключение.

    Но человек, просто посмотрев на код PHP, увидит что переменная $i лежит в диапазоне от 0 до 100, и никакого переполнения быть не может, проверок типов не нужно, и никаких исключений тоже быть не может. В PHP 7.1 мы попытались научить компилятор понимать это.

    Оптимизация Control Flow Graph



    Для этого нужно вывести типы, а чтобы ввести типы надо сначала построить формальное представление потоков данных, понятное компьютеру. Но начнем мы с построения Control Flow Graph — графа зависимости по управлению. Первоначально мы разбиваем код на basic-блоки — набор инструкций с одним входом и одним выходом. Поэтому мы режем код в тех местах, на которых происходит переход, то есть метки L0, L1. Мы также режем его после операторов условного и безусловного перехода, и потом соединяем дугами, которые показывают зависимости по управлению.


    Так у нас получился CFG.

    Оптимизация Static Single Assignment Form


    Ну а теперь нам нужна зависимость по данным. Для этого мы используем Static Single Assignment Form — популярное представление в мире оптимизирующих компиляторов. Оно подразумевает, что значение каждой переменной может быть присвоено только один раз.


    Для каждой переменной мы добавляем индекс, или номер реинкарнации. В каждом месте, где происходит присваивание мы ставим новый индекс, а там, где мы их используем — пока знаки вопроса, потому что не везде он пока известен. Например, в инструкции IS_SMALLER $i может прийти как из блока L0 с номером 4, так и из первого блока с номером 2.

    Для решения этой проблемы SSA вводит псевдо-функцию Phi, которая по необходимости вставляется в начало basic->block-а, берет всевозможные индексы одной переменной, пришедшие в basic-block из разных мест, и создает новую реинкарнацию переменной. Именно такие переменные потом и используются для устранения неоднозначности.


    Заменив таким образом все знаки вопроса мы и построим SSA.

    Оптимизация по типам


    Теперь выводим типы — как будто пытаемся выполнить этот код непосредственно по управлению.


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


    Считаем, что на выходе phi() у нас будет long.


    Распространяем дальше. Приходим к конкретным функциям, например, ASSIGN_ADD и PRE_INC. Складываем два long. В результате может получиться либо long, либо double, если произойдет переполнение.


    Эти значения опять попадают в функцию Phi, происходит объединение множеств возможных типов пришедших по разным веткам. Ну и так далее продолжаем распространение, пока не придем к fixed point и все не устаканится.


    Мы получили возможное множество значений типов в каждой точке программы. Это уже хорошо. Компьютер уже знает что $i может быть только long или double, и может исключить часть ненужных проверок. Но мы-то знаем что и double $i быть не может. А как мы знаем? А мы видим условие которое ограничивает рост $i в цикле до возможного переполнения. Научим и компьютер видеть это.

    Оптимизация Range Propagation


    В инструкции PRE_INC мы так и не узнали, что i может быть только целым — стоит long или double. Происходит это потому, что мы не пытались вывести возможные диапазоны. Тогда бы мы могли ответить на вопрос, произойдет или не произойдет переполнение.

    Производится этот вывод диапазонов похожим, но чуть более сложным образом. В результате получаем фиксированный диапазон переменных $i с индексами 2, 4, 6 7, и теперь можем уверенно сказать что инкремент $i не приведет к переполнению.


    Скомбинировав эти два результата, мы можем точно сказать, что double переменная $i никогда стать не сможет.


    Все что мы получили это еще не оптимизация, это информация для оптимизации! Рассмотрим инструкцию ASSIGN_ADD. В общем виде старое значение суммы, которое пришло к этой инструкции, могло быть, например, объектом. Тогда, после сложения, старое значение должно было быть удалено. Но в нашем случае мы точно знаем, что там long или double, то есть скалярное значение. Никакого уничтожения не требуется, мы можем заменить ASSIGN_ADD на ADD — инструкцию попроще. ADD использует переменную sum в качестве и аргумента и значения.


    Для операций пре-инкремент мы точно знаем, что операнд всегда long, и что переполнения произойти не может. Используем высокоспециализированный обработчик для этой инструкции, который будет выполнять только необходимые действия без всяких проверок.


    Теперь сравнение переменной в конце цикла. Мы знаем, что значение переменной будет только long — можно сразу проверить это значение, сравнив его с сотней. Если раньше мы записывали результат проверки во временную переменную, а потом еще раз проверяли временную переменную на значение true/false, теперь это можно сделать с помощью одной инструкции, то есть упростить.


    Результат байт-кода по сравнению с оригиналом.


    В цикле осталось всего 3 инструкции, и две из них высокоспециализированные. В результате код справа работает в 3 раза быстрее, оригинала.

    Высокоспециализированные обработчики


    Любой обработчик обхода в PHP — это просто С-функция. Слева стандартный обработчик, а наверху справа — высокоспециализированный. Левый проверяет: тип операнда, не произошел ли overflow, не произошел ли exception. Правый просто добавляет единицу и всё. Он транслируется в 4 машинные инструкции. Если бы мы пошли дальше и делали JIT, то нам бы была нужна только однократная инструкция incl.


    Что дальше?


    Мы продолжаем повышать скорость PHP ветки 7 без JIT. PHP 7.1 опять будет на 60% быстрее на характерных синтетических тестах, но на реальных приложениях выигрыша это практически не дает — всего 1-2% на WordPress. Это не особо интересно. С августа 2016, когда ветка 7.1 была заморожена для существенных изменений, мы снова начали трудиться над JIT для PHP 7.2 или скорее PHP 8.

    В новой попытке мы используем для генерации кода DynAsm, который разработан Майком Полом для LuaJIT-2. Он хорош тем, что генерирует код очень быстро: то, что в версии JIT на LLVM компилировалось минуты, сейчас происходит за 0,1-0,2 с. Уже сегодня ускорение на bench.php на JIT в 75 раз быстрее чем PHP 5.

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

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

    Ниже машинный код, который генерирует наш JIT все для того же примера. Многие инструкции скомпилированы оптимально: инкремент — в одну инструкцию CPU, инициализация переменной константам — в две. Там, где типы не вывелись, приходится возиться чуть больше.


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

    На диаграмме отражено время исполнения в секундах, меньше — лучше.

    Возможно Mandelbrot — не лучший тест. Он вычислительный, но зато простой и реализован на всех языках одинаково. Неплохо было бы узнать, с какой скоростью заработал бы Wordpress на С++, но вряд ли найдется чудак готовый переписать его просто чтобы проверить, да еще повторить все извраты PHP-ного кода. Если есть идеи по более адекватному набору бенчмарков — предлагайте.

    Встретимся на PHP Russia 17 мая, обсудим перспективы и развитие экосистемы и опыт использования PHP для действительно сложных и крутых проектов. Уже с нами:


    Конечно, это далеко не все. Да и Call for Papers еще на закрыт, до 1 апреля ждём заявки от тех, кто умеет применять современные подходы и лучшие практики, чтобы реализовать классные сервисы на PHP. Не бойтесь конкуренции с именитыми спикерами — мы ищем опыт использования того, что они делают, в реальных проектах и поможем показать пользу ваших кейсов.
    Конференции Олега Бунина (Онтико)
    Конференции Олега Бунина

    Comments 16

      +1
      Возможно Mandelbrot — не лучший тест. Он вычислительный,


      Именно так. Не лучший. Что очень хорошо видно на наиболее медленных динамических языках на графике — они вовсе не под математику заточены, соответственно она в них плохая. Тащем-та в местах, где именно вычисления важны, не то что плюсы (игры/графика), а до сих пор и Фортран применяется (в научной среде).

      Для реальных задач надо сравнивать что-нибудь другое.
        0
        Если есть идеи по более адекватному набору бенчмарков — предлагайте.

        wordpress — самый показательный, а из «синтетических» можно посмотреть проект benchmarksgame, там кроме мандельброта есть ещё бинарные деревья, к-нуклеотиды и т.п. Хотя возможно автор как раз и использовал эти бенчмарки.
          0
          Почитал, что Дмитрий Стогов пишет в rfc/jit:
          С введением jit в php 8 (большинство пока что голосует «за») Мандельброт ускоряется в 4 раза с 0.046 до 0.011 sec, а эта самая нижняя строчка на картинке (GCC) и быстрее уже ничего нет, ну по крайней мере 2 года назад ничего не было. Т.е. использовать Мандельброта уже будет бесполезно, цифру не уменьшить.
          Единственное жаль, что jit скорее всего не войдёт PHP 7.4 (большинство пока что проголосовало против).

          PS: на графике единица измерения «sec» накладывается на копирайт и не разобрать что там написано, поэтому не сразу понятно с чего вдруг php 5.3 быстрее чем 7.0. Лучше добавить подпись под картинкой «секунды (меньшее значение — лучше)», так обычно делают для неоднозначных диаграмм.
          0
          Насколько сложно написать расширение пхп с очень жесткой типизацией, чтобы оптимизировать байткод под типы?
            0

            Только расширение не php, а zend. Да, и нужно ли? Если код тормозит, то скорее всего дело не в php, а, если в нём, то проще переписать критический участок на что-то другое: хоть zephir, хоть плюсы, хоть brainfuck. А, если участок настолько большой, что озвученная идея кажется хорошей, значит где-то архитектурная ошибка и всё очень плохо.

              +1
              Обновить код усилинной типизацией не так уж сложно и долго. Вопрос в том — сколько надо усилий, чтобы компилятор смог улучшить оптимизацию за счет этой новой информации.
            0
            И интересно узнать как там живет современный HHVM. Кажется с версии 7.1 он обратно сжался внутрь фейсбука
              0
              Достаточно много кто из больших на HHVM, поэтому вряд ли все так быстро слезут. Но с учётом отказа от поддержки новых фич из PHP в новых версиях HHVM, да, скорее всего с него все будут слезать :).
                0
                Возможно, я неправильно понял этот пост, конечно: hhvm.com/blog/2017/09/18/the-future-of-hhvm.html
                  +2
                  А кто еще использует хипхоп?

                  Гугление случайной выборки отсюда github.com/facebook/hhvm/wiki/Users — показало, что она устарела. Все, кого я гуглил — обратно свичнулись на пхп7.
                0
                А почему современная версия php(7+) сравнивается с старой версией python(3.6 давно мейнстрим)?

                Кстати pypy, который в этом тесте в 31 раз быстрее питона, в нашем проекте, например, работает медленее питона(django).

                В общем, какойто странный выбор теста.
                  0
                  Там же в статье рядом с графиком написано:
                  данные актуальны на конец 2016 года

                  А «современная версия php(7+)» — не такая уж и современная.
                  php 7.0.0 — 03 Dec 2015
                  php 7.0.7 — 26 May 2016
                  php 7.1.0 — 01 Dec 2016
                  А сейчас уже вроде как 28 Mar 2019.
                    0
                    python 3.6 — Dec. 23, 2016
                    python 3.4 — March 16, 2014
                    И что?
                    Ну вот не понятна мне идея сравнивать на тесте, не имеющего отношения к применению php да и еще с заведомо более старыми версиями других языков.
                    Типа «посмотрите, мы тут внесли улучшения, они у других давно есть, но мы сравним со старыми».
                +1
                Если есть идеи по более адекватному набору бенчмарков — предлагайте.

                https://www.techempower.com/benchmarks/
                Ближе к реальным ситуациям, особенно тем, что там именно та ниша, в которой используется пхп.

                  –1

                  Познавательно.

                  Only users with full accounts can post comments. Log in, please.