簡體   English   中英

PHP 中 FOR 與 FOREACH 的性能

[英]Performance of FOR vs FOREACH in PHP

首先,我知道在 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更快
  • PHPs foreach復制它迭代的數組; 為了使它更快,您需要使用引用
  • 代碼如下: $key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    $key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    $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

簡而言之:

  • foreachforeach with reference 快
  • foreachfor
  • foreachfor哈希表快

有人可以解釋一下嗎?

  1. 難道我做錯了什么?
  2. PHP foreach 參考的東西真的有所作為嗎? 我的意思是如果你通過引用傳遞它為什么不復制它?
  3. foreach 語句的等效迭代器代碼是什么? 我在網上看到過一些,但每次我測試它們時,時間都相差甚遠; 我還測試了一些簡單的迭代器構造,但似乎從來沒有得到過像樣的結果——PHP 中的數組迭代器是不是很糟糕?
  4. 是否有更快的方法/方法/結構來迭代除 FOR/FOREACH(和 WHILE)以外的數組?

PHP 版本 5.3.0


編輯:回答在這里人們的幫助下,我能夠拼湊出所有問題的答案。 我將在這里總結它們:

  1. “難道我做錯了什么?” 共識似乎是:是的,我不能在基准測試中使用 echo。 就我個人而言,我仍然不明白 echo 是某個具有隨機執行時間的函數,或者任何其他函數有何不同——以及該腳本生成完全相同的 foreach 結果的能力比一切都好解釋雖然只是“你正在使用回聲”(我應該使用什么)。 但是,我承認測試應該用更好的方法來完成。 雖然沒有想到理想的妥協。
  2. “PHP foreach 引用的東西真的有所作為嗎?我的意思是,如果您通過引用傳遞,它為什么不復制它?” ircmaxell 表明是的,進一步的測試似乎證明在大多數情況下參考應該更快——盡管鑒於我上面的代碼片段,絕對不是全部。 我接受這個問題可能太不直觀,無法在這樣的級別上打擾,並且需要一些極端的東西,例如反編譯來實際確定哪種情況更適合每種情況。
  3. “foreach 語句的等效迭代器代碼是什么;我在網上看到了一些,但每次我測試它們時,時間都相差甚遠;我還測試了一些簡單的迭代器結構,但似乎從未得到過像樣的結果-- PHP 中的數組迭代器很糟糕嗎?” ircmaxell 在下面提供了答案; 雖然代碼可能只對 PHP 版本 >= 5 有效
  4. “是否有更快的方法/方法/結構來迭代除 FOR/FOREACH(和 WHILE)以外的數組?” 感謝戈登的回答。 在 PHP5 中使用新的數據類型應該可以提高性能或提高內存(根據您的情況,這兩者都可能是可取的)。 雖然在速度方面,許多新類型的數組似乎並不比 array() 好,但 splpriorityqueue 和 splobjectstorage 似乎要快得多。 Gordon 提供的鏈接: http : //matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

感謝所有試圖提供幫助的人。

對於任何簡單的遍歷,我可能會堅持使用 foreach(非參考版本)。

我個人的意見是使用上下文中有意義的內容。 個人而言,我幾乎從來不使用for數組遍歷。 我將它用於其他類型的迭代,但foreach太簡單了......在大多數情況下,時間差異將是最小的。

需要注意的大事是:

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

這是一個昂貴的循環,因為它在每次迭代時調用計數。 只要你不這樣做,我認為這並不重要......

至於引用有所不同,PHP使用copy-on-write,所以如果不寫入數組,循環時開銷相對較小。 但是,如果您開始修改數組中的數組,那么您將開始看到它們之間的差異(因為需要復制整個數組,而引用只能修改內聯)...

對於迭代器, 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 上)好像引用明顯更快......

編輯:使用 PHP 8.0 我得到以下信息:

Completed in 0.0005030632019043 Seconds
Completed in 0.00066304206848145 Seconds
Completed in 0.00016379356384277 Seconds
Completed in 0.00056815147399902 Seconds

多次重復此測試,排名結果一致。

我不確定這是否如此令人驚訝。 大多數使用 PHP 編碼的人並不精通 PHP 在裸機上實際執行的操作。 我將陳述一些事情,這在大多數情況下都是正確的:

  1. 如果您不修改變量,PHP 中的按值會更快。 這是因為無論如何它都是引用計數的,而按值則讓它少做。 它知道您修改 ZVAL(大多數類型的 PHP 內部數據結構)的那一刻,它必須以直接的方式將其斷開(復制它並忘記其他 ZVAL)。 但是你永遠不會修改它,所以沒關系。 引用使這變得更加復雜,因為它必須做更多的簿記才能知道在修改變量時要做什么 所以如果你是只讀的,矛盾的是最好不要用 & 來解決這個問題。 我知道,這是違反直覺的,但這也是事實。

  2. Foreach 並不慢。 對於簡單的迭代,它測試的條件——“我在這個數組的末尾”——是使用本機代碼完成的,而不是 PHP 操作碼。 即使它是 APC 緩存的操作碼,它仍然比在裸機上完成的一堆本機操作慢。

  3. 使用 for 循環“for ($i=0; $i < count($x); $i++) 很慢,因為 count(),並且缺乏 PHP 的能力(或實際上任何解釋語言)在解析時進行評估time 是否有任何修改數組。這會阻止它評估一次計數。

  4. 但即使你用 "$c=count($x); for ($i=0; $i<$c; $i++) 修復它, $i<$c 充其量也是一堆 Zend 操作碼,就像這樣$i++。在 100000 次迭代的過程中,這很重要。Foreach 在本機級別知道要做什么。不需要 PHP 操作碼來測試“我在這個數組的末尾”條件。

  5. 老派的“while(list(”) 怎么樣?嗯,使用 each()、current() 等都會涉及至少 1 個函數調用,這並不慢,但不是免費的。是的,那些又是 PHP 操作碼!所以 while + list + each 也有它的成本。

由於這些原因,foreach 是簡單迭代的最佳選擇,這是可以理解的。

而且不要忘記,它也是最容易閱讀的,所以它是雙贏的。

在基准測試(尤其是 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

現在是 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

正如你所看到的,進化是瘋狂的,比 2012 年報告的速度快了大約560 倍

在我的機器和服務器上,經過我無數次的實驗,循環的基礎知識是最快的。 使用嵌套循環( $i $j $k ..)

它在使用上也是最靈活的,在我看來具有更好的可讀性。

我想但我不確定: for循環需要兩個操作來檢查和遞增值。 foreach將數據加載到內存中,然后它會迭代每個值。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM