【问题标题】:Performance of FOR vs FOREACH in PHPPHP 中 FOR 与 FOREACH 的性能对比
【发布时间】:2011-03-26 16:53:17
【问题描述】:

首先,我知道在 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); <br>for ($i=0; $i &lt; $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。就个人而言,我仍然看不到 echo 是如何具有随机执行时间的某个函数,或者任何其他函数有何不同——而且该脚本生成与 foreach 完全相同的结果的能力比一切都好虽然只是解释“你正在使用回声”(我应该一直使用什么)。但是,我承认测试应该用更好的方法来完成。虽然没有想到一个理想的折衷方案。
  2. “PHP foreach 引用的东西真的有影响吗?我的意思是如果你通过引用它为什么不复制它?” ircmaxell 表明是的,进一步的测试似乎证明在大多数情况下引用应该更快——尽管考虑到我上面的 sn-p 代码,但绝对不是全部。我接受这个问题可能太不直观,无法在这样的级别上烦恼,并且需要一些极端的东西,例如反编译才能实际确定哪种情况更适合每种情况。
  3. “foreach 语句的等效迭代器代码是什么;我在网上看到了一些,但每次我测试它们时时间都差了;我还测试了一些简单的迭代器构造,但似乎从未得到甚至不错的结果——PHP 中的数组迭代器真的很糟糕吗?” ircmaxell 提供了下面的答案;虽然代码可能只对 PHP 版本 >= 5 有效
  4. “除了 FOR/FOREACH(和 WHILE)之外,还有更快的方法/方法/构造来迭代数组吗?” 感谢 Gordon 的回答。在 PHP5 中使用新的数据类型应该可以提高性能或提高内存(根据您的情况,这两者都可能是可取的)。虽然在速度方面很多新类型的数组似乎并不比 array() 好,但 splpriorityqueue 和 splobjectstorage 似乎确实要快得多。戈登提供的链接:http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

感谢所有试图提供帮助的人。

对于任何简单的遍历,我可能会坚持使用 foreach(非参考版本)。

【问题讨论】:

  • 基准测试规则 2.71:不要重复基准测试。
  • foreach with reference 必须以 for with reference 为基准。你有一个有缺陷的结论。即使在 do-while 循环中,任何引用的使用显然都会比没有引用的情况慢。
  • 由于这是针对 php 5.3 的,您可能还需要考虑测试新的 Spl 数据类型与数组。或者看看这里:matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
  • @ Mchl:我运行了几次,得到了相同的结果——如果 echo 破坏了基准,那么我不应该得到完全随机的结果吗?我也想迭代一些东西并输出它,所以 echo 实际上对我来说真的很重要;如果在回显时 foreach 更快,那么这是我应该使用 foreach 的一大块代码。 @ stillstanding:我所听到的基本上是“foreach 中的参考使(总是)更快,总是参考参考”,这就是我这样测试的原因——我对与其他参考循环的比较并不感兴趣
  • 这些空洞的问题自然应该被禁止。以及那个欺骗性的 phpbench 网站

标签: php arrays performance foreach for-loop


【解决方案1】:

我的个人意见是使用在上下文中有意义的内容。就我个人而言,我几乎从不使用for 进行数组遍历。我将它用于其他类型的迭代,但foreach 太简单了……在大多数情况下,时间差会很小。

需要注意的是:

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

这是一个昂贵的循环,因为它每次迭代都会调用 count 。只要你不这样做,我认为这并不重要......

至于引用有所不同,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 上)好像引用明显更快...

编辑:使用 PHP 8.0 我得到以下结果:

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

多次重复这个测试,排名结果一致。

【讨论】:

  • 你不是说“[未计划]优化是万恶之源”吗? ;) 好吧,他们都做同样的事情,所以与其说是优化不如说是优化:这是“采用更好的标准方式”。还有一些未回答的问题:您说因为它不必复制,但是使用引用不是开销吗? Stillstanding 在我的问题中的评论似乎也不同意您的假设。另外,为什么那里的代码生成速度也较慢以供参考。 5.3.0 中的 foreach 是否更改为将任何 array() 转换为对象(例如 SplFixedArray)?
  • @srcspider:使用基准代码和结果显示参考的编辑答案确实比非参考快得多...
  • @srcspider "the better standard way to adopt." 性能并不是选择采用什么的唯一标准。尤其是在这种牵强附会的情况下。坦率地说,你只是在浪费时间
  • @Col。弹片我同意 100%。在这种特殊情况下,可读性和可维护性在很大程度上胜过性能......我同意选择一个标准并坚持下去,但该标准基于其他更重要的因素......
  • @ircmaxell:快速运行你的脚本似乎证明了你的观点,但我想进一步研究一下;我可能会通过更多测试来编辑我的原始问题,以包括一些新的 5.3 功能。 @Col。弹片:FOR 几乎是通用编程-幼儿园级别,FOREACH 是更简单的语法。就可读性而言,它们似乎处于同等地位。这一切都太低了,我不认为维护是一些高级模式的问题。而且我不认为我在浪费时间,因为这个“基本结构”会占用我要编写的大量代码。 :)
【解决方案2】:

我不确定这是否令人惊讶。大多数使用 PHP 编写代码的人并不熟悉 PHP 在裸机上实际所做的事情。我会陈述几件事情,这在大多数情况下都是正确的:

  1. 如果您不修改变量,则在 PHP 中按值更快。这是因为无论如何它都是引用计数的,并且按值计算它的工作量更少。它知道你修改 ZVAL(PHP 对大多数类型的内部数据结构)的第二次,它必须以一种直接的方式将其断开(复制它并忘记另一个 ZVAL)。但是你从不修改它,所以没关系。当您修改变量时,为了知道要做什么,引用会使事情变得更加复杂。因此,如果您是只读的,自相矛盾的是,最好不要用 & 指出这一点。我知道,这与直觉相反,但也是如此。

  2. Foreach 并不慢。对于简单的迭代,它测试的条件——“我在这个数组的末尾吗”——是使用本机代码完成的,而不是 PHP 操作码。即使是 APC 缓存的操作码,它仍然比在裸机上完成的一堆本机操作要慢。

  3. 使用 for 循环 "for ($i=0; $i

  4. 但即使你用 "$c=count($x); for ($i=0; $i

  5. 那么老派的“while(list(”) 呢?嗯,使用 each()、current() 等都将涉及至少 1 个函数调用,这并不慢,但是不是免费的。是的,这些又是 PHP 操作码!所以 while + list + each 也有其成本。

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

别忘了,它也是最容易阅读的,所以是双赢的。

【讨论】:

  • 这正是我正在寻找的解释,谢谢。
  • 这个答案应该是对标记答案的补充或总结。很高兴我读到它,干得好。
【解决方案3】:

在基准测试(尤其是 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

【讨论】:

    【解决方案4】:

    现在是 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..)

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

    【讨论】:

    • 这里的数学不太对。硬件和软件环境的潜在其他方面的差异没有被考虑在内,这可能导致基准测试之间的加速速度显着提高了 560 倍。为了进行更仔细的比较,您可以尝试的一件事是在同一硬件上的 Docker 中运行两个 PHP 版本。
    • 在 Docker 中对软件版本之间的相对性能进行基准测试通常没有什么是无效的。 11 年后使用完全不同的硬件运行基准测试来比较不同版本的 PHP 的相对速度将导致错误的测量。这些都是客观事实,陈述清楚,没有侮辱,所以没有理由诉诸人身攻击。
    【解决方案5】:

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

    【讨论】:

    • 每个人都有意见,人们来 Stack Overflow 寻找答案。如果您不确定您所说的内容,请检查源代码、文档、谷歌搜索等。
    • 由于性能基于研究和测试,您应该拿出一些证据。请相应地提供您的参考资料。希望你能改进你的答案。
    • 我认为这也取决于服务器的实际负载以及您想要在循环中执行的操作。我认为这还取决于服务器的实际负载以及您想要在循环中执行的操作。我想知道迭代编号数组是否应该更好地使用 foreach 或 for-loop,因此我使用 PHP 7.4 在sandbox.onlinephpfunctions.com 上运行了基准测试。我多次重复运行相同的脚本,每次运行都会给我不同的结果。一次for循环更快,另一次foreach循环更快,另一次它们相等。
    猜你喜欢
    • 2015-07-25
    • 2012-08-21
    • 2011-06-10
    • 1970-01-01
    • 2013-12-25
    • 2010-11-14
    • 1970-01-01
    • 2018-03-14
    相关资源
    最近更新 更多