FOR 与 FOREACH 在 PHP 中的性能

2022-08-30 06:52:14

首先,我知道在90%的应用程序中,性能差异是完全无关紧要的,但我只需要知道哪个是更快的构造。那和...

目前网上关于他们的信息令人困惑。很多人说 foreach 不好,但从技术上讲,它应该更快,因为它应该简化使用迭代器编写数组遍历的过程。迭代器,它们再次被认为更快,但在PHP中显然也非常慢(或者这不是PHP的东西吗?我说的是数组函数:next() prev() reset() reset() 等,如果它们甚至是函数,而不是看起来像函数的PHP语言特性之一。

缩小一点范围:我对以超过1的步骤遍历数组不感兴趣(也没有负步骤,即反向迭代)。我对任意点之间的遍历也不感兴趣,只有0到长度。我也没有看到操作超过1000个键的数组定期发生,但我确实看到数组在应用程序的逻辑中被遍历多次!同样对于操作,很大程度上只是字符串操作和回声。

以下是一些参考网站:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

我到处听到的:

  • foreach很慢,因此 /更快forwhile
  • PHPs 复制它循环访问的数组;为了加快速度,您需要使用引用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结果的能力比所有内容都好,尽管只是“你正在使用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(非参考版本)。


答案 1

我个人的观点是在上下文中使用有意义的东西。就我个人而言,我几乎从不用于数组遍历。我将其用于其他类型的迭代,但太容易了...在大多数情况下,时间差将很小。forforeach

需要注意的重要事情是:

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上)好像引用明显更快...

编辑:使用 PHP 8.0,我得到了以下内容:

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

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


答案 2

我不确定这是否如此令人惊讶。大多数用PHP编码的人都不太熟悉PHP在裸机上实际在做什么。我将陈述一些事情,这些事情在大部分时间都是正确的:

  1. 如果不修改变量,则 PHP 中的按值速度更快。这是因为无论如何,它都是参考计数的,而按值使它不那么重要。它知道你修改ZVAL(PHP的大多数类型的内部数据结构)的那一秒,它将不得不以一种直接的方式将其分解(复制它并忘记其他ZVAL)。但是你永远不会修改它,所以这并不重要。引用使这变得更加复杂,更多的簿记必须知道在修改变量时该怎么做。因此,如果你是只读的,矛盾的是,最好不要用&.我知道,这是违反直觉的,但这也是事实。

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

  3. 使用 for 循环 “for ($i=0;$i <计数($x);$i++)很慢,因为count(),并且缺乏PHP的能力(或者实际上是任何解释型语言)在解析时评估是否有任何东西修改了数组。这样可以防止它评估计数一次。

  4. 但即使你用“$c=count($x))修复它;对于($i=0;$i<$c;$i++),$i<$c充其量是一堆Zend操作码,$i++也是如此。在 100000 次迭代的过程中,这可能很重要。Foreach在原生层面上知道该怎么做。无需 PHP 操作码即可测试“我是否在此数组的末尾”条件。

  5. 那么老派的“while(list(”)呢?好吧,使用athing(),current()等都将涉及至少1个函数调用,这并不慢,但不是免费的。是的,这些又是PHP操作码!因此,虽然+列表+每个也有其成本。

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

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