Производительность FOR против FOREACH в PHP

134

Прежде всего, я понимаю, что в 90% приложений разница в производительности совершенно не важна, но мне просто нужно знать, какая конструкция является более быстрой. Это и ...

Информация, доступная в настоящее время о них в сети, сбивает с толку. Многие люди говорят, что foreach - это плохо, но технически он должен быть быстрее, поскольку предполагает упрощение написания обхода массива с использованием итераторов. Итераторы, которые, как предполагается, будут быстрее, но в PHP также явно очень медленны (или это не PHP?). Я говорю о функциях массива: next () prev () reset () и т. Д., Если это даже функции, а не одна из тех особенностей языка PHP, которые выглядят как функции.

Чтобы немного сузить круг вопросов: мне неинтересно обходить массивы с шагом, превышающим 1 (также нет отрицательных шагов, то есть обратной итерации). Меня также не интересует переход к произвольным точкам и обратно, просто от 0 до длины. Я также не вижу, чтобы манипулирование массивами с более чем 1000 ключами происходило на регулярной основе, но я действительно вижу, как массив проходит несколько раз в логике приложения! Также, что касается операций, в основном только манипуляции со строками и эхо.

Вот несколько справочных сайтов:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Что я слышу везде:

  • foreachмедленный, и, следовательно, for/ whileбыстрее
  • PHP foreachкопирует массив, который выполняет итерацию ; чтобы сделать это быстрее вам нужно использовать ссылки
  • код вроде этого: быстрее, чем$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

Вот моя проблема. Я написал этот тестовый скрипт: http://pastebin.com/1ZgK07US, и сколько бы раз я ни запускал скрипт, я получаю что-то вроде этого:

foreach 1.1438131332397
foreach (using reference) 1.2919359207153
for 1.4262869358063
foreach (hash table) 1.5696921348572
for (hash table) 2.4778981208801

Коротко:

  • foreachбыстрее, чем foreachсо ссылкой
  • foreach быстрее чем for
  • foreachбыстрее, чем forдля хеш-таблицы

Может кто-нибудь объяснить?

  1. Я делаю что-то неправильно?
  2. Действительно ли справочная вещь PHP foreach имеет значение? В смысле, почему бы не скопировать, если пройти по ссылке?
  3. Какой эквивалентный код итератора для оператора foreach; Я видел несколько в сети, но каждый раз, когда я их проверяю, время идет не так; Я также протестировал несколько простых конструкций итераторов, но, похоже, никогда не получил даже достойных результатов - итераторы массивов в PHP просто ужасны?
  4. Есть ли более быстрые способы / методы / конструкции для итерации по массиву, отличному от FOR / FOREACH (и WHILE)?

Версия PHP 5.3.0


Изменить: Ответ. С помощью людей здесь я смог собрать ответы на все вопросы. Я резюмирую их здесь:

  1. "Я делаю что-то неправильно?" Похоже, что консенсус таков: да, я не могу использовать эхо в тестах. Лично я до сих пор не понимаю, как echo - это какая-то функция со случайным временем выполнения или как любая другая функция чем-то отличается - это и способность этого скрипта просто генерировать точно такие же результаты foreach лучше, чем все, что сложно чтобы объяснить, хотя просто «вы используете эхо» (хорошо, что я должен был использовать). Тем не менее, я признаю, что тест нужно проводить с чем-нибудь получше; хотя идеального компромисса в голову не приходит.
  2. «Действительно ли справочная функция PHP foreach имеет значение? Я имею в виду, почему она не копирует ее, если вы передаете ее по ссылке?» ircmaxell показывает, что да, дальнейшее тестирование, похоже, доказывает, что в большинстве случаев ссылка должна быть быстрее - хотя, учитывая мой приведенный выше фрагмент кода, определенно не означает все. Я согласен, что проблема, вероятно, слишком неинтуитивна, чтобы возиться с ней на таком уровне, и потребует чего-то экстремального, например декомпиляции, чтобы определить, что лучше для каждой ситуации.
  3. "Каков эквивалентный код итератора для оператора foreach? Я видел несколько таких в сети, но каждый раз, когда я их тестирую, время идет далеко; я также тестировал несколько простых конструкций итераторов, но, похоже, никогда не получал даже достойных результатов - итераторы массивов в PHP просто ужасны? " ircmaxell предоставил ответ ниже; хотя код может быть действителен только для версии PHP> = 5
  4. «Существуют ли более быстрые способы / методы / конструкции для итерации по массиву, кроме FOR / FOREACH (и WHILE)?» Спасибо Гордону за ответ. Использование новых типов данных в PHP5 должно дать либо повышение производительности, либо увеличение памяти (что может быть желательно в зависимости от вашей ситуации). Хотя с точки зрения скорости многие из новых типов массивов кажутся не лучше, чем array (), splpriorityqueue и splobjectstorage кажутся значительно быстрее. Ссылка предоставлена ​​Гордоном: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Спасибо всем, кто пытался помочь.

Я, скорее всего, буду придерживаться foreach (не справочной версии) для любого простого обхода.

srcspider
источник
7
Правило 2.71 эталонного тестирования: не повторяйте эталонный тест.
Mchl 07
1
foreach со ссылкой должен быть помечен как для ссылки. у вас есть ошибочный вывод. очевидно, что любое использование ссылки будет медленнее, чем без ссылки, даже в цикле do-while.
bcosca,
2
Поскольку это для php 5.3, вы также можете рассмотреть возможность тестирования новых типов данных Spl и массивов. Или просто посмотрите здесь: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Гордон,
@ Mchl: Я запускал его несколько раз и получил те же результаты - если эхо искажает тест, разве я не должен получать совершенно случайные результаты? также я хотел бы повторить что-то и вывести это, поэтому эхо действительно важно для меня; если при эхо-выводе foreach работает быстрее, то это большой кусок кода, в котором я должен использовать foreach. @ stillstanding: то, что я слышу, в основном похоже на «ссылка в foreach делает быстрее (всегда), всегда пишите со ссылкой», поэтому я тестировал именно так - меня не очень интересует сравнение с другими ссылочными циклами
srcspider
2
эти пустые вопросы, естественно, следует запретить. а также этот обманчивый сайт phpbench
Your Common Sense

Ответы:

110

Мое личное мнение - использовать то, что имеет смысл в контексте. Лично я почти никогда не использую forдля обхода массива. Я использую его для других типов итераций, но foreachэто слишком просто ... В большинстве случаев разница во времени будет минимальной.

Главное, на что стоит обратить внимание:

for ($i = 0; $i < count($array); $i++) {

Это дорогой цикл, поскольку он вызывает счетчик на каждой итерации. Пока вы этого не делаете, я не думаю, что это имеет значение ...

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

Что касается итераторов, foreachэто эквивалентно:

$it->rewind();
while ($it->valid()) {
    $key = $it->key();     // If using the $key => $value syntax
    $value = $it->current();

    // Contents of loop in here

    $it->next();
}

Что касается более быстрых способов итерации, это действительно зависит от проблемы. Но мне действительно нужно спросить, почему? Я понимаю, что хочу сделать вещи более эффективными, но я думаю, что вы тратите время на микрооптимизацию. Помните, Premature Optimization Is The Root Of All Evil...

Изменить: на основании комментария я решил провести быстрый тест ...

$a = array();
for ($i = 0; $i < 10000; $i++) {
    $a[] = $i;
}

$start = microtime(true);
foreach ($a as $k => $v) {
    $a[$k] = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {
    $v = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => $v) {}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {}    
echo "Completed in ", microtime(true) - $start, " Seconds\n";

И результаты:

Completed in 0.0073502063751221 Seconds
Completed in 0.0019769668579102 Seconds
Completed in 0.0011849403381348 Seconds
Completed in 0.00111985206604 Seconds

Поэтому, если вы изменяете массив в цикле, использовать ссылки в несколько раз быстрее ...

И накладные расходы только на ссылку на самом деле меньше, чем копирование массива (это на 5.3.2) ... Таким образом, кажется (по крайней мере, на 5.3.2), как будто ссылки значительно быстрее ...

ircmaxell
источник
1
Разве вы не имеете в виду «[незапланированная] оптимизация - корень всех зол»? ;) Дело в том, что все они делают одно и то же, так что это не столько оптимизация, сколько оптимизация: это «лучший стандартный способ принятия». Еще несколько вопросов без ответа: вы говорите, потому что не нужно копировать, но разве использование ссылки не накладные расходы? Комментарий stillstanding в моем вопросе, похоже, также не согласуется с вашими предположениями. Кроме того, почему здесь код работает медленнее. Изменился ли foreach в 5.3.0 для преобразования любого array () в объект (например, SplFixedArray)?
srcspider
@srcspider: Отредактированный ответ с тестовым кодом и результатами, показывающими ссылки, действительно намного быстрее, чем не-ссылки ...
ircmaxell
1
Эффективность @srcspider "the better standard way to adopt." - не единственный критерий выбора того, что следует принять. особенно в таком надуманном случае. Честно говоря, вы просто зря теряете время
Ваш здравый смысл
@Col. Shrapnel Согласен на 100%. В этом конкретном случае удобочитаемость и ремонтопригодность значительно превосходят производительность ... Я согласен с выбором стандарта и его соблюдением, но основываю этот стандарт на других - более важных - факторах ...
ircmaxell
@ircmaxell: быстрый запуск вашего скрипта, кажется, подтверждает вашу точку зрения, но я хочу изучить его немного подробнее; Я мог бы отредактировать свой исходный вопрос, добавив больше тестов, чтобы включить некоторые из новых функций версии 5.3. @Col. Shrapnel: FOR - это почти универсальный программно-детский уровень, FOREACH - более простой синтаксис. Что касается удобочитаемости, то они вроде бы равны. Все это настолько низкий уровень, что я не думаю, что обслуживание является проблемой, как это было бы для некоторого шаблона высокого уровня. И я не думаю, что зря трачу время, поскольку эта «базовая конструкция» потребовала бы большого количества кода, который я бы написал. :)
srcspider 07
54

Не уверен, что это так уж удивительно. Большинство людей, пишущих на PHP, не очень хорошо разбираются в том, что на самом деле делает PHP на «голом железе». Я скажу несколько вещей, которые будут верны в большинстве случаев:

  1. Если вы не изменяете переменную, в PHP будет быстрее работать по значению. Это потому, что ссылки в любом случае подсчитываются, а по значению меньше работы. Он знает, что как только вы измените этот ZVAL (внутренняя структура данных PHP для большинства типов), ему придется разорвать его простым способом (скопировать и забыть о другом ZVAL). Но вы его никогда не изменяете, так что это не имеет значения. Ссылки усложняют это, поскольку требуется больше учета, чтобы знать, что делать, когда вы изменяете переменную. Так что, если вы - только для чтения, как ни парадоксально, лучше не использовать &. Я знаю, это нелогично, но это также правда.

  2. Foreach не медленный. А для простой итерации условие, по которому выполняется проверка - «я в конце этого массива» - выполняется с использованием собственного кода, а не кодов операций PHP. Даже если это кэшированные коды операций APC, он все равно медленнее, чем набор собственных операций, выполняемых на голом железе.

  3. Использование цикла for "for ($ i = 0; $ i <count ($ x); $ i ++) медленно из-за count () и отсутствия способности PHP (или действительно любого интерпретируемого языка) оценивать при синтаксическом анализе время, изменяет ли что-либо массив, что предотвращает однократную оценку счетчика.

  4. Но даже если вы исправите это с помощью "$ c = count ($ x); for ($ i = 0; $ i <$ c; $ i ++)" $ i <$ c в лучшем случае представляет собой набор кодов операций Zend, как и $ i ++. В течение 100000 итераций это может иметь значение. Foreach знает на нативном уровне, что делать. Никаких кодов операций PHP не требуется для проверки условия «Я в конце этого массива».

  5. А как насчет старой школы "while (list ("?) Ну, использование each (), current () и т. Д. Потребует хотя бы одного вызова функции, что не медленно, но не бесплатно. Да, те снова являются кодами операций PHP! Так что while + list + также имеет свои затраты.

По этим причинам foreach по понятным причинам является лучшим вариантом для простой итерации.

И не забывайте, что его легче всего читать, поэтому он беспроигрышный.

Джейми Сирович
источник
Это именно то объяснение, которое я искал, спасибо.
Hardsetting
Этот ответ действительно должен быть дополнением или резюме к отмеченному ответу. Я рад, что прочитал это, хорошая работа.
doz87
30

Одна вещь, на которую следует обратить внимание в тестах (особенно на phpbench.com), заключается в том, что даже если цифры верны, тесты нет. Многие тесты на phpbench.com выполняют тривиальные функции и злоупотребляют способностью PHP кэшировать поиск в массиве для искажения результатов тестов или в случае итерации по массиву фактически не тестируют его в реальных случаях (никто не пишет пустое значение для петли). Я провел свои собственные тесты, которые, как я обнаружил, в значительной степени отражают результаты реального мира, и они всегда показывают, что родной синтаксис итераций языка foreachвыходит на первое место (сюрприз, сюрприз).

//make a nicely random array
$aHash1 = range( 0, 999999 );
$aHash2 = range( 0, 999999 );
shuffle( $aHash1 );
shuffle( $aHash2 );
$aHash = array_combine( $aHash1, $aHash2 );


$start1 = microtime(true);
foreach($aHash as $key=>$val) $aHash[$key]++;
$end1 = microtime(true);

$start2 = microtime(true);
while(list($key) = each($aHash)) $aHash[$key]++;
$end2 = microtime(true);


$start3 = microtime(true);
$key = array_keys($aHash);
$size = sizeOf($key);
for ($i=0; $i<$size; $i++) $aHash[$key[$i]]++;
$end3 = microtime(true);

$start4 = microtime(true);
foreach($aHash as &$val) $val++;
$end4 = microtime(true);

echo "foreach ".($end1 - $start1)."\n"; //foreach 0.947947025299
echo "while ".($end2 - $start2)."\n"; //while 0.847212076187
echo "for ".($end3 - $start3)."\n"; //for 0.439476966858
echo "foreach ref ".($end4 - $start4)."\n"; //foreach ref 0.0886030197144

//For these tests we MUST do an array lookup,
//since that is normally the *point* of iteration
//i'm also calling noop on it so that PHP doesn't
//optimize out the loopup.
function noop( $value ) {}

//Create an array of increasing indexes, w/ random values
$bHash = range( 0, 999999 );
shuffle( $bHash );

$bstart1 = microtime(true);
for($i = 0; $i < 1000000; ++$i) noop( $bHash[$i] );
$bend1 = microtime(true);

$bstart2 = microtime(true);
$i = 0; while($i < 1000000) { noop( $bHash[$i] ); ++$i; }
$bend2 = microtime(true);


$bstart3 = microtime(true);
foreach( $bHash as $value ) { noop( $value ); }
$bend3 = microtime(true);

echo "for ".($bend1 - $bstart1)."\n"; //for 0.397135972977
echo "while ".($bend2 - $bstart2)."\n"; //while 0.364789962769
echo "foreach ".($bend3 - $bstart3)."\n"; //foreach 0.346374034882
Кендалл Хопкинс
источник
3

На дворе 2020 год, и благодаря php 7.4 и opcache многое изменилось. .

Вот тест OP ^, запущенный как unix CLI , без частей echo и html.

Тест запускался локально на обычном компьютере.

php -v

PHP 7.4.6 (cli) (built: May 14 2020 10:02:44) ( NTS )

Измененный скрипт теста:

<?php 
 ## preperations; just a simple environment state

  $test_iterations = 100;
  $test_arr_size = 1000;

  // a shared function that makes use of the loop; this should
  // ensure no funny business is happening to fool the test
  function test($input)
  {
    //echo '<!-- '.trim($input).' -->';
  }

  // for each test we create a array this should avoid any of the
  // arrays internal representation or optimizations from getting
  // in the way.

  // normal array
  $test_arr1 = array();
  $test_arr2 = array();
  $test_arr3 = array();
  // hash tables
  $test_arr4 = array();
  $test_arr5 = array();

  for ($i = 0; $i < $test_arr_size; ++$i)
  {
    mt_srand();
    $hash = md5(mt_rand());
    $key = substr($hash, 0, 5).$i;

    $test_arr1[$i] = $test_arr2[$i] = $test_arr3[$i] = $test_arr4[$key] = $test_arr5[$key]
      = $hash;
  }

  ## foreach

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr1 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach '.(microtime(true) - $start)."\n";  

  ## foreach (using reference)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr2 as &$value)
    {
      test($value);
    }
  }
  echo 'foreach (using reference) '.(microtime(true) - $start)."\n";

  ## for

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $size = count($test_arr3);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr3[$i]);
    }
  }
  echo 'for '.(microtime(true) - $start)."\n";  

  ## foreach (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr4 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach (hash table) '.(microtime(true) - $start)."\n";

  ## for (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $keys = array_keys($test_arr5);
    $size = sizeOf($test_arr5);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr5[$keys[$i]]);
    }
  }
  echo 'for (hash table) '.(microtime(true) - $start)."\n";

Вывод:

foreach 0.0032877922058105
foreach (using reference) 0.0029420852661133
for 0.0025191307067871
foreach (hash table) 0.0035080909729004
for (hash table) 0.0061779022216797

Как видите, эволюция безумна, примерно в 560 раз быстрее. чем сообщалось в 2012 году.

На моих машинах и серверах, после моих многочисленных экспериментов, основы для циклов были самыми быстрыми. Это еще яснее при использовании вложенных циклов ( $ i $ j $ k ..)

Он также является наиболее гибким в использовании и, на мой взгляд, лучше читается.

NVRM
источник
0

Я думаю, но не уверен: forцикл требует двух операций для проверки и увеличения значений. foreachзагружает данные в память, затем перебирает все значения.

Eswer
источник
7
У всех есть свое мнение, люди приходят в Stack Overflow, чтобы найти ответы. Если вы не уверены в своих
Себастьен Ф.
Поскольку производительность основана на исследованиях и тестировании, вы должны предоставить некоторые доказательства. Пожалуйста, предоставьте свои ссылки соответственно. Надеюсь, вы сможете улучшить свой ответ.
Марван Салим
Я думаю, это также зависит от фактической загрузки сервера и того, что вы хотите делать в цикле. Я думаю, это также зависит от фактической загрузки сервера и того, что вы хотите делать в цикле. Я хотел знать, следует ли мне выполнять итерацию по нумерованному массиву, лучше использовать цикл foreach или for, поэтому я провел тест на sandbox.onlinephpfunctions.com с PHP 7.4. Я повторяю один и тот же сценарий несколько раз, и каждый запуск дает разные результаты. Один раз цикл for был быстрее, другой цикл foreach, а в другой раз они были равны.
Александр Белинг,