Содержание

  1. Общие сведения.

  2. Увеличиваем потребление памяти вдвое.

  3. Увеличиваем потребление памяти втрое.

  4. Ещё раз увеличиваем потребление памяти на ровном месте.

  5. Заключение.

Общие сведения

Известно, что PHP активно использует механизм copy-on-write. Это означает, что при попытке внутри функции что-то записать в переданные ей параметры, вначале будет сделана копия этой переменной, а уж затем в неё что-то запишется. Такая же ситуация наблюдается с итерацией массива с помощью foreach. Отсюда следует, что вам потребуется увеличить количество памяти для создания копии переменной и времени (ресурсов ЦП), чтобы всё это проделать. Т.е. возникнет пауза, прежде чем PHP перейдёт к следующей строчке вашей программы.

Но прежде чем продолжить дальше по теме, я бы хотел рассказать зачем вообще, что-то передаётся по ссылке, а что-то - по значению. Честно, говоря, я об этом узнал несколько месяцев назад. Т.е. то, что объекты (и массивы, об этом - далее) в PHP всегда передаются по ссылке, а всё остальное по значению - я знал. Но вот зачем - нет. Ответ нашёлся в курсе по Go, как ни странно. Это компромисс. Если умолчать про массивы (и, как заметили в комментариях, строки, которые тоже массивы), то все остальные типы данных в PHP - это скаляры (чтобы быть точным см. is_scalar). Скаляры не занимают много памяти, поэтому их можно быстро скопировать, и передать в функцию копию хранимого значения. При этом на вызывающей стороне значение переменной не изменится. Объекты же могут быть огромными, например DOM-дерево огромного XML-документа. Делать копию такого объекта слишком дорого и по времени и по памяти, поэтому он передаётся по ссылке. Так почему бы не передавать скаляры тоже по ссылке? Дело в том, что передавая что-либо по ссылке мы таким образом теряем контроль над переменными в месте вызова. Представьте себе функцию с кучей параметров:

function doSmth($x1, $y2, $scale, $pojection, $alpha, $type, $reference, $mode): float;

И все они передаются по ссылке. Что случится с локальным контекстом после вызова этой функции? Останутся ли все эти переменные в тех же значениях, что и были до вызова doSmth? Неведомо сие. Всё это остаётся на совести разработчика функции doSmth. Т.о. вы частично или полностью теряете контроль над своей программой. Поэтому и придумали компромисс: скаляры всегда передаём по значению, а объекты - по ссылке.

Ещё одно уточнение, которое кажется необходимым сделать, судя по комментариям. В «Reference Counting Basics» есть следующий текст:

Since PHP allows user-land references, as created by the & operator, a zval container also has an internal reference counting mechanism to optimize memory usage.

Это означает, что в PHP есть как ссылки, которые определяет пользователь через амперсанд, так и внутренние ссылки, которые PHP использует где-то там в своих недрах и для своих нужд. Когда в статье говорится о ссылках и передаче по ссылке, то имеется в виду такое поведение, когда копия переменной не создаётся.

Увеличиваем потребление памяти вдвое

Массивы в PHP передаются по ссылке. Но если вы что-то попытаетесь записать в него, то будет создана копия массива со всеми вытекающими по памяти и процессору. Это и есть реализация механизма copy-on-write. Пример:

<?php

function doSmth(array $array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);

На моем компе с PHP 8.2.3, кстати, вывод будет таким:

memory: 2616
memory: 5264

Т.е. всего лишь записав нолик в первый элемент массива, мы увеличили потребление памяти вдвое! Это ли не чудо! Array assignment always involves value copying. Use the reference operator to copy an array by reference, см. тут. Что делать с этим? Да, нужно поставить амперсанд перед параметром $array, вот так: function doSmth(array &$array, int $memory). Тогда вывод станет таким:

memory: 2648
memory: 2680

2680 - 2648 = 32. 32 - это скорее всего кол-во памяти, выделенное на саму переменную $array (но не её значение). Как бы там ни было, это не вдвое. Проблема решена. Сейчас расскажу, как увеличить потребление памяти втрое (да, сам понимаю, что немного странно написана статья: казалось бы нужно рассказывать, как уменьшить потребление памяти, но... так показалось проще объяснить).

Увеличиваем потребление памяти втрое

Затираем амперсанд, возвращаем всё взад и попробуем сделать что-нибудь с массивом, например увеличить на 1 каждый его элемент (всё то же самое, только добавили foreach и break, чтобы не мотать весь массив):

<?php

function doSmth(array $array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

  foreach ($array as $i => $value) {
    $array[$i] ++;
    printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
    break;
  }

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

Вывод:

memory: 2616
memory: 5264
memory: 7880, i: 0
memory: 5264
memory: 2648

Как видите, потребляемая память максимальна внутри цикла. Дело в том, что при попытке что-то записать в массив внутри цикла foreach, PHP создаёт (в нашем случае ещё одну) копию массива. И даже если поставить амперсанд перед $value , то это не поможет никак.

Полный вывод работы скрипта без break и с амперсандом перед $value
<?php

function doSmth(array $array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

  foreach ($array as $i => &$value) {
    // $array[$i] ++;
    $value ++;
    printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
    // break;
  }

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
memory: 2616
memory: 5264
memory: 5328, i: 0
memory: 5360, i: 1
memory: 5392, i: 2
memory: 5424, i: 3
memory: 5456, i: 4
memory: 5488, i: 5
memory: 5520, i: 6
memory: 5552, i: 7
memory: 5584, i: 8
memory: 5616, i: 9
memory: 5648, i: 10
memory: 5680, i: 11
memory: 5712, i: 12
memory: 5744, i: 13
memory: 5776, i: 14
memory: 5808, i: 15
memory: 5840, i: 16
memory: 5872, i: 17
memory: 5904, i: 18
memory: 5936, i: 19
memory: 5968, i: 20
memory: 6000, i: 21
memory: 6032, i: 22
memory: 6064, i: 23
memory: 6096, i: 24
memory: 6128, i: 25
memory: 6160, i: 26
memory: 6192, i: 27
memory: 6224, i: 28
memory: 6256, i: 29
memory: 6288, i: 30
memory: 6320, i: 31
memory: 6352, i: 32
memory: 6384, i: 33
memory: 6416, i: 34
memory: 6448, i: 35
memory: 6480, i: 36
memory: 6512, i: 37
memory: 6544, i: 38
memory: 6576, i: 39
memory: 6608, i: 40
memory: 6640, i: 41
memory: 6672, i: 42
memory: 6704, i: 43
memory: 6736, i: 44
memory: 6768, i: 45
memory: 6800, i: 46
memory: 6832, i: 47
memory: 6864, i: 48
memory: 6896, i: 49
memory: 6928, i: 50
memory: 6960, i: 51
memory: 6992, i: 52
memory: 7024, i: 53
memory: 7056, i: 54
memory: 7088, i: 55
memory: 7120, i: 56
memory: 7152, i: 57
memory: 7184, i: 58
memory: 7216, i: 59
memory: 7248, i: 60
memory: 7280, i: 61
memory: 7312, i: 62
memory: 7344, i: 63
memory: 7376, i: 64
memory: 7408, i: 65
memory: 7440, i: 66
memory: 7472, i: 67
memory: 7504, i: 68
memory: 7536, i: 69
memory: 7568, i: 70
memory: 7600, i: 71
memory: 7632, i: 72
memory: 7664, i: 73
memory: 7696, i: 74
memory: 7728, i: 75
memory: 7760, i: 76
memory: 7792, i: 77
memory: 7824, i: 78
memory: 7856, i: 79
memory: 7888, i: 80
memory: 7920, i: 81
memory: 7952, i: 82
memory: 7984, i: 83
memory: 8016, i: 84
memory: 8048, i: 85
memory: 8080, i: 86
memory: 8112, i: 87
memory: 8144, i: 88
memory: 8176, i: 89
memory: 8208, i: 90
memory: 8240, i: 91
memory: 8272, i: 92
memory: 8304, i: 93
memory: 8336, i: 94
memory: 8368, i: 95
memory: 8400, i: 96
memory: 8432, i: 97
memory: 8464, i: 98
memory: 8496, i: 99
memory: 8496
memory: 2648

foreach вообще достаточно проблемная конструкция для синтаксического сахара. Без проблем её можно использовать только для "посмотреть" на каждой итерации, "сделать" же что-то обходится слишком дорого:

Альтернативой будет использование цикла for.

Hidden text
<?php

function doSmth(array $array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

  $count = count($array);
  for ($i = 0; $i < $count; $i ++) {
    $array[$i] = 100;
    printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
  }

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
memory: 2616
memory: 5264
memory: 5264, i: 0
memory: 5264, i: 1
memory: 5264, i: 2
memory: 5264, i: 3
memory: 5264, i: 4
memory: 5264, i: 5
memory: 5264, i: 6
memory: 5264, i: 7
memory: 5264, i: 8
memory: 5264, i: 9
memory: 5264, i: 10
memory: 5264, i: 11
memory: 5264, i: 12
memory: 5264, i: 13
memory: 5264, i: 14
memory: 5264, i: 15
memory: 5264, i: 16
memory: 5264, i: 17
memory: 5264, i: 18
memory: 5264, i: 19
memory: 5264, i: 20
memory: 5264, i: 21
memory: 5264, i: 22
memory: 5264, i: 23
memory: 5264, i: 24
memory: 5264, i: 25
memory: 5264, i: 26
memory: 5264, i: 27
memory: 5264, i: 28
memory: 5264, i: 29
memory: 5264, i: 30
memory: 5264, i: 31
memory: 5264, i: 32
memory: 5264, i: 33
memory: 5264, i: 34
memory: 5264, i: 35
memory: 5264, i: 36
memory: 5264, i: 37
memory: 5264, i: 38
memory: 5264, i: 39
memory: 5264, i: 40
memory: 5264, i: 41
memory: 5264, i: 42
memory: 5264, i: 43
memory: 5264, i: 44
memory: 5264, i: 45
memory: 5264, i: 46
memory: 5264, i: 47
memory: 5264, i: 48
memory: 5264, i: 49
memory: 5264, i: 50
memory: 5264, i: 51
memory: 5264, i: 52
memory: 5264, i: 53
memory: 5264, i: 54
memory: 5264, i: 55
memory: 5264, i: 56
memory: 5264, i: 57
memory: 5264, i: 58
memory: 5264, i: 59
memory: 5264, i: 60
memory: 5264, i: 61
memory: 5264, i: 62
memory: 5264, i: 63
memory: 5264, i: 64
memory: 5264, i: 65
memory: 5264, i: 66
memory: 5264, i: 67
memory: 5264, i: 68
memory: 5264, i: 69
memory: 5264, i: 70
memory: 5264, i: 71
memory: 5264, i: 72
memory: 5264, i: 73
memory: 5264, i: 74
memory: 5264, i: 75
memory: 5264, i: 76
memory: 5264, i: 77
memory: 5264, i: 78
memory: 5264, i: 79
memory: 5264, i: 80
memory: 5264, i: 81
memory: 5264, i: 82
memory: 5264, i: 83
memory: 5264, i: 84
memory: 5264, i: 85
memory: 5264, i: 86
memory: 5264, i: 87
memory: 5264, i: 88
memory: 5264, i: 89
memory: 5264, i: 90
memory: 5264, i: 91
memory: 5264, i: 92
memory: 5264, i: 93
memory: 5264, i: 94
memory: 5264, i: 95
memory: 5264, i: 96
memory: 5264, i: 97
memory: 5264, i: 98
memory: 5264, i: 99
memory: 5264
memory: 2648

Обратите внимание, что размер массива должен вычисляться только один раз перед массивом, а не на каждой итерации:

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

По ходу пьесы обнаружил ещё две интересные статьи на Хабре:

  1. Сравнение производительности перебора массивов в цикле через for() и foreach(). Так ли это для 8-ой версии не знаю, не проверял.

  2. array_* vs foreach или PHP7 vs PHP5. Тест на скорую руку показал, что array_map потребляет тоже почти в 3 раза больше памяти (а без амперсанда перед $array ещё больше).

Использование array_map
<?php

function doSmth(array &$array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

  // foreach ($array as $i => &$value) {
  //   // $array[$i] ++;
  //   $value ++;
  //   printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
  //   // break;
  // }

  // $count = count($array);
  // for ($i = 0; $i < $count; $i ++) {
  //   $array[$i] = 100;
  //   printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
  // }

  array_map(function($value) use ($memory) {
    $value ++;
    printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $value, PHP_EOL);
  }, $array);

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
memory: 2648
memory: 2680
memory: 6160, i: 1
memory: 6160, i: 2
memory: 6160, i: 3
memory: 6160, i: 4
memory: 6160, i: 5
memory: 6160, i: 6
memory: 6160, i: 7
memory: 6160, i: 8
memory: 6160, i: 9
memory: 6160, i: 10
memory: 6160, i: 11
memory: 6160, i: 12
memory: 6160, i: 13
memory: 6160, i: 14
memory: 6160, i: 15
memory: 6160, i: 16
memory: 6160, i: 17
memory: 6160, i: 18
memory: 6160, i: 19
memory: 6160, i: 20
memory: 6160, i: 21
memory: 6160, i: 22
memory: 6160, i: 23
memory: 6160, i: 24
memory: 6160, i: 25
memory: 6160, i: 26
memory: 6160, i: 27
memory: 6160, i: 28
memory: 6160, i: 29
memory: 6160, i: 30
memory: 6160, i: 31
memory: 6160, i: 32
memory: 6160, i: 33
memory: 6160, i: 34
memory: 6160, i: 35
memory: 6160, i: 36
memory: 6160, i: 37
memory: 6160, i: 38
memory: 6160, i: 39
memory: 6160, i: 40
memory: 6160, i: 41
memory: 6160, i: 42
memory: 6160, i: 43
memory: 6160, i: 44
memory: 6160, i: 45
memory: 6160, i: 46
memory: 6160, i: 47
memory: 6160, i: 48
memory: 6160, i: 49
memory: 6160, i: 50
memory: 6160, i: 51
memory: 6160, i: 52
memory: 6160, i: 53
memory: 6160, i: 54
memory: 6160, i: 55
memory: 6160, i: 56
memory: 6160, i: 57
memory: 6160, i: 58
memory: 6160, i: 59
memory: 6160, i: 60
memory: 6160, i: 61
memory: 6160, i: 62
memory: 6160, i: 63
memory: 6160, i: 64
memory: 6160, i: 65
memory: 6160, i: 66
memory: 6160, i: 67
memory: 6160, i: 68
memory: 6160, i: 69
memory: 6160, i: 70
memory: 6160, i: 71
memory: 6160, i: 72
memory: 6160, i: 73
memory: 6160, i: 74
memory: 6160, i: 75
memory: 6160, i: 76
memory: 6160, i: 77
memory: 6160, i: 78
memory: 6160, i: 79
memory: 6160, i: 80
memory: 6160, i: 81
memory: 6160, i: 82
memory: 6160, i: 83
memory: 6160, i: 84
memory: 6160, i: 85
memory: 6160, i: 86
memory: 6160, i: 87
memory: 6160, i: 88
memory: 6160, i: 89
memory: 6160, i: 90
memory: 6160, i: 91
memory: 6160, i: 92
memory: 6160, i: 93
memory: 6160, i: 94
memory: 6160, i: 95
memory: 6160, i: 96
memory: 6160, i: 97
memory: 6160, i: 98
memory: 6160, i: 99
memory: 6160, i: 100
memory: 2680
memory: 2680

Таким образом, оптимальным, без всяких дополнительных затрат, будет вариант с передачей массива по ссылке и использованием for.

Пример
<?php

function doSmth(array &$array, int $memory) {

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
  $array[0] = 0;
  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

  $count = count($array);
  for ($i = 0; $i < $count; $i ++) {
    $array[$i] = 100;
    printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
  }

  printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
}

$memory = memory_get_usage();
$array = range(0, 99);
doSmth($array, $memory);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
memory: 2648
memory: 2680
memory: 2680, i: 0
memory: 2680, i: 1
memory: 2680, i: 2
memory: 2680, i: 3
memory: 2680, i: 4
memory: 2680, i: 5
memory: 2680, i: 6
memory: 2680, i: 7
memory: 2680, i: 8
memory: 2680, i: 9
memory: 2680, i: 10
memory: 2680, i: 11
memory: 2680, i: 12
memory: 2680, i: 13
memory: 2680, i: 14
memory: 2680, i: 15
memory: 2680, i: 16
memory: 2680, i: 17
memory: 2680, i: 18
memory: 2680, i: 19
memory: 2680, i: 20
memory: 2680, i: 21
memory: 2680, i: 22
memory: 2680, i: 23
memory: 2680, i: 24
memory: 2680, i: 25
memory: 2680, i: 26
memory: 2680, i: 27
memory: 2680, i: 28
memory: 2680, i: 29
memory: 2680, i: 30
memory: 2680, i: 31
memory: 2680, i: 32
memory: 2680, i: 33
memory: 2680, i: 34
memory: 2680, i: 35
memory: 2680, i: 36
memory: 2680, i: 37
memory: 2680, i: 38
memory: 2680, i: 39
memory: 2680, i: 40
memory: 2680, i: 41
memory: 2680, i: 42
memory: 2680, i: 43
memory: 2680, i: 44
memory: 2680, i: 45
memory: 2680, i: 46
memory: 2680, i: 47
memory: 2680, i: 48
memory: 2680, i: 49
memory: 2680, i: 50
memory: 2680, i: 51
memory: 2680, i: 52
memory: 2680, i: 53
memory: 2680, i: 54
memory: 2680, i: 55
memory: 2680, i: 56
memory: 2680, i: 57
memory: 2680, i: 58
memory: 2680, i: 59
memory: 2680, i: 60
memory: 2680, i: 61
memory: 2680, i: 62
memory: 2680, i: 63
memory: 2680, i: 64
memory: 2680, i: 65
memory: 2680, i: 66
memory: 2680, i: 67
memory: 2680, i: 68
memory: 2680, i: 69
memory: 2680, i: 70
memory: 2680, i: 71
memory: 2680, i: 72
memory: 2680, i: 73
memory: 2680, i: 74
memory: 2680, i: 75
memory: 2680, i: 76
memory: 2680, i: 77
memory: 2680, i: 78
memory: 2680, i: 79
memory: 2680, i: 80
memory: 2680, i: 81
memory: 2680, i: 82
memory: 2680, i: 83
memory: 2680, i: 84
memory: 2680, i: 85
memory: 2680, i: 86
memory: 2680, i: 87
memory: 2680, i: 88
memory: 2680, i: 89
memory: 2680, i: 90
memory: 2680, i: 91
memory: 2680, i: 92
memory: 2680, i: 93
memory: 2680, i: 94
memory: 2680, i: 95
memory: 2680, i: 96
memory: 2680, i: 97
memory: 2680, i: 98
memory: 2680, i: 99
memory: 2680
memory: 2680

Ещё раз увеличиваем потребление памяти на ровном месте

Ну и сладкое на десерт. В замечательной статье «Массивы в РНР 7: хэш-таблицы» говорится о том, что массив, точнее его внутреннее представление может храниться в упакованном и классическом виде. В упакованном - значит в сжатом. Сжатым массив создаётся, когда в нем используются только целочисленные ключи и только по порядку, например так:

<?php

$array = [];
$array[] = 'Один';
$array[] = 'Два';
...

Или используя функцию range, как это делалось в коде выше, например:

<?php

$memory = memory_get_usage();
$array = range(0, 99);
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
$array['qwe'] = 'new item value';
printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

Вывод:

memory: 2616
memory: 8280

Массив начал занимать в 3 раза больше памяти после того, как в него добавили один (один!) элемент. В 3 раза, Карл! На ровном месте. Никто не ждал подвоха, а тут опять и снова!

Для других значений (вместо 100) статистика по увеличению потребления памяти выглядит так:

Размер массива, шт

Кол-во памяти до, байт

Кол-во памяти после, байт

Разница, раз

10

408

728

1.8

100

2648

8280

3.1

1000

20568

41048

2

10000

266328

655448

2.5

100000

2101360

5242992

2.5

1000000

16781424

41943152

2.5

Таким образом, если вдруг вам захочется добавить в ваш массив какую-то информацию об этом же массиве (например среднее значение или min и max), то не надо. Создайте для этого другой массив или используйте stdClass:

<?php

$bigArray = range(0, 10000000);
$info = new stdClass;        // $info = []
$info->data = $bigArray;     // $info['data'] = $bigArray;
$info->min = min($bigArray); // $info['min'] = min($bigArray);
$info->max = ...;            // $info['max'] = ...;
...

Справедливости ради отмечу, что утверждение о том, что целочисленные ключи должны идти строго по порядку - не совсем верно. Они могут идти не по порядку до тех пор, пока значение ключа не превысит размер хэш-таблицы, см. раздел «Конструкция хэш-таблицы».

Заключение

Аккуратно работайте с массивами, особенно с большими. Их точно лучше передавать по ссылке, как PHP передаёт объекты. Используйте for. Думайте, проверяйте и замеряйте. Держите массивы упакованными. Рассмотрите возможность использования SplFixedArray.

Тут вообще нужно быть осторожным и проверять всё именно для вашей версии PHP. Работает ли for быстрее foreach на восьмёрке? А на семёрке? А на пятёрке? Т.е. всё то, что написано, верно для моей версии PHP на моём компьютере, но верно ли для вас - вопрос.

P. S. Обнаружил ещё одну интересную статью: «Насколько большие массивы (и значения) в PHP? (Подсказка: ОЧЕНЬ БОЛЬШИЕ)». А в комментариях интересный пост, где у пользователя @inilim2 показатели потребления памяти оказались в 3 раза выше, чем у меня. Оказалось, что с версии 8.2 PHP стал потреблять гораздо меньше памяти (по крайней мере для хранения целочисленных массивов). А @FanatPHP подсказал инструмент для тестирования кода PHP на разных версиях одновременно. Итого:

Комментарии (45)


  1. FanatPHP
    09.07.2023 14:00
    +22

    Есть хорошее эмпирическое правило: не писать статей о том, про что ты узнал всего несколько месяцев назад. Лучше подождать годик или два. Ну или хотя бы разобрать тему повнимательнее.
    Впрочем, у вас почти получилось, про память в целом написано верно. Кроме путаницы со ссылками.


    Называть copy-on-write передачей по ссылке — это упрощение, граничащее с невежеством. Вас за него закидают тухлыми камнями, и поделом.
    Это два совершенно разных механизма, и то, что вы их путаете, говорит о том что вы не поняли оба. Ну как можно утверждать, что массивы передаются по ссылке, если исходный массив не меняется после его изменения внутри функции?
    И про скаляры вы тоже попали пальцем в небо (особенно про "не занимают много места", ха-ха), хотя могли бы элементарно проверить, заточив свой пример под передачу строк вместо массивов. И убедиться, что в отношении потребления памяти скаляры ведут себя совершенно идентично массивам — не занимают места, пока не поменялись, и удваивают потребление памяти при изменении. В чем, собственно, и заключается суть copy-on-write.


    Но это детали, а основная мысль статьи передана верно. Ничего странного, кстати, в ней нет. Есть такой жанр, "вредные советы", и уже из заголовка он прекрасно считывается.


    1. aleksandr-s-zelenin Автор
      09.07.2023 14:00
      +1

      Кроме путаницы со ссылками.

      А что я там напутал?

      Называть copy-on-write передачей по ссылке — это упрощение, граничащее с невежеством.

      Я нигде не называл copy-on-write передачей по ссылке.

      Ну как можно утверждать, что массивы передаются по ссылке, если исходный массив не меняется после его изменения внутри функции?

      В этом и есть суть copy-on-write. Как только вы решите изменить массив переданный в функцию PHP создаст копию исходного массива. Поэтому исходный и останется без изменений. Что собственно и подтверждается приведённым выше экспериментом. Про это написано тут.

      И про скаляры вы тоже попали пальцем в небо (особенно про "не занимают много места", ха-ха), хотя могли бы элементарно проверить, заточив свой пример под передачу строк вместо массивов. И убедиться, что в отношении потребления памяти скаляры ведут себя совершенно идентично массивам — не занимают места, пока не поменялись, и удваивают потребление памяти при изменении. В чем, собственно, и заключается суть copy-on-write.

      Я опять же не писал, что скаляры ведут себя как-то отдельно и что их копии не создаются. Создаются, просто они маленькие по памяти. Но про строки вы верно заметили, это я поправлю в статье. Internally, PHP strings are byte arrays. As a result, accessing or modifying a string using array brackets is not multi-byte safe, and should only be done with strings that are in a single-byte encoding such as ISO-8859-1.


      1. FanatPHP
        09.07.2023 14:00
        +5

        Ну вот это вы зря, конечно. Стоило перечитать свой текст, перед тем как отвечать. Возможно, у себя в голове вы действительно не путаете (хотя я уже начинаю в этом сомневаться). Но я-то в любом случае не могу знать, что вы думаете, а вижу только то, что вы пишете. А пишете вы откровенную чушь.


        Я нигде не называл copy-on-write передачей по ссылке.

        Ну как не называли-то? Вот же, прямым текстом, первая фраза в разделе "Увеличиваем потребление памяти вдвое":


        Массивы в PHP передаются по ссылке.

        Данное утверждение является на 100% неверным. Массивы не передаются по ссылке. Вы называете передачей по ссылке передачу по значению с использованием copy-on-write. То есть, путаете эти два механизма.


        Я опять же не писал, что скаляры ведут себя как-то отдельно

        А вы точно автор этой статьи? В ней написано, что ведут:


        Если умолчать про массивы, то все остальные типы данных в PHP — это скаляры (чтобы быть точным см. is_scalar). Скаляры не занимают много памяти, поэтому их можно быстро скопировать и передать в функцию копию хранимого значения.

        Здесь вы явно выделяете скаляры в отдельную категорию, поведение которой отличается от поведения массивов.


        Я опять же не писал ..., что что их копии не создаются.

        Правильно. Вы писали, что создаются. Что, опять же, является 100% неверным. При передаче копия скаляра в памяти не создаётся. А создаётся при записи.


        Плюс чуть раньше вы повторяете то же самое:


        то, что объекты (и массивы, об этом — далее) в PHP всегда передаются по ссылке, а всё остальное по значению я знал.

        То есть опять вы заявляете, что массивы передаются по ссылке, а все остальное (фактически, остаются только скаляры) — по значению.


        На всякий случай повторю: Данное утверждение является дважды неверным.
        Массивы передаются не по ссылке
        Передача "всего остального" (т.е., по контексту статьи — скаляров) не отличается от передачи массивов. И те и другие передаются по значению, с использованием copy-on-write.


        1. aleksandr-s-zelenin Автор
          09.07.2023 14:00

          Данное утверждение является на 100% неверным. Массивы не передаются по ссылке. Вы называете передачей по ссылке передачу по значению с использованием copy-on-write. То есть, путаете эти два механизма.

          Действительно, в документации, кажется, нигде явно не написано, что массивы передаются по ссылке. На stackoverflow.com есть разбор этого вопроса: Are arrays in PHP copied as value or as reference to new variables, and when passed to functions? И там говорится, что по ссылке. Там же есть ссылка на другой документ (один я уже приводил: «Reference-counting and copy-on-write»): PHP internals: When does foreach copy? И там есть вот такие строки (вырвано из контекста, нужно ознакомиться с разделом целиком):

          The reason is that the array zval is now shared between multiple variables: The $array variable outside the function and the $array variable inside it.

          Т.е. подразумевается, что массив передался по ссылке и теперь две переменные указывают на один и тот же zval.

          Я склонен считать, что массивы в PHP передаются по ссылке всегда.


          1. FanatPHP
            09.07.2023 14:00
            +3

            Ага, вот теперь мы уже сильно ближе к пониманию.
            То есть вы действительно путаете передачу по ссылке с copy-on-write.
            И, следуя вашему ходу мысли, придется заключить что строки-таки тоже передаются по ссылке. Верно?


            Но мне кажется, я знаю, как вам объяснить разницу. Собственно, вам надо просто попытаться её сформулировать. Вербализовать. Вот просто поставьте рядом в голове эти два механизма, и примите за аксиому, что они разные. И попробуйте эту разницу сформулировать. Мне кажется, это должно сработать. Особенно если помнить, что при передаче по ссылке изменения в одном из экземпляров переменной отражаются и во всех остальных.


            Кроме этого, можете попытаться объяснить, в чем смысл передавать массив в foreach по ссылке… если — как вы утверждаете — он и так передается по ссылке! ;)


            По источникам: доверять какому-то васе пупкину, который решил выступить на стаковерфлое, всегда следует с осторожностью. Из всех приведенных вами источников следует читать только то, что пишет Никита про 7 версию.


            1. aleksandr-s-zelenin Автор
              09.07.2023 14:00

              Баста! Я придумал как доказать это:

              <?php
              
              function passByReference(&$array, int $baseMemory) {
              
                printf('memory in %s: %s%s', __FUNCTION__, memory_get_usage() - $baseMemory, PHP_EOL);
              }
              
              function passByValue($array, int $baseMemory) {
              
                printf('memory in %s: %s%s', __FUNCTION__, memory_get_usage() - $baseMemory, PHP_EOL);
              }
              
              $baseMemory = memory_get_usage();
              $array = range(0, 99);
              
              printf('base memory: %s%s', memory_get_usage() - $baseMemory, PHP_EOL);
              passByReference($array, $baseMemory);
              printf('memory between: %s%s', memory_get_usage() - $baseMemory, PHP_EOL);
              passByValue($array, $baseMemory);
              printf('memory after: %s%s', memory_get_usage() - $baseMemory, PHP_EOL);

              Вывод:

              base memory: 2616
              memory in passByReference: 2680
              memory between: 2680
              memory in passByValue: 2680
              memory after: 2680
              1. Разницу в 2680 - 2616 = 64 опустим как несущественную.

              2. Внутри passByReference как видно объём не увеличился вдвое. Почему? Потому что массив передаётся по ссылке.

              3. Внутри passByValue как видно объём не увеличился вдвое. Почему? По той же самой причине: массив передаётся по ссылке.

              @FanatPHP, как вы объясните наблюдаемые данные?


              1. FanatPHP
                09.07.2023 14:00
                +2

                По той же самой причине: массив передаётся по ссылке.

                Ага, а у таракана уши в ногах.


                1. aleksandr-s-zelenin Автор
                  09.07.2023 14:00

                  Отличный анекдот, кстати )

                  Только это не объясняет почему в обоих случаях потребление памяти осталось одинаковым. Ведь очевидно, что копия сделана не была.


                  1. FanatPHP
                    09.07.2023 14:00
                    +2

                    Почему не объясняет? Как раз отлично объясняет. Вы точно так же из объективных данных эксперимента делаете неверные выводы.


                    1. Таракан не убежал. Следовательно, уши у него в ногах
                    2. Копия сделана не была. Следовательно, массив был передан по ссылке.

                    Ход мысли ну совершенно же идентичный.


                    Ваша проблема в том, что вы смотрите на ситуацию исключительно с точки зрения потребления памяти. И ни на что больше не обращаете внимание.


                    Знаете, я как-то подарил племянникам лазерные шахматы, Khet. Отличная игра, кстати. И часто проигрывал. Потому что мысленно выстраивая путь лазера, смотрел только на фигуры противника, и забывал про свои.


                    У вас здесь то же самое. Вы пытаетесь объяснить поведение РНР, и по-своему, в своем узком контексте правы. Но вы забываете, что передача по ссылке — это совершенно конкретный механизм языка, который здесь НЕ используется. А используется какой-то другой. Который хотя и имеет сходную природу, но всё равно совершенно отдельный. Именно поэтому ваши заявления выглядят так дико. Вам необходимо использовать другую терминологию. Передача по ссылке уже занята.


                    Попробуйте выйти из своего контекста, и взглянуть на свои утверждения с чистого листа. Вот представьте, что кто-то вас останавливает в коридоре и говорит, что в РНР массивы и строки передаются по ссылке. Представили? Вот именно это состояние охудивления и испытывают все, кто читает ваш опус.


              1. Hardcoin
                09.07.2023 14:00
                +3

                Потому что массив передаётся по ссылке.

                Строки тоже "передаются по ссылке". Вы можете это проверить тем же самым кодом.


            1. SuperCat911
              09.07.2023 14:00

              С большим интересом читаю Ваши комментарии. Но я уже сам запутался. Можно как-то доступнее? :)

              П.С. Я уже на старте проверять код на массивы и форичи.


              1. FanatPHP
                09.07.2023 14:00

                Ну вот я как раз написал только что последний. Хотя писал уже об этом и раньше. Автор путает сам язык и его внутреннюю реализацию. По ссылке передаются только объекты. Массивы передаются по значению. При этом не происходит тупого копирования, а память часто экономится за счет механизма copy-on-write. Внутри себя этот механизм использует механизм ссылок. Но говорить при из-за этого, что массивы передаются по ссылке — неграмотно.


                Я уже на старте проверять код на массивы и форичи.

                И сразу сделали неправильно.
                Проверять надо не "массивы и форичи", а их размер. Если вы в коде работаете с огромным массивом, то сначала надо разобраться, откуда он взялся, и можно ли его уменьшить. Именно размер в данном случае имеет значение, и обработка такого массива в любом случае будет отнимать много ресурсов. Поэтому таких ситуаций следует избегать. И только в редких случаях, когда обойтись без изменения огромного массива не получится, можно применять ухищрения, описанные в статье.


                1. SuperCat911
                  09.07.2023 14:00

                  Большое спасибо за подробное разъяснение.


          1. amberovsky
            09.07.2023 14:00
            +2

            Может у меня получится прояснить разницу.

            Вот есть функция. У неё есть аргументы. Есть код, который вызывает это функцию и передаёт какие-то переменные.

            Вопрос - а что по факту передаётся в функцию?

            Ответ - по факту это всегда некий адрес в памяти. Массив, int, объект, и т.д. - это всё просто некий адрес в памяти, а дальше уже компилятор разруливает как вызвать метод объекта по этому адресу памяти или как трактовать это как элемент массива.

            На заре программирования думали-думали и решили сделать два способа передачи параметров (забавный факт - в компуктерах PDP-11 было аж 42 способа адресовать аргумент) :

            1) По ссылке - когда функция получает ровно тот же адрес переменной, который был когда вызвали функцию. В данном случае компилятор просто копирует адрес переменной.

            2) По значению - когда функция получает совершенной другой адрес переменной при вызове. В данном случае компилятор выполняет полное копирование переменной.

            После это стали думать ещё. А вдруг аргумент функции не изменяется, а мы передаём его по значению. Если это огромный массив - то мы зря делаем полную копию массива.

            Пришла гениальная идея - а давайте не будем делать полную копию, вместо этого передадим по факту по ссылке, дождёмся пока код решит что-то записать в эту переменную, и только тогда сделаем полную копию (ну и подвиснем ещё на некоторое время). В итоге компилятор вставляет дополнительный код чтобы разруливать такие сценарии.

            Итого: передача аргумента (в более широком смысле - адресация переменных) и стратегия cope-on-write разные вещи, но существуют в одном контексте. Поэтому их часто употребляют как взаимозаменямо. Но их гораздо больше!


        1. diakin
          09.07.2023 14:00

          Написано же "Но.." Не вижу, что ТС что-то путает .

          Увеличиваем потребление памяти вдвое Массивы в PHP передаются по ссылке. Но если вы что-то попытаетесь записать в него, то будет создана копия массива со всеми вытекающими по памяти и процессору. Это и есть реализация механизма copy-on-write.


          1. Hardcoin
            09.07.2023 14:00

            будет создана копия массива

            Это не передача по ссылке.

            Для понимания разницы обратите внимание, как работает передача объекта в функцию - объекты передаются по ссылке и разница с массивами огромна.


            1. diakin
              09.07.2023 14:00

              Да понятно, что если "будет создана копия массива", то это не по ссылке.
              Если в функции массив только читается - то по ссылке. А если в функции в массив что-то пишется - то создается копия. Так наверное?


              1. Hardcoin
                09.07.2023 14:00
                +5

                Если в функции массив только читается - то по ссылке.

                Это нельзя назвать передачей по ссылке. "Передача по ссылке" - конкретный термин имеющий вполне точное значение. Передача по ссылке позволяет функции изменять переменную без создания копии. А в данном случае изменить (без создания копии) не получится, значит это не оно.


                1. diakin
                  09.07.2023 14:00
                  -1

                  Это уже проблемы терминологии.
                  Передача по ссылке позволяет функции иметь доступ к переменной без создания копии.
                  При передаче по значению создается локальная переменная, в которую копируется внешняя.
                  А если при передаче по ссылке доступ только на чтение, то это проблемы конкретного языка.
                  Иначе придется вводить третий термин, ведь получатся и не по ссылке, и не по значению.

                  https://learn.microsoft.com/ru-ru/dotnet/visual-basic/programming-guide/language-features/procedures/differences-between-passing-an-argument-by-value-and-by-reference

                  передача по значению
                  При использовании этого механизма передачи Visual Basic копирует значение базового программного элемента в локальную переменную в процедуре. Код процедуры не имеет доступа к базовому элементу в вызывающем коде.
                  Передача по ссылке
                  При использовании этого механизма передачи Visual Basic предоставляет процедуре прямую ссылку на базовый программный элемент в вызывающем коде.


                  1. Hardcoin
                    09.07.2023 14:00
                    +1

                    Разумеется это вопрос терминологии. И есть общепринятая. ТС путает именно использование общепринятой терминологии.

                    Так-то легко можно сказать, что объекты передаются по значению, потому что значимый объект передается внутрь функции. Просто вас не поймут, если вы будете так говорить.

                    А если при передаче по ссылке доступ только на чтение, то это проблемы конкретного языка.

                    В php доступ не только на чтение (это означало бы ошибку при попытке изменить переменную). Доступ только на чтение у констант. Сравните разницу.

                    Иначе придется вводить третий термин, ведь получатся и не по ссылке, и не по значению.

                    Верно, этот третий термин ввели. Copy-on-write.


                    1. aleksandr-s-zelenin Автор
                      09.07.2023 14:00

                      Copy-on-write - это не третий и никакой вообще способ передачи. Это механизм позволяющий оттянуть или вовсе избежать копирования параметров функции. Он опционален. И без COW можно реализовать передачу по ссылке и по значению.

                      Что касается терминологии, то возможно, следовало упомянуть в статье, что имеется ввиду по ссылкой (я подправлю потом текст). В «Reference Counting Basics» сказано:

                      Since PHP allows user-land references, as created by the & operator, a zval container also has an internal reference counting mechanism to optimize memory usage.

                      Я имею ввиду именно те самые внутренние ссылки - internal reference - которые можно явно задать при помощи амперсанда или те, которые PHP создаст сам, где посчитает нужным. В данном случае он создаёт внутреннюю ссылку (не внутрь куда-то, а другую, не user-land) на массив при передаче его в функцию по значению.


                      1. Hardcoin
                        09.07.2023 14:00
                        +2

                        Он опционален.

                        Нет, не опционален. Вы не можете включить в php классическую передачу по значению.

                        И без COW можно реализовать передачу по ссылке и по значению.

                        Конечно можно. Но в php не реализована классическая передача по значению, только cow.

                        Я имею ввиду именно те самые внутренние ссылки

                        Для массивов? А для объектов вы тоже имеете ввиду именно внутренние ссылки? Они ведь передаются не так, как массивы, а вы назвали их одинаковым термином.

                        Сами запутались и других путаете - это не страшно, бывает. Но вы продолжаете настаивать на своей нестандартной терминологии неясно зачем.


                      1. aleksandr-s-zelenin Автор
                        09.07.2023 14:00
                        -4

                        Я устал с вами с спорить. Вы не ответили на вопрос как объяснить то, что при передаче массива в функцию и по ссылке и по значению кол-во потребляемой памяти не увеличивается. Потому что ответ очевиден, но вы не хотите признать эту очевидность, потому что всегда считали, что массив передаётся по значению. И ввиду нежелания принять этот факт решили свести всё к терминологическому спору. Моя статья не про точность терминов, а про то, как работать с массивами не вызывая чрезмерного потребления памяти. И эти чёртовы массивы в PHP передаются по ссылке, указателю или ещё какому-то фантомному референсу не вызывая копирования, как это ожидается, при передаче по значению. Потому что они передаются не по значению.

                        Вот ваше расписание на неделю:


                      1. Hardcoin
                        09.07.2023 14:00

                        Вы не ответили на вопрос как объяснить

                        Вы, кажется, этот вопрос задавали не мне? Но ответ простой - copy-on-write копирует не сразу, а только при попытке изменения. Поэтому и не увеличивается. Конечно при чтении php пользуется тем же самым блоком памяти. Пользуется тем же адресом, если угодно.

                        Потому что ответ очевиден

                        Да, очевиден, php не копирует область памяти без необходимости.

                        вы не хотите признать эту очевидность

                        Как видите, признал. Теперь у вас пропал повод для бессмысленных переходов на личности?

                        не вызывая копирования, как это ожидается

                        Кем ожидается? Вами? При cow копирование ожидается только при изменении, не раньше. Вы это знаете, я это знаю. Так к чему этот странный оборот "ожидается"?

                        На мой вопрос, пожалуйста, тоже ответьте. Нехорошо указывать оппоненту на неотвеченный вопрос, а самому при этом вопрос проигнорировать.


                      1. aleksandr-s-zelenin Автор
                        09.07.2023 14:00

                        Я прошу прощения, я вас с @FanatPHPперепутал. А ожидается как минимум мной потому что в документации сказано, что by default, variables are always assigned by value. А тонкие материи там освещены так себе. Поэтому и ожидается... дефолтное поведение.


                      1. FanatPHP
                        09.07.2023 14:00
                        +1

                        Copy-on-write — это не третий и никакой вообще способ передачи.

                        Правильно!!!
                        И вот в случае с вашими тараканами происходит именно ЭТО! А никакая не передача по ссылке. Которая происходит по значению.


                        Вы упорно путаете сам язык и его внутреннюю реализацию. Массивы передаются по значению. Запомните уже это наконец! Но память при этом экономится. За счет cow. Это будет корректное описание ситуации. А не у таракана уши в ногах "массивы передаются по ссылке".


                        Вы пытаетесь коряво сформулировать причину экономии памяти, но вместо "с использованием механизма Copy-on-write, который там где-то внутри себя использует механизм ссылок" постоянно повторяете эту чушь, "массивы передаются по ссылке!".


                        Если уж на то пошло, вы говорите не о массивах, а о структурах zval.


                        те самые внутренние ссылки — internal reference — которые можно явно задать при помощи амперсанда или те, которые PHP создаст сам

                        Вы опять все перепутали. Ссылки, которые можно задать при помощи амперсанда — это механизм языка. Который реализуется с помощью internal reference. У вас логика на уровне "Марк Цукерберг и Вася Пупкин оба произошли от обезьяны. Поэтому Марк Цукерберг и Вася Пупкин — это один и тот же человек!".


                        Причем эти самые "internal reference" в при реализации передачи по ссылке и при реализации cow — тоже разные, насколько я могу судить. А вы валите всё в кучу.


                  1. diakin
                    09.07.2023 14:00

                    А если при передаче по ссылке доступ только на чтение, то это проблемы конкретного языка.

                    Ну в общем-то не конкретного языка, я погорячился.
                    https://ru.wikipedia.org/wiki/Копирование_при_записи

                    Механизм копирования при записи (англ. Copy-on-write, COW) используется для оптимизации многих процессов, происходящих в операционной системе, таких как, например, работа с оперативной памятью или файлами на диске...


              1. FanatPHP
                09.07.2023 14:00
                +4

                Вы, как и автор, путаете сам язык с его внутренними оптимизациями.


                Смотрите, передача по ссылке — это не просто "термин". А конкретный механизм языка, совершенно определённый, не допускающий интерпретаций и толкований. Который описывает поведение переменной с точки зрения программиста.


                Copy-on-write же — это внутренняя кухня языка, которая оптимизирует потребление памяти каким-то способом, которого мы не будем сейчас касаться, при передаче по значению.


                Учитывая, что передача по ссылке — это конкретный механизм, мы не можем употреблять этот термин по своему усмотрению. И называть передачу по значению передачей по ссылке будет просто неграмотно. Как и любые попытки выдумать свою собственную терминологию, "если только читается — то по ссылке, а если пишется — то по значению".


                Массивы передаются по значению, а не по ссылке. Точка.


                А вот внутренняя реализация copy-on-write — это уже другой вопрос. Здесь можно порассуждать, но чётко понимая, что говорим мы совсем о другом.


                Но даже и там не поставишь знак равенства. Например, в документации к пятой версии однозначно сказано, что


                If one zval can be used in multiple places, PHP needs some way to find out when the zval is no longer used by anyone, in order to destroy (and free) it. PHP accomplishes this simply by keeping track of how often the zval is referenced. Note that “referenced” here has nothing to do with PHP references (as in &) and just means that something (a variable, function, etc) makes use of the zval.

                В доке по 7 версии этого предложения нет, но зато в разделе, посвящённом передаче по ссылке, написано, что она реализуется с помощью отдельной структуры zend_reference. То есть, насколько я понимаю, это тоже ссылки, но не те, которые использует внутренний механизм Zend по управлению памятью.


                1. diakin
                  09.07.2023 14:00

                  https://ru.wikipedia.org/wiki/Стратегия_вычисления#вызов_по_имени

                  Да там можно закопаться с головой.


  1. LaserPro
    09.07.2023 14:00
    +2

    Их (массивы) точно лучше передавать по ссылке, как PHP передаёт объекты

    Точно лучше? Вообще, явно передавать что-то по ссылке надо, только если вам это действительно нужно. Например, если вам надо изменять в вызываемом методе/функции содержимое огромного массива, так чтобы это изменение сохранилось в вызывающем контексте. Но это весьма частный кейс, и возможно вы что-то делаете не так.

    Используйте for

    Вот так, без каких либо условий, всегда? Так себе совет. Используйте for в тех случаях, когда вы знаете для какой конкретно цели он вам нужен. Например, если вам надо итерируясь по огромному массиву менять его содержимое, так чтобы они сохранилось по завершению цикла... Что также достаточно редкий кейс.


    1. aleksandr-s-zelenin Автор
      09.07.2023 14:00

      Точно лучше?

      Да, это может показаться криминалом и что этого нужно избегать и применять только когда действительно нужно. Но PHP передаёт все объекты по ссылке и ничего, работает. С массивами думаю так же. Я, к слову, к этому неожиданному выводу и пришёл в рамках написания этой статьи. Но я не настаиваю )

      Например, если вам надо итерируясь по огромному массиву менять его содержимое, так чтобы они сохранилось по завершению цикла

      Можно и просто по номеру индекса менять в for'е и всё тоже сохранится. У foreach многовато побочных эффектов, я о них упомянул. Должно было облегчить жизнь, а вышло наоборот. Он только для "посмотреть", но не потрогать.


      1. Ksoo
        09.07.2023 14:00
        +1

        Передача по ссылки очень частая причина багов.Datetime (вместо DatetimeImmutable или DatetimeInterface) первый в списке источников багов, передал в функцию время, а она тебе его поменяла. Или более неявно, ты передал dto, а тебе через геттер в dto поменяли состояние dto.


  1. Ksoo
    09.07.2023 14:00
    +2

    Обратите внимание, что размер массива должен вычисляться только один раз перед массивом, а не на каждой итерации

    Функция count() работает за O(1), вычислений никаких там не будет, только оверхед на вызов функции


    1. mobi
      09.07.2023 14:00

      Там для count даже отдельный опкод, поэтому и накладные расходы на вызов функции сведены к минимуму. Но чтение переменной всё-равно быстрее.


    1. aleksandr-s-zelenin Автор
      09.07.2023 14:00

      Да, есть такое. Массивы в PHP хранят свой размер, а не вычисляют его при каждом вызове.


  1. koreychenko
    09.07.2023 14:00

    Самая мякотка начинается когда у вас массив объектов :-)


    1. Ksoo
      09.07.2023 14:00
      +1

      А что там такого особенного или неочевидного?


      1. koreychenko
        09.07.2023 14:00

        Ну, например, мутабельность.
        Если внутрь какой-нибудь функции передан по значению (на самом деле не важно) массив, например, строк, то пыха создаст копию этого массива и внутри этой функции эти строки массива можно менять как угодно - значение строк в массиве снаружи не изменится. (Что ожидаемо)
        А вот если передан массив объектов, то после изменения этих объектов изнутри функции, снаружи они тоже будут изменены (что не очевидно)

        <?php
        
        class Obj {
        	private string $prop = 'Initial value';
        	
        	public function setProp(string $value)
        	{
        		$this->prop = $value;
        	}
        	
        	public function getProp(): string
        	{
        		return $this->prop;
        	}
        }
        
        function changeProp(array $arrayOfObjects)
        {
        	$arrayOfObjects[0]->setProp('Changed value');
        }
        
        $object = new Obj();
        $arrayOfObjects = [$object];
        
        changeProp($arrayOfObjects);
        
        echo $object->getProp(); // Changed value

        Ну и про память. Поскольку ссылки на объекты в массиве это не сами объект, то, памяти копии тоже занимается не x2 или x3


        1. Ksoo
          09.07.2023 14:00
          +1

          То что вы описываете это базовое поведение объектов в PHP, что переменная содержит ссылку на объект и если ты не делаешь clone то ты работаешь с исходным объектом.


  1. resolution07
    09.07.2023 14:00
    +3

    После таких любителей "оптимизировать" внутреннюю кухню языка порой вешаешься на проекте... Давайте будем честными, на небольших объемах данных в 9 из 10 случаев всем будет насрать на сколько там память увеличилась. А если вы пытаетесь изменить что-то в массиве размером со "слона", то вы явно делаете что-то не так. Я даже не могу представить такую ситуацию)


    1. FanatPHP
      09.07.2023 14:00

      Вот, золотые слова!


  1. inilim2
    09.07.2023 14:00

    function doSmth(array &$array, int $memory) {
    
       printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
       $array[0] = 0;
       printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
     
       foreach ($array as $i => &$value) {
         $array[$i] ++;
         printf('memory: %s, i: %s%s', memory_get_usage() - $memory, $i, PHP_EOL);
         break;
       }
     
       printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);
     }
     
     $memory = memory_get_usage();
     $array = range(0, 99);
     doSmth($array, $memory);
     printf('memory: %s%s', memory_get_usage() - $memory, PHP_EOL);

    memory: 8280
    memory: 8312
    memory: 8344, i: 0
    memory: 8344
    memory: 8344

    Если добавить две ссылки, тогда встает все в норму, но все таки полезно знать.


    1. aleksandr-s-zelenin Автор
      09.07.2023 14:00

      У меня вот такой PHP:

      PHP 8.2.3 (cli) (built: Feb 15 2023 00:30:25) (NTS)
      Copyright (c) The PHP Group
      Zend Engine v4.2.3, Copyright (c) Zend Technologies
          with Xdebug v3.2.1, Copyright (c) 2002-2023, by Derick Rethans
          with Zend OPcache v8.2.3, Copyright (c), by Zend Technologies

      И вот такой вывод:

      memory: 2648
      memory: 2680
      memory: 2712, i: 0
      memory: 2712
      memory: 2712

      Т.е. PHP сразу в 3 раза меньше памяти потребляет, интересно. Покажите ваш php -v. Если уберёте break из вашего кода, то увидите рост.


      1. FanatPHP
        09.07.2023 14:00
        +1

        Не надо просить версию, можно и самому посмотреть
        Тем более что вопрос тут не в абсолютных цифрах, а в относительных.


    1. FanatPHP
      09.07.2023 14:00

      Только не две, а одну. Вторая у вас лишняя, и — как правильно вам ответил автор — приводит к увеличению потребления памяти в вашем коде при полном проходе цикла. А вот если убрать ссылку у $value, то потребление памяти не растёт.


      Причем про первую ссылку в статье и так написано:


      нужно поставить амперсанд перед параметром $array, вот так: function doSmth(array &$array, int $memory)

      Так что не очень понятно, что вы хотели сказать своим комментарием.