Содержание.
-
Общие сведения.
-
Увеличиваем потребление памяти вдвое.
-
Увеличиваем потребление памяти втрое.
-
Ещё раз увеличиваем потребление памяти на ровном месте.
-
Заключение.
Общие сведения.
Известно, что 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: 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 на моём компьютере, но верно ли для вас — вопрос.
ссылка на оригинал статьи https://habr.com/ru/articles/746868/
Добавить комментарий