Почему вызовы функций PHP * так дорого?
Вызов функции в PHP стоит дорого. Вот небольшой тест для тестирования:
<?php
const RUNS = 1000000;
// create test string
$string = str_repeat('a', 1000);
$maxChars = 500;
// with function call
$start = microtime(true);
for ($i = 0; $i < RUNS; ++$i) {
strlen($string) <= $maxChars;
}
echo 'with function call: ', microtime(true) - $start, "\n";
// without function call
$start = microtime(true);
for ($i = 0; $i < RUNS; ++$i) {
!isset($string[$maxChars]);
}
echo 'without function call: ', microtime(true) - $start;
Это тестирует функционально идентичный код сначала с использованием функции (strlen
), а затем без использования функции (isset
не является функцией).
Я получаю следующий вывод:
with function call: 4.5108239650726
without function call: 0.84017300605774
Как вы можете видеть, реализация, использующая вызов функции, более чем в пять (5,38) раз медленнее, чем реализация, не вызывающая какую-либо функцию.
Я хотел бы знать, почему вызов функции такой дорогой. Какое главное узкое место? Это поиск в хэш-таблице? Или что так медленно?
Я вернулся к этому вопросу и решил снова запустить тест, с полностью отключенным XDebug (не только отключенным профилированием). Это показало, что мои тесты были довольно запутанными, на этот раз с 10000000 прогонов, которые я получил:
with function call: 3.152988910675
without function call: 1.4107749462128
Здесь только вызов функции примерно в два раза (2,23) медленнее, поэтому разница намного меньше.
Я только что протестировал приведенный выше код на снимке PHP 5.4.0 и получил следующие результаты:
with function call: 2.3795559406281
without function call: 0.90840601921082
Здесь разница снова немного увеличилась (2,62). (Но с другой стороны, время выполнения обоих методов значительно сократилось).
Ответы
Ответ 1
Функциональные вызовы стоят дорого в PHP, потому что там много вещей.
Обратите внимание, что isset
не является функцией (для него есть специальный код операции), поэтому он быстрее.
Для простой программы, например:
<?php
func("arg1", "arg2");
Есть шесть (четыре + один для каждого аргумента) опкодов:
1 INIT_FCALL_BY_NAME 'func', 'func'
2 EXT_FCALL_BEGIN
3 SEND_VAL 'arg1'
4 SEND_VAL 'arg2'
5 DO_FCALL_BY_NAME 2
6 EXT_FCALL_END
Вы можете проверить реализации кодов операций в zend_vm_def.h
. Подготовьте ZEND_
к именам, например. для ZEND_INIT_FCALL_BY_NAME
и поиска.
ZEND_DO_FCALL_BY_NAME
особенно сложна. Затем выполняется сама реализация самой функции, которая должна разматывать стек, проверять типы, преобразовывать zvals и, возможно, их разделять и выполнять фактическую работу...
Ответ 2
Я бы утвердил, что это не так. Вы вообще не проверяете вызов функции. Вы проверяете разницу между проверкой низкого уровня за пределами (isset) и прохождением строки, чтобы подсчитать количество байтов (strlen).
Я не могу найти никакой информации, специфичной для PHP, но strlen обычно реализует что-то вроде (включая служебные служебные вызовы):
$sp += 128;
$str->address = 345;
$i = 0;
while ($str[$i] != 0) {
$i++;
}
return $i < $length;
Проверка вне границ обычно реализуется примерно так:
return $str->length < $length;
Первый - это цикл. Второй - простой тест.
Ответ 3
Являются ли накладные расходы для вызова функции пользователя действительно такой большой? Или, вернее, это действительно так больно? Как PHP, так и компьютерное оборудование продвинулись в прыжках в течение почти 7 лет с тех пор, как этот вопрос изначально был задан.
Я написал собственный бенчмаркинг script, ниже которого вызывает mt_rand() в цикле как напрямую, так и через вызов пользовательской функции:
const LOOPS = 10000000;
function myFunc ($a, $b)
{
return mt_rand ($a, $b);
}
// Call mt_rand, simply to ensure that any costs for setting it up on first call are already accounted for
mt_rand (0, 1000000);
$start = microtime (true);
for ($x = LOOPS; $x > 0; $x--)
{
mt_rand (0, 1000000);
}
echo "Inline calling mt_rand() took " . (microtime(true) - $start) . " second(s)\n";
$start = microtime (true);
for ($x = LOOPS; $x > 0; $x--)
{
myFunc (0, 1000000);
}
echo "Calling a user function took " . (microtime(true) - $start) . " second(s)\n";
Результаты на PHP 7 на настольном компьютере на базе i6806 (в частности, Intel® Core ™ i5-6500 с процессором 3,20 ГГц × 4):
Внутренний вызов mt_rand() занял 3.5181620121002 секунду (ы)
Вызов функции пользователя занял 7.2354700565338 секунд (ы)
Накладные расходы на вызов пользовательской функции, по-видимому, примерно удваивают время выполнения. Но для этого потребовалось 10 миллионов итераций, чтобы стать особенно заметными. Это означает, что в большинстве случаев различия между встроенным кодом и функцией пользователя, вероятно, будут незначительными. Вы должны только беспокоиться об этой оптимизации в самых внутренних циклах вашей программы, и даже тогда, только если бенчмаркинг демонстрирует ясную проблему с производительностью. Все остальное было бы micro-optimisation, которые незначительное отсутствие значимого повышения производительности для дополнительной сложности в исходном коде.
Если ваш PHP script медленный, то вероятность почти наверняка будет заключаться в сокращении ввода-вывода или недостаточном выборе алгоритма, а не накладных расходов на функционирование. Подключение к базе данных, выполнение запроса CURL, запись в файл или даже эхо в stdout на все порядки дороже вызова функции пользователя. Если вы мне не верите, mt_rand и myfunc эхо их вывод и посмотрите, насколько медленнее работает script!
В большинстве случаев лучшим способом оптимизации PHP script является минимизация объема ввода-вывода, которое он должен делать (выберите только то, что вам нужно в запросах БД, вместо того, чтобы полагаться на PHP для фильтрации нежелательных строк, для пример), или заставить его кэшировать операции ввода-вывода, хотя что-то вроде memcache, чтобы уменьшить стоимость ввода-вывода для файлов, баз данных, удаленных сайтов и т.д.
Ответ 4
Функциональные вызовы дороги по причине, отлично объясненной выше @Artefacto. Обратите внимание, что их производительность напрямую связана с количеством параметров/аргументов. Это одна из областей, на которую я уделял пристальное внимание при разработке собственной платформы приложений. Когда это имеет смысл и возможно избежать вызова функции, я делаю.
Одним из таких примеров является недавняя замена вызовов is_numeric()
и is_integer()
с простым булевым тестом в моем коде, особенно когда может быть сделано несколько вызовов этих функций. Хотя некоторые могут подумать, что такая оптимизация бессмысленна, я заметил резкое улучшение в реагировании своих сайтов с помощью такого рода оптимизации.
Следующий быстрый тест будет TRUE для числа и FALSE для чего-либо еще.
if ($x == '0'.$x) { ... }
Гораздо быстрее, чем is_numeric()
и is_integer()
. Опять же, только тогда, когда это имеет смысл, это совершенно справедливо для использования некоторых оптимизаций.
Ответ 5
Я думаю, что богатый ответ на помощь на самом деле довольно точен. Вы сравниваете яблоки с апельсинами с вашим оригинальным примером. Попробуйте вместо этого:
<?php
$RUNS = 100000;
// with function call
$x = "";
$start = microtime(true);
for ($i = 0; $i < $RUNS; ++$i) {
$x = $i.nothing($x);
}
echo 'with function call: ', microtime(true) - $start, "\n<br/>";
// without function call
$x = "";
$start = microtime(true);
for ($i = 0; $i < $RUNS; ++$i) {
$x = $i.$x;
}
echo 'without function call: ', microtime(true) - $start;
function nothing($x) {
return $x;
}
Единственное различие в этом примере - это сам вызов функции. С 100 000 прогонов (как указано выше) мы видим разницу в 1% от использования вызова функции из нашего вывода:
with function call: 2.4601600170135
without function call: 2.4477159976959
Конечно, все это зависит от того, что делает ваша функция и что вы считаете дорогостоящим. Если nothing()
возвращен $x*2
(и мы заменили нефункционный вызов $x = $i.$x
на $x = $i.($x*2)
, мы увидели бы потерю 4% при использовании вызова функции.