PHP中FOR与FOREACH的性能


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

简而言之:

  • foreachforeach参考更快
  • foreachfor
  • foreachfor散列表快

有人可以解释吗?

  1. 难道我做错了什么?
  2. PHP foreach参考对象真的有所作为吗?我的意思是,如果您通过引用,为什么不复制它?
  3. foreach语句的等效迭代器代码是什么?我已经在网上看到了一些,但每次测试它们时,时机都会变差。我还测试了一些简单的迭代器构造,但似乎从未获得过像样的结果-PHP中的数组迭代器太糟糕了吗?
  4. 除了FOR / FOREACH(和WHILE)以外,是否有更快的方法/方法/构造来遍历数组?

PHP版本5.3.0


编辑:答案 在这里人们的帮助下,我得以整理出所有问题的答案。我在这里总结一下:

  1. “难道我做错了什么?” 共识似乎是:是的,我不能在基准测试中使用echo。就我个人而言,我仍然看不到回声是具有随机执行时间的某个函数是如何实现的,或者任何其他函数在某种程度上是如何有所不同的-以及该脚本能够产生比所有函数更好的foreach完全相同结果的能力很难解释一下“您正在使用echo”(以及我应该一直使用的东西)。但是,我承认应该用更好的方法来完成测试。尽管没有想到理想的折衷方案。
  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(非参考版本)。


7
基准测试规则2.71:请勿回覆基准测试。
Mchl

1
带有参考的foreach必须标记为带有参考的。您那里的结论有误。显然,即使在do-while循环中,使用引用的速度显然也会比不使用引用的速度慢。
bcosca 2010年

2
由于这是针对php 5.3的,因此您可能还需要考虑测试新的Spl数据类型与数组。或只看这里:matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
戈登

@ Mchl:我跑了几次,得到了相同的结果-如果echo破坏了基准,那我是否应该得到完全随机的结果?我也想迭代一些东西并输出它,所以回声实际上对我来说很重要。如果在回显时foreach更快,那么我应该在foreach中使用大量代码。@ stillstanding:我所听到的基本上是“ foreach中的引用使速度更快(总是),始终使用引用编写”,这就是为什么我要进行这样的测试-我对与其他引用循环进行比较并不感兴趣
srcspider 2010年

2
这些空洞的问题自然应该被禁止。以及欺骗性的phpbench网站
您的常识

Answers:


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
您不是说“ [非计划中的]优化是万恶之源”吗?;)好吧,所有这些人都在做同一件事,因此,它并不是一种优化,而是一种“更好的标准采用方法”。还有一些未解决的问题:您说是因为它不必复制,但是使用引用不是开销吗?我的问题中stillstanding的评论似乎也与您的假设不同。另外,为什么代码的生成速度也较慢。foreach是否在5.3.0中进行了更改,以将任何array()转换为对象(例如SplFixedArray)?
srcspider 2010年

@srcspider:带有基准代码的编辑答案和显示引用的结果确实比非引用要快得多
ircmaxell 2010年

1
@srcspider的"the better standard way to adopt." 性能并不是选择采用哪种标准的唯一标准。特别是在这种牵强附会的情况下。坦白地说,您只是在浪费时间
您的常识2010年

@Col。弹片我同意100%。在这种特殊情况下,可读性和可维护性大大压倒了性能……我同意选择并坚持使用该标准,但是该标准基于其他(更重要的因素)...
ircmaxell,2010年

@ircmaxell:快速运行脚本似乎可以证明您的观点,但我想进一步研究一下;我可能会通过更多测试来编辑我的原始问题,以包括一些新的5.3功能。@Col。弹片:FOR几乎是通用编程级的,而FOREACH是更简单的语法。就可读性而言,它们似乎是平等的。这些都是低级别的,我认为维护不会像某些高级模式那样成为问题。而且我不认为我在浪费时间,因为这种“基本构造”将占用我编写的许多代码。:)
srcspider

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);对于($ i = 0; $ i <$ c; $ i ++)修复它,$ i <$ c充其量也是一堆Zend操作码。 $ i ++。在100000次迭代过程中,这可能很重要。Foreach在本机级别上知道要做什么,不需要PHP操作码来测试“在此数组末尾的我”。

  5. 老式的“ while(list()”东西呢?那么,使用each(),current()等都将涉及至少1个函数调用,这并不慢,但也不免费。再次是PHP操作码!因此,虽然+ list +每个都有其成本。

由于这些原因,foreach是简单迭代的最佳选择,这是可以理解的。

并且不要忘记,它也是最容易阅读的,所以是双赢的。


正是我所要的解释,谢谢。
硬性设定2015年

该答案实际上应该是标记答案的补充或总结。我很高兴阅读,很好的工作。
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

如您所见,进化是疯狂的,比2012年报告的速度快560倍

在我的机器和服务器上,经过大量的实验,循环的基础知识是最快的。使用嵌套循环($ i $ j $ k ..)甚至更清楚。

在我看来,它也是最灵活的用法,并且具有更好的可读性。


0

我认为但不确定:for循环需要执行两个操作来检查和增加值。foreach将数据加载到内存中,然后将迭代每个值。


7
每个人都有自己的见解,人们来到Stack Overflow寻找答案。如果你不知道的东西,你的状态,检查源代码,文档,做谷歌搜索等
塞巴斯蒂安·F.

由于性能基于研究和测试,因此您应该提供一些证据。请提供相应的参考。希望您可以改善答案。
Marwan Salim '18

我认为这还取决于服务器的实际负载以及您要在循环中执行的操作。我认为这还取决于服务器的实际负载以及您要在循环中执行的操作。我想知道是否要遍历带编号的数组,所以我最好使用foreach-或for-loop,所以我在sandbox.onlinephpfunctions.com上使用PHP 7.4 进行了基准测试。我反复运行相同的脚本多次,每次运行都给我不同的结果。一次for循环更快,另一次foreach循环又一次相等。
亚历山大·贝林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.