PHP Profi

Оптимизация производительности PHP с помощью полных вызовов функций Перевод

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

Wondering how much #PHP I could break by removing the local namespace lookup semantics for namespaced vs. core functions...

— Reviewed, BLYATIFUL! (@Ocramius) December 21, 2016

 

Разговор начался с приведенного выше твита. Чтобы лучше понять разницу между глобальными и "пространственными" вызовами функций, я объясню, что происходит под капотом.


Вызов функций глобально

В глобальном пространстве имен вызов функций выглядит следующим образом:

// global.php

function foo() {
    echo 'bar';
}

call_user_func('foo');

После парсинга этого скрипта получаем такие "опкоды" (коды операций, от англ. opcodes):

$ php -d vld.active=1 -d vld.execute=0 global.php

...
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   4     0  E >   EXT_STMT
         1        NOP
   8     2        EXT_STMT
         3        EXT_FCALL_BEGIN
         4        SEND_VAL                                                 'foo'
         5        DO_FCALL                                      1          'call_user_func'
         6        EXT_FCALL_END
         7      > RETURN                                                   1

...

Как можно заметить, это простая последовательность опкодов EXT_FCALL.

Вызов функций в пространстве имен

В namespace вызов функций выглядит примерно так:

// namespaced.php

namespace baz;

function foo() {
    echo 'bar';
}

call_user_func('foo');

После парсинга этого скрипта опкоды следующие:

$ php -d vld.active=1 -d vld.execute=0 global.php

...
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   4     0  E >   NOP
   6     1        EXT_STMT
         2        NOP
  10     3        EXT_STMT
         4        INIT_NS_FCALL_BY_NAME
         5        EXT_FCALL_BEGIN
         6        SEND_VAL                                                 'foo'
         7        DO_FCALL_BY_NAME                              1
         8        EXT_FCALL_END
         9      > RETURN                                                   1
...

Коды операций выглядят почти так же, как и в глобальном пространстве имен. Однако есть еще один добавленный код операции: INIT_NS_FCALL_BY_NAME. Когда PHP запускает этот опкод, он проверяет, находится ли функция call_user_func() в namespace. Если функция существует в пространстве имен, PHP запустит ее. Если функция не существует в текущем пространстве имен, PHP проверит, существует ли она в глобальном пространстве имен, и выполнит ее.

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

Бенчмарки

В одном из следующих твитов говорилось, что повышение производительности на 4,5% было достигнуто за счет использования полного вызова функции. Разумеется, это всего лишь необъективное число, которое зависит от проекта, над которым вы работаете. Чтобы убедиться, что я не пишу ерунду, я произвел небольшой бенчмарк в PHP 7.1:

$ php -v

PHP 7.1.0 (cli) (built: Dec  2 2016 11:29:13) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.1.0-dev, Copyright (c) 1998-2016 Zend Technologies

Затем я написал код, который можно запустить с помощью инструмента phpbench. Я охватил 4 случая:

  • Выполнение глобальной функции с полным именем.
  • Запуск глобальной функции с неполным именем.
  • Запуск переписанной функции, которая существует как глобально, так и в пространстве имен.
  • Запуск функции в пространстве имен.

Я выбрал довольно большое количество ревизий и итераций, чтобы убедиться, что результаты точны. Код в бенчмарке выглядит примерно так:

namespace {
    function a(){}
    function b() {}
}

namespace foo {

    function b() {}
    function c() {}

    /**
     * @Revs(10000)
     * @Iterations(100)
     */
    class MixedBench
    {
        public function benchFqGlobalFunction()
        {
            \a();
        }

        public function benchGlobalFunction()
        {
            a();
        }

        public function benchOverriddenFunction()
        {
            b();
        }

        public function benchNamespacedFunction()
        {
            c();
        }
    }
}

Краткий обзор результатов:

$ phpbench run bench

\foo\MixedBench

    benchFqGlobalFunction         I99 P0    [μ Mo]/r: 0.145 0.141 (μs)  [μSD μRSD]/r: 0.018μs 12.59%
    benchGlobalFunction           I99 P0    [μ Mo]/r: 0.148 0.145 (μs)  [μSD μRSD]/r: 0.021μs 14.38%
    benchOverriddenFunction       I99 P0    [μ Mo]/r: 0.157 0.157 (μs)  [μSD μRSD]/r: 0.022μs 13.82%
    benchNamespacedFunction       I99 P0    [μ Mo]/r: 0.157 0.159 (μs)  [μSD μRSD]/r: 0.019μs 12.38%

4 subjects, 400 iterations, 40,000 revs, 0 rejects
(best [mean mode] worst) = 0.124 [0.152 0.151] 0.225 (μs)
⅀T: 60.773μs μSD/r 0.020μs μRSD/r: 13.293%
suite: 133a2c5566a4e9fb57b0251cebfd189bc150f104, date: 2016-12-21, stime: 22:06:03

+-------------------------+-------+-----+---------+--------+
| subject                 | revs  | its | mean    | diff   |
+-------------------------+-------+-----+---------+--------+
| benchFqGlobalFunction   | 10000 | 100 | 0.145μs | 0.00%  |
| benchGlobalFunction     | 10000 | 100 | 0.148μs | +2.26% |
| benchNamespacedFunction | 10000 | 100 | 0.157μs | +8.55% |
| benchOverriddenFunction | 10000 | 100 | 0.157μs | +8.38% |
+-------------------------+-------+-----+---------+--------+

Как и ожидалось, полный вызов глобальной функции является самым быстрым. Это происходит потому, что PHP не требуется проходить через опкод INIT_NS_FCALL_BY_NAME. Глобальная функция выполняется медленнее, если она вызывается не полностью. Выполнение функций внутри пространства имен всегда медленнее, чем выполнение глобальных функций.

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

Ускорение работы вашего приложения

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

// solution 1:
namespace baz;
\foo();

// solution 2:
namespace baz;
use function foo;
foo();

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

Вывод

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


2020-11-02 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий