Квест → Как хакнуть форму
Прошли: 77
Недавно в Твиттере быстро разгорелось небольшое обсуждение. Очевидно, 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();
К счастью для нас, сообщество очень креативно и бдительно, когда речь заходит о производительности. Возможно, одно из следующих (будущих) решений будет не таким скучным при реализации:
declare(no_dynamic_functions=1)
поверх PHP-файла или, возможно, в предстоящем методе namespace_scoped_declares
.Как вы можете видеть, "убийцы производительности" могут прятаться в небольших углах. Здорово наблюдать, как этот маленький твит может получить такую большую обратную связь от сообщества в такие короткие сроки. Будем надеяться, что хорошее решение этой проблемы производительности будет добавлено в PHP в ближайшее время, чтобы мы могли легко ускорить работу наших приложений еще значительнее. Теперь, когда секрет этой оптимизации раскрыт, я с нетерпением жду, чтобы узнать больше об этих маленьких "убийцах производительности".