Содержание.
Общие сведения.
Увеличиваем потребление памяти вдвое.
Увеличиваем потребление памяти втрое.
Ещё раз увеличиваем потребление памяти на ровном месте.
Заключение.
Общие сведения.
Известно, что 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. Т.о. вы частично или полностью теряете контроль над своей программой. Поэтому и придумали компромис: скаляры всегда передаём по значению, а объекты по ссылке.
Увеличиваем потребление памяти вдвое.
Массивы в 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: 26802680 - 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: 2648foreach вообще достаточно проблемная конструкция для синтаксического сахара. Без проблем её можно использовать только для "посмотреть" на каждой итерации, "сделать" же что-то обходится слишком дорого:
О тонкостях работы foreach в PHP
Подводный камень в 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 на моём компьютере, но верно ли для вас - вопрос.
