Продуктивність FOR vs FOREACH в PHP


134

Перш за все, я розумію, що в 90% додатків різниця в продуктивності абсолютно не має значення, але мені просто потрібно знати, яка конструкція швидша. Це і ...

Інформація, наявна на даний момент у них в мережі, є заплутаною. Багато людей кажуть, що передбачити це погано, але технічно це повинно бути швидшим, оскільки, як передбачається, спростити написання обходу масиву за допомогою ітераторів. Ітератори, які, мабуть, швидше, але в 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копіює масив, який він повторює; щоб зробити це швидше, вам потрібно використовувати посилання
  • такий код: швидше, ніж a$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 прореагує на зміну? Я маю на увазі, чому б не скопіювати це, якщо перейти за посиланням?
  3. Який еквівалентний код ітератора для оператора foreach; Я бачив декількох в мережі, але кожен раз, коли я перевіряю їх, час закінчується; Я також протестував кілька простих ітераторних конструкцій, але ніколи не отримує навіть гідних результатів - чи просто ітератори масиву в PHP просто жахливі?
  4. Чи існують більш швидкі способи / методи / конструкції для ітерації через масив, відмінний від FOR / FOREACH (і WHILE)?

PHP версії 5.3.0


Редагувати: Відповісти З допомогою людей, які я тут змогла зібрати відповіді на всі питання. Я підсумую їх тут:

  1. "Хіба я щось не так роблю?" Здається, що консенсус такий: так, я не можу використовувати ехо в тестах. Особисто я все ще не бачу, як відлуння - це якась функція з випадковим часом виконання або як будь-яка інша функція якось інакше відрізняється - це і здатність цього сценарію просто генерувати точно такі самі результати просування, як усе важко пояснити, хоч просто "ти використовуєш ехо" (ну що я маю використовувати). Однак, я визнаю, тест слід робити з чимось кращим; хоча ідеальний компроміс не приходить до тями.
  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/

Дякую всім, хто намагався допомогти.

Я, швидше за все, дотримуватимуся передбачення (нереференційна версія) для будь-якого простого обходу.


7
Правило 2.71 бенчмаркінгу: не перегукуйтесь з еталоном.
Mchl

1
foreach з посиланням повинні бути відмічені для посилання. у вас є хибний висновок. будь-яке використання посилання, очевидно, буде повільніше, ніж без посилання, навіть у циклі, що працює в режимі очікування.
bcosca

2
Оскільки це для php 5.3, ви також можете розглянути тестування нових типів даних Spl проти масивів. Або просто подивіться тут: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon

@ Mchl: Я запускав його кілька разів і отримував однакові результати - якщо відлуння пошкоджує орієнтир, чи не повинен я отримати абсолютно випадкові результати? також я хотів би повторити, хоча щось і вивести це так, що луна насправді дуже важлива для мене; якщо foreach швидше під час лунання, то це великий фрагмент коду, де я повинен використовувати foreach. @ все ще розумію: те, що я чую, це в основному за принципом "посилання в foreach робить швидше (завжди), завжди пишу з посиланням", тому я тестував так - я не дуже зацікавлений порівняно з іншими циклами опор
srcspider

2
ці порожні запитання, природно, слід заборонити. а також на оманливому веб-сайті phpbench
Ваше

Відповіді:


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) так, ніби посилання значно швидше ...


1
Ви не маєте на увазі "оптимізація [незапланованої] - корінь усього зла"? ;) Добре, що всі вони роблять те саме, тож це не стільки оптимізація, скільки це: "це кращий стандартний спосіб прийняття". Також ще кілька питань без відповіді: Ви кажете, тому що це не потрібно копіювати, але чи не використання посилання також накладні? Коментар нерухомості в моєму питанні, здається, не згоден і з вашими припущеннями. Крім того, чому код створюється повільніше для посилання на нього. Чи змінився foreach в 5.3.0 для перетворення будь-якого масиву () в об'єкт (наприклад, SplFixedArray)?
srcspider

@srcspider: Відредагована відповідь з контрольним кодом та результатами, що показують посилання, дійсно набагато швидше, ніж не посилання ...
ircmaxell

1
@srcspider "the better standard way to adopt." продуктивність - не єдиний критерій для вибору того, що прийняти. особливо в такому надуманому випадку. Відверто кажучи, ви просто витрачаєте свій час
ваш здоровий глузд

@Col. Шрапнель Я згоден на 100%. Читальність та ремонтопридатність козирних показників з великим запасом у цьому конкретному випадку ... Я згоден про вибір стандарту та дотримання його, але базувати цей стандарт на інших - більш важливих - факторах ...
ircmaxell

@ircmaxell: швидкий запуск вашого сценарію, здається, підтверджує вашу думку, але я хочу переглянути це трохи далі; Я можу відредагувати своє первісне запитання ще більше тесту, включаючи деякі нові 5.3 функції. @Col. Шрапнель: FOR - це майже універсальний програмово-дитячий рівень, FOREACH - простіший синтаксис. Що стосується читабельності, то, здається, вони знаходяться на рівній основі. Це все настільки низький рівень, я не вважаю, що обслуговування - це проблема, як це було б для певного рівня високого рівня. І я не думаю, що я витрачаю свій час, оскільки ця "основна конструкція" складе багато коду, який я б написав. :)
srcspider

54

Я не впевнений, що це так дивно. Більшість людей, які кодують PHP, недостатньо розбираються в тому, що PHP насправді робить на голому металі. Я зазначу кілька речей, які будуть правдою більшість часу:

  1. Якщо ви не змінюєте змінну, значення PHP швидше в PHP. Це тому, що в будь-якому випадку його посилання враховується, і побічна вартість дає йому менше робити. Він знає, що ти зміниш, що ZVAL (внутрішня структура даних PHP для більшості типів), їй доведеться розірвати її прямо (скопіювати та забути про інший ZVAL). Але ви ніколи його не змінюєте, тому це не має значення. Посилання роблять це складнішим з більш бухгалтерським обліком, який потрібно робити, щоб знати, що робити, коли ви змінюєте змінну. Тож, якщо ви читаєте лише для читання, парадоксально, що краще не зважати на &. Я знаю, це протилежне інтуїтивному, але це теж правда.

  2. Foreach не повільний. А для простої ітерації умова, яку він перевіряє, - "я в кінці цього масиву", виконується за допомогою власного коду, а не PHP-коду. Навіть якщо це опціони, кешовані APC, це все-таки повільніше, ніж купа нативної операції, виконаної на голому металі.

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

  4. Але навіть коли ви виправите це за допомогою "$ c = count ($ x); для ($ i = 0; $ i <$ c; $ i ++), $ i <$ c - це в кращому випадку купа Zend-кодів, як це $ i ++. Під час 100000 ітерацій це може мати значення. Foreach знає на рідному рівні, що робити. Жодних PHP-кодів не потрібно, щоб перевірити стан "я в кінці цього масиву".

  5. Як щодо старої школи "while (список (" речі? Ну, використовуючи кожний (), current () тощо), буде задіяно принаймні 1 виклик функції, який не повільний, але не безкоштовний. Так, ці знову є PHP-кодами! Тож у той час як + list + кожен також має свої витрати.

З цієї причини пропонування, зрозуміло, найкращий варіант для простої ітерації.

І не забувайте, це також найпростіше читати, тож це безпрограшно.


Це саме пояснення, яке я шукав, дякую.
хардсетінг

Ця відповідь дійсно має бути доповненням або підсумком до позначеної відповіді. Я радий, що я її прочитав, хороша робота.
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 , без частин ехо та 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 ..)

Він також є найбільш гнучким у використанні та має кращу читабельність з мого погляду.


0

Я думаю, але я не впевнений: forцикл виконує дві операції для перевірки та збільшення значень. foreachзавантажує дані в пам'ять, тоді вона повторює всі значення.


7
У кожного є думка, люди приходять до Stack Overflow, щоб знайти відповіді. Якщо ви не впевнені в тому, що ви заявляєте, перевірте вихідний код, документацію, зробіть пошук в Google і т. Д.
Sebastien F.

Оскільки результативність ґрунтується на дослідженні та тестуванні, вам слід ознайомитися з деякими доказами. Будь ласка, надайте свої довідки відповідно. Сподіваємось, ви зможете покращити свою відповідь.
Марван Салим

Я думаю, це також залежить від фактичного навантаження сервера і того, що ви хочете робити в циклі. Я думаю, це також залежить від фактичного навантаження сервера і того, що ви хочете робити в циклі. Мені хотілося дізнатися, чи слід повторювати численний масив, і я краще використовувати foreach - або for-loop, тому я запустив орієнтир на sandbox.onlinephpfunctions.com з PHP 7.4. Я повторно запускаю один і той самий сценарій багато разів, і кожен запуск дає мені різний результат. Один раз цикл for-циклу був швидшим, інший раз циклом foreach, а іншим разом - рівним.
Олександр Белінг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.