PHP. Как увеличить потребление памяти в 3 и более раз при работе с массивами

в 13:26, , рубрики: php, оптимизация кода, потребление памяти, Серверная оптимизация

Содержание

  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 на моём компьютере, но верно ли для вас - вопрос.

Автор: Александр Зеленин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js