Последнее обновление:
О циклах языка PHP: вопросы производительности
Язык PHP позволяет выполнять циклы, как минимум, при помощи следующих команд и конструкций:
for, while, foreach
- "обычные" циклы,
array_walk, array_walk_recursive
- итераторы,
array_map
- функция перебора массива.
Бытует мнение, что надо стараться применять специальные функции, при их наличии в языке, они, мол, работают быстрее, так как написаны на С++. А не "обычные" циклы for, while, foreach
. Посмотрим, однако, насколько это верно. Сделаем тестовое сравнение производительности циклов языка РНР на простом примере. Вот тестовый программный код:
<?php
$num = 10000;
global $m;
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
foreach($a as $k => $v){
$rez = work__($v);
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
$m = $rez[1];
echo '<br/>foreach by key (using named function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
foreach($a as $k => $v){
$f = function() use ($a, $k){
$a[$k] .= 'text';
memory_get_usage();
};
$f();
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>foreach by key (using anonim function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
foreach($a as $k => $v){
$a[$k] .= 'text';
memory_get_usage();
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>foreach by key: <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
foreach($a as &$v){
$rez = work__($v);
}
unset($v);
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
$m = $rez[1];
echo '<br/>foreach by reference (using named function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
$c = count($a);
for($i = 0; $i < $c; $i++){
$rez = work__($a[$i]);
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
$m = $rez[1];
echo '<br/>for (using named function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
unset($rez);
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
$c = count($a);
for($k = 0; $k < $c; $k++){
$f = function() use ($a, $k){
$a[$k] .= 'text';
memory_get_usage();
};
$f();
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>for (using anonim function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
array_walk($a, 'work__');
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>array_walk (using named function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
array_walk($a, function ($v){
$v .= 'text';
$m = memory_get_usage();
});
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>array_walk (using anonim function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
array_walk_recursive($a, 'work__');
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
echo '<br/>array_walk_recursive (using named function): <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
$i = 0;
while ($i < $c){
$rez = work__($a[$i]);
$i++;
}
$time = microtime(1) - $s;
$m = memory_get_usage() - $m;
$m = $rez[1];
echo '<br/>while: <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
$a = range(1, $num);
$s = microtime(1);
$m = memory_get_usage();
$a1 = array_map('work__', $a);
$m = memory_get_usage() - $m;
echo '<br/>array_map: <br />time: '.$time.'<br />memory: '.$m.'bytes<br /><br />';
function work__($v){
$v .= 'text';
$m = memory_get_usage();
return array($v, $m);
}
В рамках этого кода каждая из конструкций, позволяющих осуществлять цикл, делает две простых операции: добавляет к элементу массива строку "text"
и делает замер потребляемой оперативной памяти.
Для сравнения, некоторые циклы выполнены как с именными, так и анонимными функциями.
Вот что получается при работе (тестировалось на РНР 5.3):
- foreach by key (using named function):
time: 0.025254011154175
memory: 1259200bytes
- foreach by key (using anonim function):
time: 6.2507939338684
memory: 576bytes
- foreach by key:
time: 0.010780096054077
memory: 234192bytes
- foreach by reference (using named function):
time: 0.020576000213623
memory: 2125704bytes
- for (using named function):
time: 0.020446062088013
memory: 2125792bytes
- for (using anonim function):
time: 6.4758911132812
memory: -865672bytes
- array_walk (using named function):
time: 0.021581888198853
memory: 0bytes
- array_walk (using anonim function):
time: 0.02426815032959
memory: 0bytes
- array_walk_recursive (using named function):
time: 0.021921873092651
memory: 0bytes
- while:
time: 0.020758867263794
memory: 2125776bytes
- array_map:
time: 0.020758867263794
memory: 3577728bytes
А вот результаты после запуска в РНР 8.0.9 (на том же самом компьютере, но интерпретатор РНР был запущен в виртуальной машине Virtual Box, а в ней - та же самая операционная система Windows 7):
- foreach by key (using named function):
time: 0.0022928714752197
memory: 940208bytes
- foreach by key (using anonim function):
time: 0.40930008888245
memory: 696bytes
- foreach by key:
time: 0.0011470317840576
memory: 392008bytes
- foreach by reference (using named function):
time: 0.0022928714752197
memory: 1789376bytes
- for (using named function):
time: 0.0022931098937988
memory: 1469376bytes
- for (using anonim function):
time: 0.40012788772583
memory: -528440bytes
- array_walk (using named function):
time: 0.0034401416778564
memory: 320000bytes
- array_walk (using anonim function):
time: 0.0022928714752197
memory: 320000bytes
- array_walk_recursive (using named function):
time: 0.0022931098937988
memory: 320000bytes
- while:
time: 0.0011458396911621
memory: 1469376bytes
- array_map:
time: 0.0011458396911621
memory: 4680448bytes
В ходе выполнения измерялись время выполнения и объемы потребляемой оперативной памяти. Что касается объемов памяти, то в некоторых случаях, например, для итераторов, для ее измерения потребовались бы некие специальные приемы, поэтому соответствующие данные не следует принимать во внимание. А вот для всего остального - можно.
Обсуждение результатов
Необходимо сказать сразу, что в более новых версиях PHP результаты могут быть совсем другими. Например, как видно, для PHP 8.0.9 скорость выполнения на порядки выше для всех команд (даже в виртуальной машине!).
PHP 5.3
Судя по полученным типичным временам выполнения, рекордсменом по скорости является цикл foreach, не использующий внутри себя никаких функций.Время его выполнения - всего 0,01078 секунд.
Примерно в два раза дольше выполняется этот цикл, если внутри его тела есть ссылка на именованную функцию. А вот анонимная функция замедляет работу аж в 600 раз.
Практически то же самое можно сказать и в отношении цикла for
. Т.е. использование анонимных функций внутри "обычных" циклов - плохая практика. Объясняется это, видимо, тем, что анонимная функция создается на каждой итерации цикла заново.
Однако, с итераторами ситуация иная. Как видно, различие по скорости итератора array_walk
для именованной и анонимной функций не столь критично и составляет всего в районе 14%. Однако, и для итераторов применение анонимных функций замедляет выполнение скрипта.
Если использовать именованные функции, то, как видно, различие не столь существенно.
В связи с этим возникает закономерный вопрос: зачем же тогда в языке PHP придуманы итераторы типа array_walk
? Неужели только для того, чтобы сделать "красивую" обертку?...
Что же касается функции array_map
, то она сильно разочаровала. Время ее выполнения получилось практически таким же, как и для "обычных" циклов. Что видится слегка странным, ведь она написана на языке С++ и должна, вроде как, работать быстрее в любом случае, чем тело циклов скриптового языка РНР. Однако, что есть, то есть. Возможно, если callback-функция будет посложнее, то array_map
как-то проявит свое преимущество.
Объем потребляемой памяти функции array_map
примерно в два раза выше, чем для обычных циклов. Это вызвано тем, что она создает новый массив $a1
. Занимающий столько же места, сколько исходный массив.
Да, кстати. Расчеты производились в среде Denwer (том самом старом, но добром), в операционной системе Windows 7, Intel Core i5, 8 ГБ оперативной памяти, частота 3,1 ГГц.
PHP 8.0.9
Скорость выполнения выше на порядок. Что, возможно, связано с отсутствием оболочки Denwer, а также с усовершенствованной работой данной, более новой версией РНР.
И вот в ней, что интересно, неплохо проявили себя и цикл while
, а также функция array_map
. Также интересно, что для итераторов array_walk
скорость выполнения получилась выше, наоборот, при использовании анонимной, а не именованной функции. Впрочем, эта разница при очередных запусках иногда практически исчезала.
Выводы
Если подвести итоги результатам расчетов, то получается примерно такая картина (будем считать, что в любом случае необходимо использование функций внутри циклов):
- Если нет особой необходимости, НЕ СЛЕДУЕТ увлекаться написанием многочисленных функций и классов. Если есть возможность, лучше тело функции разместить прямо внутри цикла. Так будет достигнута максимальная скорость выполнения.
- Наиболее скоростным (быстрым) циклом в РНР5.3 является
foreach
со ссылкой, а также циклfor
иwhile
. - Итераторы
array_walk
работают слегка медленнее и для них можно использовать (как ни странно) как именованную, так и анонимную функции. Различие в скорости работы - сравнительно небольшое. - А вот функция
array_map
, похоже бесполезна, если использовать ее именно в качестве разновидности цикла. Другое дело, что она может(?) работать быстрее для некоторых других видов задач. - Анонимные функции в PHP - не слишком хорошая практика. Их применение, видимо, может быть оправдано, если они вызываются всего лишь несколько раз. В любом случае, в циклах их лучше не использовать.
Впрочем, указанные наши выводы - не единственные. Например, в этой статье отмечается, что цикл foreach
и в самом деле является наиболее быстрым среди циклов, однако, итераторы работают на 43% быстрее "обычных" циклов.
А здесь утверждается, что цикл foreach
с получением значения по ссылке работает медленнее, чем foreach
с выборкой ключа. Что противоречит полученным выше результатам.
Вероятно, в каждом конкретном случае, в целях оптимизации производительност и работы сайта следует проводить соответствующее тестирование. Также может быть существенная зависимость от версии PHP.