Подтвердить что ты не робот

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

4b9b3361

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

Если ваш 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% при использовании вызова функции.