Почему вызовы функций PHP * такие * дорогие? - PullRequest
35 голосов
/ 11 сентября 2010

вызов функции в PHP стоит дорого. Вот небольшой тест для тестирования:

// 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). (Но, с другой стороны, время выполнения обоих методов значительно сократилось).

Ответы [ 5 ]

43 голосов
/ 11 сентября 2010

Вызовы функций в 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 особенно сложно. Затем есть реализация самой функции, которая должна разматывать стек, проверять типы, преобразовывать zval и, возможно, отделять их от фактической работы ...

4 голосов
/ 01 февраля 2017

Действительно ли накладные расходы на вызов пользовательской функции настолько велики?Или скорее это действительно так велико сейчас?Как PHP, так и компьютерное оборудование продвинулись стремительно за почти 7 лет с тех пор, как этот вопрос был задан изначально.

Ниже я написал свой собственный скрипт для сравнительного анализа, который вызывает 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 на настольном компьютере на базе i5 2016 года сбора винограда (в частности, процессор Intel® Core ™ i5-6500 @ 3,20 ГГц × 4) выглядят следующим образом:

Встроенный вызов mt_rand () занял 3,5181620121002 секунд. Вызов пользовательской функции занял 7,2354700565338 секунд.

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

Если ваш PHP-скрипт медленный, то почти наверняка есть вероятность, что он будет связан с вводом-выводом или плохим выбором алгоритма, а не с накладными расходами при вызове функции.Подключение к базе данных, выполнение запроса CURL, запись в файл или даже просто отображение на стандартный вывод - все это на несколько порядков дороже, чем вызов пользовательской функции.Если вы мне не верите, попросите mt_rand и myfunc повторить вывод и посмотреть, насколько медленнее работает скрипт!

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

4 голосов
/ 25 июля 2013

Я бы сказал, что это не так.Вы вообще не тестируете вызов функции.Вы проверяете разницу между проверкой низкого уровня за пределами границ (isset) и обходом строки для подсчета количества байтов (strlen).

Я не могу найти какую-либо информацию, специфичную для PHP,но в strlen обычно реализовано что-то вроде (включая накладные расходы на вызовы функций):

$sp += 128;
$str->address = 345;
$i = 0;
while ($str[$i] != 0) {
    $i++;
}
return $i < $length;

Обычно проверка вне границ будет реализована примерно так:

return $str->length < $length;

Первый цикл - цикл.Второй - простой тест.

3 голосов
/ 01 ноября 2016

Я думаю, что ответ богатого Ремера на самом деле довольно точный. Вы сравниваете яблоки с апельсинами с вашим оригинальным примером. Попробуйте вместо этого:

<?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% разницы в использовании вызова функции из нашего вывода: </p>

with function call: 2.4601600170135 
without function call: 2.4477159976959

Конечно, все зависит от того, что ваша функция делает и , что вы считаете дорогим . Если nothing() вернул $x*2 (и мы заменили нефункциональный вызов $x = $i.$x на $x = $i.($x*2), мы увидели бы ~ 4% потерь при использовании вызова функции.

3 голосов
/ 28 мая 2014

Функциональные вызовы дороги по причине, прекрасно описанной @Artefacto выше. Обратите внимание, что их производительность напрямую связана с количеством задействованных параметров / аргументов. Это одна из областей, на которую я обратил пристальное внимание при разработке собственной платформы приложений. Когда есть смысл и возможно избежать вызова функции, я делаю.

Одним из таких примеров является недавняя замена вызовов is_numeric() и is_integer() простым логическим тестом в моем коде, особенно когда можно выполнить несколько вызовов этих функций. Хотя некоторые могут подумать, что такая оптимизация не имеет смысла, я заметил значительное улучшение скорости отклика моих веб-сайтов благодаря такой работе по оптимизации.

Следующий быстрый тест будет ИСТИНА для числа и ЛОЖЬ для всего остального.

if ($x == '0'.$x) { ... }

Гораздо быстрее, чем is_numeric() и is_integer(). Опять же, только когда это имеет смысл, вполне допустимо использовать некоторые оптимизации.

...