FORVS FOREACH 在 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比较快
  • 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。就我个人而言,我仍然不明白 echo 是一个随机执行时间的函数,或者任何其他函数在某种程度上有什么不同——那个脚本能够比任何东西都更好地产生完全相同的 foreach 结果,这很难解释,尽管只是“你在使用 echo”(好吧,我应该使用什么)。然而,我承认应该用更好的方法来进行测试; 尽管我没有想到一个理想的折衷方案。
  2. “ PHP foreach 参考真的有影响吗?我的意思是,如果你通过引用,它为什么不能复制它呢?”Ircmaxell 显示,是的,在大多数情况下,进一步的测试似乎证明引用应该更快——尽管根据我上面的代码片段,大多数情况下肯定不意味着全部。我承认这个问题可能太不直观,以至于在这样的层面上无法进行讨论,并且需要一些极端的东西,比如反编译,以确定哪种方式更适合每种情况。
  3. Foreach 语句的等价迭代器代码是什么; 我在网上看到过一些,但是每次我测试它们的时间都差得很远; 我也测试过一些简单的迭代器结构,但是似乎从来没有得到像样的结果—— PHP 中的数组迭代器真的很糟糕吗?”Ircmaxell 提供了下面的答案; 尽管代码可能只对 PHP version > = 5有效
  4. 有没有比 FOR/FOREACH (和 WHILE)更快的方法/方法/结构来迭代一个数组谢谢戈登的回答。在 PHP5中使用新的数据类型可以提高性能或者提高内存(根据您的情况,这两者都是可取的)。虽然许多新类型的数组在速度方面似乎并不比 array ()好,但 splpriorityqueue 和 splobjectStorage 似乎确实要快得多。链接由 Gordon 提供: http://matthewturland.com/2010/05/20/new-spl-Features-in-php-5-3/”rel = “ nofollow noReferrer”> http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

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

126705 次浏览

我个人的观点是在上下文中使用有意义的东西。就个人而言,我几乎从不使用 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

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

在基准测试(尤其是 phpbench. com)中需要注意的一件事是,尽管数据是可靠的,但测试并非如此。Phpbench 上的很多测试都是无关紧要的,它们滥用 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

我不觉得这有什么好惊讶的。大多数用 PHP 编写代码的人并不精通 PHP 实际上在做什么。我将陈述一些事情,大多数情况下是正确的:

  1. 如果不修改变量,by-value 在 PHP 中会更快。这是因为它的引用无论如何都要计数,而 by-value 使它要做的事情更少。它知道,一旦修改了 ZVAL (大多数类型的 PHP 内部数据结构) ,就必须以一种简单的方式将其分离(复制它并忘记其他 ZVAL)。但你从不修改它,所以没关系。当你修改变量的时候,引用使得记账变得更加复杂,因为它必须对 知道该怎么做做更多的记账工作。因此,如果您是只读的,矛盾的是,最好不要指出 & 。我知道,这有违直觉,但这也是事实。

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

  3. 使用 for 循环“ for ($i = 0; $i < count ($x) ; $i + +)比较慢,因为 count ()和 PHP 没有能力(或者说没有任何直译语言)在解析时判断是否有任何东西修改了数组。这可以防止它计算一次计数。

  4. 但是,即使您使用“ $c = count ($x) ; for ($i = 0; $i < $c; $i + +)”修复它,$i < $c 充其量只是一堆 Zend 操作码,$i + + 也是如此。在100000次迭代的过程中,这可能很重要。Foreach 在本地级别知道该做什么。不需要 PHP 操作码来测试“ am I at the end of this array”条件。

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

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

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

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

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

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