Содержание
Общие сведения.
Увеличиваем потребление памяти вдвое.
Увеличиваем потребление памяти втрое.
Ещё раз увеличиваем потребление памяти на ровном месте.
Заключение.
Общие сведения
Известно, что 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 ++)
По ходу пьесы обнаружил ещё две интересные статьи на Хабре:
Сравнение производительности перебора массивов в цикле через for() и foreach(). Так ли это для 8-ой версии не знаю, не проверял.
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 на разных версиях одновременно. Итого:
сколько памяти PHP потребляет на массив из 100 элементов, который я использую в этой статье: https://3v4l.org/RK5sK (тут в байтах).
сколько памяти PHP потребляет на массив из 100000 элементов, который используется в статье «Насколько большие массивы в PHP?»: https://3v4l.org/c377f (тут в мегабайтах).
Комментарии (45)
LaserPro
09.07.2023 14:00+2Их (массивы) точно лучше передавать по ссылке, как PHP передаёт объекты
Точно лучше? Вообще, явно передавать что-то по ссылке надо, только если вам это действительно нужно. Например, если вам надо изменять в вызываемом методе/функции содержимое огромного массива, так чтобы это изменение сохранилось в вызывающем контексте. Но это весьма частный кейс, и возможно вы что-то делаете не так.
Используйте
for
Вот так, без каких либо условий, всегда? Так себе совет. Используйте for в тех случаях, когда вы знаете для какой конкретно цели он вам нужен. Например, если вам надо итерируясь по огромному массиву менять его содержимое, так чтобы они сохранилось по завершению цикла... Что также достаточно редкий кейс.
aleksandr-s-zelenin Автор
09.07.2023 14:00Точно лучше?
Да, это может показаться криминалом и что этого нужно избегать и применять только когда действительно нужно. Но PHP передаёт все объекты по ссылке и ничего, работает. С массивами думаю так же. Я, к слову, к этому неожиданному выводу и пришёл в рамках написания этой статьи. Но я не настаиваю )
Например, если вам надо итерируясь по огромному массиву менять его содержимое, так чтобы они сохранилось по завершению цикла
Можно и просто по номеру индекса менять в
for
'е и всё тоже сохранится. Уforeach
многовато побочных эффектов, я о них упомянул. Должно было облегчить жизнь, а вышло наоборот. Он только для "посмотреть", но не потрогать.Ksoo
09.07.2023 14:00+1Передача по ссылки очень частая причина багов.Datetime (вместо DatetimeImmutable или DatetimeInterface) первый в списке источников багов, передал в функцию время, а она тебе его поменяла. Или более неявно, ты передал dto, а тебе через геттер в dto поменяли состояние dto.
Ksoo
09.07.2023 14:00+2Обратите внимание, что размер массива должен вычисляться только один раз перед массивом, а не на каждой итерации
Функция count() работает за O(1), вычислений никаких там не будет, только оверхед на вызов функции
mobi
09.07.2023 14:00Там для
count
даже отдельный опкод, поэтому и накладные расходы на вызов функции сведены к минимуму. Но чтение переменной всё-равно быстрее.
aleksandr-s-zelenin Автор
09.07.2023 14:00Да, есть такое. Массивы в PHP хранят свой размер, а не вычисляют его при каждом вызове.
koreychenko
09.07.2023 14:00Самая мякотка начинается когда у вас массив объектов :-)
Ksoo
09.07.2023 14:00+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
Ksoo
09.07.2023 14:00+1То что вы описываете это базовое поведение объектов в PHP, что переменная содержит ссылку на объект и если ты не делаешь clone то ты работаешь с исходным объектом.
resolution07
09.07.2023 14:00+3После таких любителей "оптимизировать" внутреннюю кухню языка порой вешаешься на проекте... Давайте будем честными, на небольших объемах данных в 9 из 10 случаев всем будет насрать на сколько там память увеличилась. А если вы пытаетесь изменить что-то в массиве размером со "слона", то вы явно делаете что-то не так. Я даже не могу представить такую ситуацию)
inilim2
09.07.2023 14:00function 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Если добавить две ссылки, тогда встает все в норму, но все таки полезно знать.
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
из вашего кода, то увидите рост.FanatPHP
09.07.2023 14:00+1Не надо просить версию, можно и самому посмотреть
Тем более что вопрос тут не в абсолютных цифрах, а в относительных.
FanatPHP
09.07.2023 14:00Только не две, а одну. Вторая у вас лишняя, и — как правильно вам ответил автор — приводит к увеличению потребления памяти в вашем коде при полном проходе цикла. А вот если убрать ссылку у $value, то потребление памяти не растёт.
Причем про первую ссылку в статье и так написано:
нужно поставить амперсанд перед параметром $array, вот так:
function doSmth(array &$array, int $memory)
Так что не очень понятно, что вы хотели сказать своим комментарием.
FanatPHP
Есть хорошее эмпирическое правило: не писать статей о том, про что ты узнал всего несколько месяцев назад. Лучше подождать годик или два. Ну или хотя бы разобрать тему повнимательнее.
Впрочем, у вас почти получилось, про память в целом написано верно. Кроме путаницы со ссылками.
Называть copy-on-write передачей по ссылке — это упрощение, граничащее с невежеством. Вас за него закидают тухлыми камнями, и поделом.
Это два совершенно разных механизма, и то, что вы их путаете, говорит о том что вы не поняли оба. Ну как можно утверждать, что массивы передаются по ссылке, если исходный массив не меняется после его изменения внутри функции?
И про скаляры вы тоже попали пальцем в небо (особенно про "не занимают много места", ха-ха), хотя могли бы элементарно проверить, заточив свой пример под передачу строк вместо массивов. И убедиться, что в отношении потребления памяти скаляры ведут себя совершенно идентично массивам — не занимают места, пока не поменялись, и удваивают потребление памяти при изменении. В чем, собственно, и заключается суть copy-on-write.
Но это детали, а основная мысль статьи передана верно. Ничего странного, кстати, в ней нет. Есть такой жанр, "вредные советы", и уже из заголовка он прекрасно считывается.
aleksandr-s-zelenin Автор
А что я там напутал?
Я нигде не называл copy-on-write передачей по ссылке.
В этом и есть суть copy-on-write. Как только вы решите изменить массив переданный в функцию PHP создаст копию исходного массива. Поэтому исходный и останется без изменений. Что собственно и подтверждается приведённым выше экспериментом. Про это написано тут.
Я опять же не писал, что скаляры ведут себя как-то отдельно и что их копии не создаются. Создаются, просто они маленькие по памяти. Но про строки вы верно заметили, это я поправлю в статье. 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.
FanatPHP
Ну вот это вы зря, конечно. Стоило перечитать свой текст, перед тем как отвечать. Возможно, у себя в голове вы действительно не путаете (хотя я уже начинаю в этом сомневаться). Но я-то в любом случае не могу знать, что вы думаете, а вижу только то, что вы пишете. А пишете вы откровенную чушь.
Ну как не называли-то? Вот же, прямым текстом, первая фраза в разделе "Увеличиваем потребление памяти вдвое":
Данное утверждение является на 100% неверным. Массивы не передаются по ссылке. Вы называете передачей по ссылке передачу по значению с использованием copy-on-write. То есть, путаете эти два механизма.
А вы точно автор этой статьи? В ней написано, что ведут:
Здесь вы явно выделяете скаляры в отдельную категорию, поведение которой отличается от поведения массивов.
Правильно. Вы писали, что создаются. Что, опять же, является 100% неверным. При передаче копия скаляра в памяти не создаётся. А создаётся при записи.
Плюс чуть раньше вы повторяете то же самое:
То есть опять вы заявляете, что массивы передаются по ссылке, а все остальное (фактически, остаются только скаляры) — по значению.
На всякий случай повторю: Данное утверждение является дважды неверным.
Массивы передаются не по ссылке
Передача "всего остального" (т.е., по контексту статьи — скаляров) не отличается от передачи массивов. И те и другие передаются по значению, с использованием copy-on-write.
aleksandr-s-zelenin Автор
Действительно, в документации, кажется, нигде явно не написано, что массивы передаются по ссылке. На 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? И там есть вот такие строки (вырвано из контекста, нужно ознакомиться с разделом целиком):
Т.е. подразумевается, что массив передался по ссылке и теперь две переменные указывают на один и тот же zval.
Я склонен считать, что массивы в PHP передаются по ссылке всегда.
FanatPHP
Ага, вот теперь мы уже сильно ближе к пониманию.
То есть вы действительно путаете передачу по ссылке с copy-on-write.
И, следуя вашему ходу мысли, придется заключить что строки-таки тоже передаются по ссылке. Верно?
Но мне кажется, я знаю, как вам объяснить разницу. Собственно, вам надо просто попытаться её сформулировать. Вербализовать. Вот просто поставьте рядом в голове эти два механизма, и примите за аксиому, что они разные. И попробуйте эту разницу сформулировать. Мне кажется, это должно сработать. Особенно если помнить, что при передаче по ссылке изменения в одном из экземпляров переменной отражаются и во всех остальных.
Кроме этого, можете попытаться объяснить, в чем смысл передавать массив в foreach по ссылке… если — как вы утверждаете — он и так передается по ссылке! ;)
По источникам: доверять какому-то васе пупкину, который решил выступить на стаковерфлое, всегда следует с осторожностью. Из всех приведенных вами источников следует читать только то, что пишет Никита про 7 версию.
aleksandr-s-zelenin Автор
Баста! Я придумал как доказать это:
Вывод:
Разницу в 2680 - 2616 = 64 опустим как несущественную.
Внутри
passByReference
как видно объём не увеличился вдвое. Почему? Потому что массив передаётся по ссылке.Внутри
passByValue
как видно объём не увеличился вдвое. Почему? По той же самой причине: массив передаётся по ссылке.@FanatPHP, как вы объясните наблюдаемые данные?
FanatPHP
Ага, а у таракана уши в ногах.
aleksandr-s-zelenin Автор
Отличный анекдот, кстати )
Только это не объясняет почему в обоих случаях потребление памяти осталось одинаковым. Ведь очевидно, что копия сделана не была.
FanatPHP
Почему не объясняет? Как раз отлично объясняет. Вы точно так же из объективных данных эксперимента делаете неверные выводы.
Ход мысли ну совершенно же идентичный.
Ваша проблема в том, что вы смотрите на ситуацию исключительно с точки зрения потребления памяти. И ни на что больше не обращаете внимание.
Знаете, я как-то подарил племянникам лазерные шахматы, Khet. Отличная игра, кстати. И часто проигрывал. Потому что мысленно выстраивая путь лазера, смотрел только на фигуры противника, и забывал про свои.
У вас здесь то же самое. Вы пытаетесь объяснить поведение РНР, и по-своему, в своем узком контексте правы. Но вы забываете, что передача по ссылке — это совершенно конкретный механизм языка, который здесь НЕ используется. А используется какой-то другой. Который хотя и имеет сходную природу, но всё равно совершенно отдельный. Именно поэтому ваши заявления выглядят так дико. Вам необходимо использовать другую терминологию. Передача по ссылке уже занята.
Попробуйте выйти из своего контекста, и взглянуть на свои утверждения с чистого листа. Вот представьте, что кто-то вас останавливает в коридоре и говорит, что в РНР массивы и строки передаются по ссылке. Представили? Вот именно это состояние охудивления и испытывают все, кто читает ваш опус.
Hardcoin
Строки тоже "передаются по ссылке". Вы можете это проверить тем же самым кодом.
SuperCat911
С большим интересом читаю Ваши комментарии. Но я уже сам запутался. Можно как-то доступнее? :)
П.С. Я уже на старте проверять код на массивы и форичи.
FanatPHP
Ну вот я как раз написал только что последний. Хотя писал уже об этом и раньше. Автор путает сам язык и его внутреннюю реализацию. По ссылке передаются только объекты. Массивы передаются по значению. При этом не происходит тупого копирования, а память часто экономится за счет механизма copy-on-write. Внутри себя этот механизм использует механизм ссылок. Но говорить при из-за этого, что массивы передаются по ссылке — неграмотно.
И сразу сделали неправильно.
Проверять надо не "массивы и форичи", а их размер. Если вы в коде работаете с огромным массивом, то сначала надо разобраться, откуда он взялся, и можно ли его уменьшить. Именно размер в данном случае имеет значение, и обработка такого массива в любом случае будет отнимать много ресурсов. Поэтому таких ситуаций следует избегать. И только в редких случаях, когда обойтись без изменения огромного массива не получится, можно применять ухищрения, описанные в статье.
SuperCat911
Большое спасибо за подробное разъяснение.
amberovsky
Может у меня получится прояснить разницу.
Вот есть функция. У неё есть аргументы. Есть код, который вызывает это функцию и передаёт какие-то переменные.
Вопрос - а что по факту передаётся в функцию?
Ответ - по факту это всегда некий адрес в памяти. Массив, int, объект, и т.д. - это всё просто некий адрес в памяти, а дальше уже компилятор разруливает как вызвать метод объекта по этому адресу памяти или как трактовать это как элемент массива.
На заре программирования думали-думали и решили сделать два способа передачи параметров (забавный факт - в компуктерах PDP-11 было аж 42 способа адресовать аргумент) :
1) По ссылке - когда функция получает ровно тот же адрес переменной, который был когда вызвали функцию. В данном случае компилятор просто копирует адрес переменной.
2) По значению - когда функция получает совершенной другой адрес переменной при вызове. В данном случае компилятор выполняет полное копирование переменной.
После это стали думать ещё. А вдруг аргумент функции не изменяется, а мы передаём его по значению. Если это огромный массив - то мы зря делаем полную копию массива.
Пришла гениальная идея - а давайте не будем делать полную копию, вместо этого передадим по факту по ссылке, дождёмся пока код решит что-то записать в эту переменную, и только тогда сделаем полную копию (ну и подвиснем ещё на некоторое время). В итоге компилятор вставляет дополнительный код чтобы разруливать такие сценарии.
Итого: передача аргумента (в более широком смысле - адресация переменных) и стратегия cope-on-write разные вещи, но существуют в одном контексте. Поэтому их часто употребляют как взаимозаменямо. Но их гораздо больше!
diakin
Написано же "Но.." Не вижу, что ТС что-то путает .
Hardcoin
Это не передача по ссылке.
Для понимания разницы обратите внимание, как работает передача объекта в функцию - объекты передаются по ссылке и разница с массивами огромна.
diakin
Да понятно, что если "будет создана копия массива", то это не по ссылке.
Если в функции массив только читается - то по ссылке. А если в функции в массив что-то пишется - то создается копия. Так наверное?
Hardcoin
Это нельзя назвать передачей по ссылке. "Передача по ссылке" - конкретный термин имеющий вполне точное значение. Передача по ссылке позволяет функции изменять переменную без создания копии. А в данном случае изменить (без создания копии) не получится, значит это не оно.
diakin
Это уже проблемы терминологии.
Передача по ссылке позволяет функции иметь доступ к переменной без создания копии.
При передаче по значению создается локальная переменная, в которую копируется внешняя.
А если при передаче по ссылке доступ только на чтение, то это проблемы конкретного языка.
Иначе придется вводить третий термин, ведь получатся и не по ссылке, и не по значению.
Hardcoin
Разумеется это вопрос терминологии. И есть общепринятая. ТС путает именно использование общепринятой терминологии.
Так-то легко можно сказать, что объекты передаются по значению, потому что значимый объект передается внутрь функции. Просто вас не поймут, если вы будете так говорить.
В php доступ не только на чтение (это означало бы ошибку при попытке изменить переменную). Доступ только на чтение у констант. Сравните разницу.
Верно, этот третий термин ввели. Copy-on-write.
aleksandr-s-zelenin Автор
Copy-on-write - это не третий и никакой вообще способ передачи. Это механизм позволяющий оттянуть или вовсе избежать копирования параметров функции. Он опционален. И без COW можно реализовать передачу по ссылке и по значению.
Что касается терминологии, то возможно, следовало упомянуть в статье, что имеется ввиду по ссылкой (я подправлю потом текст). В «Reference Counting Basics» сказано:
Я имею ввиду именно те самые внутренние ссылки - internal reference - которые можно явно задать при помощи амперсанда или те, которые PHP создаст сам, где посчитает нужным. В данном случае он создаёт внутреннюю ссылку (не внутрь куда-то, а другую, не user-land) на массив при передаче его в функцию по значению.
Hardcoin
Нет, не опционален. Вы не можете включить в php классическую передачу по значению.
Конечно можно. Но в php не реализована классическая передача по значению, только cow.
Для массивов? А для объектов вы тоже имеете ввиду именно внутренние ссылки? Они ведь передаются не так, как массивы, а вы назвали их одинаковым термином.
Сами запутались и других путаете - это не страшно, бывает. Но вы продолжаете настаивать на своей нестандартной терминологии неясно зачем.
aleksandr-s-zelenin Автор
Я устал с вами с спорить. Вы не ответили на вопрос как объяснить то, что при передаче массива в функцию и по ссылке и по значению кол-во потребляемой памяти не увеличивается. Потому что ответ очевиден, но вы не хотите признать эту очевидность, потому что всегда считали, что массив передаётся по значению. И ввиду нежелания принять этот факт решили свести всё к терминологическому спору. Моя статья не про точность терминов, а про то, как работать с массивами не вызывая чрезмерного потребления памяти. И эти чёртовы массивы в PHP передаются по ссылке, указателю или ещё какому-то фантомному референсу не вызывая копирования, как это ожидается, при передаче по значению. Потому что они передаются не по значению.
Вот ваше расписание на неделю:
Hardcoin
Вы, кажется, этот вопрос задавали не мне? Но ответ простой - copy-on-write копирует не сразу, а только при попытке изменения. Поэтому и не увеличивается. Конечно при чтении php пользуется тем же самым блоком памяти. Пользуется тем же адресом, если угодно.
Да, очевиден, php не копирует область памяти без необходимости.
Как видите, признал. Теперь у вас пропал повод для бессмысленных переходов на личности?
Кем ожидается? Вами? При cow копирование ожидается только при изменении, не раньше. Вы это знаете, я это знаю. Так к чему этот странный оборот "ожидается"?
На мой вопрос, пожалуйста, тоже ответьте. Нехорошо указывать оппоненту на неотвеченный вопрос, а самому при этом вопрос проигнорировать.
aleksandr-s-zelenin Автор
Я прошу прощения, я вас с @FanatPHPперепутал. А ожидается как минимум мной потому что в документации сказано, что by default, variables are always assigned by value. А тонкие материи там освещены так себе. Поэтому и ожидается... дефолтное поведение.
FanatPHP
Правильно!!!
И вот в случае с вашими тараканами происходит именно ЭТО! А никакая не передача по ссылке. Которая происходит по значению.
Вы упорно путаете сам язык и его внутреннюю реализацию. Массивы передаются по значению. Запомните уже это наконец! Но память при этом экономится. За счет cow. Это будет корректное описание ситуации. А не
у таракана уши в ногах"массивы передаются по ссылке".Вы пытаетесь коряво сформулировать причину экономии памяти, но вместо "с использованием механизма Copy-on-write, который там где-то внутри себя использует механизм ссылок" постоянно повторяете эту чушь, "массивы передаются по ссылке!".
Если уж на то пошло, вы говорите не о массивах, а о структурах zval.
Вы опять все перепутали. Ссылки, которые можно задать при помощи амперсанда — это механизм языка. Который реализуется с помощью internal reference. У вас логика на уровне "Марк Цукерберг и Вася Пупкин оба произошли от обезьяны. Поэтому Марк Цукерберг и Вася Пупкин — это один и тот же человек!".
Причем эти самые "internal reference" в при реализации передачи по ссылке и при реализации cow — тоже разные, насколько я могу судить. А вы валите всё в кучу.
diakin
Ну в общем-то не конкретного языка, я погорячился.
https://ru.wikipedia.org/wiki/Копирование_при_записи
Механизм копирования при записи (англ. Copy-on-write, COW) используется для оптимизации многих процессов, происходящих в операционной системе, таких как, например, работа с оперативной памятью или файлами на диске...
FanatPHP
Вы, как и автор, путаете сам язык с его внутренними оптимизациями.
Смотрите, передача по ссылке — это не просто "термин". А конкретный механизм языка, совершенно определённый, не допускающий интерпретаций и толкований. Который описывает поведение переменной с точки зрения программиста.
Copy-on-write же — это внутренняя кухня языка, которая оптимизирует потребление памяти каким-то способом, которого мы не будем сейчас касаться, при передаче по значению.
Учитывая, что передача по ссылке — это конкретный механизм, мы не можем употреблять этот термин по своему усмотрению. И называть передачу по значению передачей по ссылке будет просто неграмотно. Как и любые попытки выдумать свою собственную терминологию, "если только читается — то по ссылке, а если пишется — то по значению".
Массивы передаются по значению, а не по ссылке. Точка.
А вот внутренняя реализация copy-on-write — это уже другой вопрос. Здесь можно порассуждать, но чётко понимая, что говорим мы совсем о другом.
Но даже и там не поставишь знак равенства. Например, в документации к пятой версии однозначно сказано, что
В доке по 7 версии этого предложения нет, но зато в разделе, посвящённом передаче по ссылке, написано, что она реализуется с помощью отдельной структуры zend_reference. То есть, насколько я понимаю, это тоже ссылки, но не те, которые использует внутренний механизм Zend по управлению памятью.
diakin
https://ru.wikipedia.org/wiki/Стратегия_вычисления#вызов_по_имени
Да там можно закопаться с головой.