Номер телефона

Последнее обновление:

О циклах языка 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):

  1. foreach by key (using named function):
    time: 0.025254011154175
    memory: 1259200bytes
     
  2. foreach by key (using anonim function):
    time: 6.2507939338684
    memory: 576bytes
     
  3. foreach by key:
    time: 0.010780096054077
    memory: 234192bytes
     
  4. foreach by reference (using named function):
    time: 0.020576000213623
    memory: 2125704bytes
     
  5. for (using named function):
    time: 0.020446062088013
    memory: 2125792bytes
     
  6. for (using anonim function):
    time: 6.4758911132812
    memory: -865672bytes
     
  7. array_walk (using named function):
    time: 0.021581888198853
    memory: 0bytes
     
  8. array_walk (using anonim function):
    time: 0.02426815032959
    memory: 0bytes
     
  9. array_walk_recursive (using named function):
    time: 0.021921873092651
    memory: 0bytes
     
  10. while:
    time: 0.020758867263794
    memory: 2125776bytes
     
  11. array_map:
    time: 0.020758867263794
    memory: 3577728bytes 

А вот результаты после запуска в РНР 8.0.9 (на том же самом компьютере, но интерпретатор РНР был запущен в виртуальной машине Virtual Box, а в ней - та же самая операционная система Windows 7):

  1. foreach by key (using named function):
    time: 0.0022928714752197
    memory: 940208bytes
     
  2. foreach by key (using anonim function):
    time: 0.40930008888245
    memory: 696bytes
     
  3. foreach by key:
    time: 0.0011470317840576
    memory: 392008bytes
     
  4. foreach by reference (using named function):
    time: 0.0022928714752197
    memory: 1789376bytes
     
  5. for (using named function):
    time: 0.0022931098937988
    memory: 1469376bytes
     
  6. for (using anonim function):
    time: 0.40012788772583
    memory: -528440bytes
     
  7. array_walk (using named function):
    time: 0.0034401416778564
    memory: 320000bytes
     
  8. array_walk (using anonim function):
    time: 0.0022928714752197
    memory: 320000bytes
     
  9. array_walk_recursive (using named function):
    time: 0.0022931098937988
    memory: 320000bytes
     
  10. while:
    time: 0.0011458396911621
    memory: 1469376bytes
     
  11. 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 скорость выполнения получилась выше, наоборот, при использовании анонимной, а не именованной функции. Впрочем, эта разница при очередных запусках иногда практически исчезала.

Выводы

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

  1. Если нет особой необходимости, НЕ СЛЕДУЕТ увлекаться написанием многочисленных функций и классов. Если есть возможность, лучше тело функции разместить прямо внутри цикла. Так будет достигнута максимальная скорость выполнения.
  2. Наиболее скоростным (быстрым) циклом в РНР5.3 является foreach со ссылкой, а также цикл for и while.
  3. Итераторы array_walk работают слегка медленнее и для них можно использовать (как ни странно) как именованную, так и анонимную функции. Различие в скорости работы - сравнительно небольшое.
  4. А вот функция array_map, похоже бесполезна, если использовать ее именно в качестве разновидности цикла. Другое дело, что она может(?) работать быстрее для некоторых других видов задач.
  5. Анонимные функции в PHP - не слишком хорошая практика. Их применение, видимо, может быть оправдано, если они вызываются всего лишь несколько раз. В любом случае, в циклах их лучше не использовать.

Впрочем, указанные наши выводы - не единственные. Например, в этой статье отмечается, что цикл foreach и в самом деле является наиболее быстрым среди циклов, однако, итераторы работают на 43% быстрее "обычных" циклов.

А здесь утверждается, что цикл foreach с получением значения по ссылке работает медленнее, чем foreach с выборкой ключа. Что противоречит полученным выше результатам.

Вероятно, в каждом конкретном случае, в целях оптимизации производительност и работы сайта следует проводить соответствующее тестирование. Также может быть существенная зависимость от версии PHP.


Комментарии:
Всего комментариев:0
Пожалуйста, не забудьте ознакомиться с правилами оставления комментариев.



Подписаться на комментарии на этой странице

Мы можем выполнить

Другие услуги
Интересная и полезная
информация
НАПИШИТЕ НАМ
Яндекс.Метрика
Номер телефона
© Copyright Все права защищены 2013-2024 Научный консалтинг