Php foreach, почему быстро использовать пропуск по ссылке массива?

Ниже приведен тест петли php foreach большого массива, я думал, что если $v не изменится, реальная копия не произойдет из-за копирования при записи, но почему он быстро проходит по ссылке?

Код 1:

function test1($a){
  $c = 0;
  foreach($a as $v){ if($v=='xxxxx') ++$c; }
}

function test2(&$a){
  $c = 0;
  foreach($a as $v){ if($v=='xxxxx') ++$c; }
}

$x = array_fill(0, 100000, 'xxxxx');

$begin = microtime(true);
test1($x);
$end1 = microtime(true);
test2($x);
$end2 = microtime(true);

echo $end1 - $begin . "\n";   //0.03320002555847
echo $end2 - $end1;           //0.02147388458252

Но на этот раз использование pass by reference медленное.

Код 2:

function test1($a){
  $cnt = count($a); $c = 0;
  for($i=0; $i<$cnt; ++$i)
    if($a[$i]=='xxxxx') ++$c;
}
function test2(&$a){
  $cnt = count($a); $c = 0;
  for($i=0; $i<$cnt; ++$i)
    if($a[$i]=='xxxxx') ++$c;
}
$x = array_fill(0, 100000, 'xxxxx');

$begin = microtime(true);
test1($x);
$end1 = microtime(true);
test2($x);
$end2 = microtime(true);

echo $end1 - $begin . "\n";   //0.024326801300049
echo $end2 - $end1;           //0.037616014480591

Может кто-нибудь объяснить, почему передача по ссылке выполняется быстрее в коде1, но медленнее в коде2?

Edit: С кодом 2 значение count($a) делает основное различие, поэтому время цикла занимает почти одно и то же.

Ответы

Ответ 1

Я думал, что если $v не изменится [foreach($a as $v)], реальная копия не произойдет из-за копирования при записи, но почему она быстро проходит по ссылке?

Влияние не на $v, а на $a - огромный массив. Вы либо передаете его как значение, либо как ссылку на функцию. Внутри функции это значение (test1) или reference (test2).

У вас есть два кода (код 1 и код 2).

Код 1: Использует foreach. С foreach у вас есть два варианта: итерация по значению или ссылке (Пример). Когда вы перебираете значение, итерация выполняется на копии значения. Если вы перебираете ссылку, копия не выполняется.

Как вы используете ссылку в test2, это быстрее. Значения не нужно копировать. Но в test1 вы передаете массив как значение, массив будет скопирован.

Код 2: Использует for. Ибо ничего действительно здесь нет. В обоих случаях. Вы получаете доступ к переменной и читаете значение из массива. Это почти одинаково независимо от того, является ли это ссылкой или копией (благодаря копированию на оптимизацию записи в PHP).

Теперь вы можете задаться вопросом, почему существует различие в коде 2. Разница не связана с for, а из-за count. Если вы передаете ссылку на count, PHP внутренне создает копию, потому что ей count нужна копия, а не ссылка.

Читайте также: Не используйте ссылки PHP Johannes Schlüter


Я собрал также набор тестов. Но я более конкретно ставил код в тестовые функции.

  • Бланк - Какая разница в вызове функции?
  • Count - имеет значение count?
  • Для - Что происходит только с for (не count)?
  • Foreach - Just foreach - даже нарушение первого элемента.

Каждый тест состоит из двух версий, один из которых называется _copy (передающий массив как копия в функцию), а один - _ref (передача массива в качестве ссылки).

Не всегда, что эти микро-тесты говорят вам правду, но если вы можете выделить определенные моменты, вы вполне можете сделать обоснованное предположение, например, что не for, а count оказало влияние

function blank_copy($a){
}
function blank_ref(&$a){
}
function foreach_copy($a){
    foreach($a as $v) break;
}
function foreach_ref(&$a){
    foreach($a as $v) break;
}
function count_copy($a){
  $cnt = count($a);
}
function count_ref(&$a){
  $cnt = count($a);
}
function for_copy($a){
    for($i=0;$i<100000;$i++)
        $a[$i];
}
function for_ref(&$a){
    for($i=0;$i<100000;$i++)
        $a[$i];
}

$tests = array('blank_copy', 'blank_ref', 'foreach_copy', 'foreach_ref', 'count_copy', 'count_ref', 'for_copy', 'for_ref');


$x = array_fill(0, 100000, 'xxxxx');
$count = count($x);
$runs = 10;

ob_start();

for($i=0;$i<10;$i++)
{
    shuffle($tests);
    foreach($tests as $test)
    {
        $begin = microtime(true);
        for($r=0;$r<$runs;$r++)
            $test($x);
        $end = microtime(true);
        $result = $end - $begin;
        printf("* %'.-16s: %f\n", $test, $result);
    }
}

$buffer = explode("\n", ob_get_clean());
sort($buffer);
echo implode("\n", $buffer);

Вывод:

* blank_copy......: 0.000011
* blank_copy......: 0.000011
* blank_copy......: 0.000012
* blank_copy......: 0.000012
* blank_copy......: 0.000012
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000015
* blank_copy......: 0.000020
* blank_ref.......: 0.000012
* blank_ref.......: 0.000012
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000014
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* blank_ref.......: 0.000015
* count_copy......: 0.000020
* count_copy......: 0.000022
* count_copy......: 0.000022
* count_copy......: 0.000023
* count_copy......: 0.000024
* count_copy......: 0.000025
* count_copy......: 0.000025
* count_copy......: 0.000025
* count_copy......: 0.000026
* count_copy......: 0.000031
* count_ref.......: 0.113634
* count_ref.......: 0.114165
* count_ref.......: 0.114390
* count_ref.......: 0.114878
* count_ref.......: 0.114923
* count_ref.......: 0.115106
* count_ref.......: 0.116698
* count_ref.......: 0.118077
* count_ref.......: 0.118197
* count_ref.......: 0.123201
* for_copy........: 0.190837
* for_copy........: 0.191883
* for_copy........: 0.193080
* for_copy........: 0.194947
* for_copy........: 0.195045
* for_copy........: 0.195944
* for_copy........: 0.198314
* for_copy........: 0.198878
* for_copy........: 0.200016
* for_copy........: 0.227953
* for_ref.........: 0.191918
* for_ref.........: 0.194227
* for_ref.........: 0.195952
* for_ref.........: 0.196045
* for_ref.........: 0.197392
* for_ref.........: 0.197730
* for_ref.........: 0.201936
* for_ref.........: 0.207102
* for_ref.........: 0.208017
* for_ref.........: 0.217156
* foreach_copy....: 0.111968
* foreach_copy....: 0.113224
* foreach_copy....: 0.113574
* foreach_copy....: 0.113575
* foreach_copy....: 0.113879
* foreach_copy....: 0.113959
* foreach_copy....: 0.114194
* foreach_copy....: 0.114450
* foreach_copy....: 0.114610
* foreach_copy....: 0.118020
* foreach_ref.....: 0.000015
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000016
* foreach_ref.....: 0.000018
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000019
* foreach_ref.....: 0.000020

Ответ 2

Собственно, я не согласен с первым ответом. Самое главное, как отмечают в комментариях, тесты не совпадают. Здесь полностью изолированы тесты, проверяющие только петли.

Версия 1:

<?php
function test1($a) {
    $c = 0;
    $begin = microtime(true);
    foreach ($a as $v) {
        if ($v == 'x') ++$c;
    }
    $end = microtime(true);
    echo $end - $begin . "\n";
    return $c;
}

function test2(&$a) {
    $c = 0;
    $begin = microtime(true);
    foreach ($a as $v) {
        if ($v == 'x') ++$c;
    }
    $end = microtime(true);
    echo $end - $begin . "\n";
    return $c;
}

$x = array_fill(0, 1000000, 'x');

test1($x); // 0.11617302894592
test2($x); // 0.059789180755615

Версия 2:

<?php
function test1($a) {
    $cnt = count($a);
    $c = 0;
    $begin = microtime(true);
    for ($i = 0; $i < $cnt; ++$i) if ($a[$i] == 'x') ++$c;
    $end = microtime(true);
    echo $end - $begin . "\n";
    return $c;
}

function test2(&$a) {
    $cnt = count($a);
    $c = 0;
    $begin = microtime(true);
    for ($i = 0; $i < $cnt; ++$i) if ($a[$i] == 'x') ++$c;
    $end = microtime(true);
    echo $end - $begin . "\n";
    return $c;
}

$x = array_fill(0, 1000000, 'x');

test1($x); // 0.086347818374634
test2($x); // 0.086491107940674

Обратите внимание, что в полностью изолированной форме во вторых тестах нет различий, в то время как первый из них делает. Почему?

Ответ заключается в том, что массив имеет внутренний указатель на такие вещи, как foreach. Доступ к ней можно получить, например, current. Когда вы делаете foreach со ссылкой, используются исходные указатели массива. Когда вы передаете по значению, внутренние элементы массива должны быть скопированы, как только будет выполняться foreach, даже если значения будут поддерживаться каким-то образом движком. Таким образом, штраф.