Большое использование памяти в цикле PHP: найден твик, но ищем больше - PullRequest
0 голосов
/ 21 февраля 2019

Настройка (пожалуйста, потерпите меня)

У меня есть этот цикл в PHP (PHP 5.6, я смотрю на обновление до 7.3), около 31K записей:

    foreach ($records as $i => $record) {
        $tuple  = call_user_func($processor, $record);

        $keys   = array_flip(
             array_filter(
                 array_keys($tuple),
                 function($key) {
                     return ('.' !== substr($key, 0, 1));
                 }
             )
        );
        if (!empty($keys)) {
            $tuple  = array_intersect_key($tuple, $keys);
            $list[] = $tuple;
        }
    }

Каждый экземпляр $ record составляет около 300 байт, каждый экземпляр $ tuple - около 1 КБ.

Итак, я начинаю цикл с выделением 175 МБ памяти, и я ожидаю, что в конечном итоге поглотит еще 30 КБх 1K = 30 млн.Но, скажем, в десять раз, это будет около 300 м.

Вместо этого, после того, как цикл memory_get_peak_usage() сообщает о сожженных 670 М, и действительно, если я сохраню memory_limit ниже 730 М, процесс PHP взорвется с памятьюошибка исчерпана.

Как я вижу вещи:

    foreach ($records as $i => $record) {
        // No O(n) memory usage
        $tuple  = call_user_func($processor, $record);

        // No O(n) memory usage
        $keys   = array_flip(
             array_filter(
                 array_keys($tuple),
                 function($key) {
                     return ('.' !== substr($key, 0, 1));
                 }
             )
        );
        if (!empty($keys)) {
            // No O(n) memory usage
            $tuple  = array_intersect_key($tuple, $keys);
            // HERE I have O(n) memory usage
            $list[] = $tuple;
        }
    }

Если я закомментирую строку $list[], потребление памяти снизится до тех 175 М. Я подтвердил, что serialize() представление каждого кортежа составляет 2,5 Кб.Таким образом, даже если бы PHP содержал эти значения в неэффективном читабельном формате, на него приходилось бы около 75 мегабайт, а не 500.

Одно открытие

Я подумал, что, возможно, был некоторый "провал"в распределении объектов PHP.Поэтому я изменил эти строки:

            $tuple  = array_intersect_key($tuple, $keys);
            $list[] = $tuple;

Кому:

            $tuple  = array_intersect_key($tuple, $keys);
            $list[] = unserialize(serialize($tuple));

, полагая, что unserialize() создаст «свежий» объект словаря PHP (для этого $tuple),Да, я применяю подход «форматировать и переустанавливать» voodoo к объектам PHP.

Действительно, потребление памяти уменьшилось с 688М до 511М, тогда как возвращаемые данные остались прежними (я выкидываю их в файл JSONи запустите md5sum для результата).

Неожиданно, с двумя дополнительными вызовами, скрипт также становится примерно на 2-5% быстрее .

Это говорит мнечто за кулисами должно быть много управления памятью, к которому я не причастен.Кроме того, я, должно быть, только что поцарапал поверхность (511 М далеко от 175 + 75 = 250 М, что на все же больше, чем самое необходимое, но я бы согласился).Может быть - есть, вероятно, будет - но больше памяти и скорости тратится где-то там.

Интересное примечание : неудивительно, что $keys = unserialize(serialize($keys));, хотя и не влияет на использование памяти, заметно увеличиваетсяскорость этого array_intersect_keys - я бы осмелился сказать еще 2% (около 3 секунд при среднем времени ответа командной строки 150 секунд).

Вопрос (ы)

Есть ли какой-то способ дальнейшего повышения эффективности памяти?Могу ли я попробовать какую-нибудь эзотерическую настройку PHP.INI?

Возможно, наиболее важно, является ли это известной ошибкой (я ничего не нашел в журналах изменений PHP), и, возможно, она исправлена ​​в 7.3 (или не имеет значения из-за изменений стратегии памяти), такэто не стоит преследовать?

  • Я не могу использовать итератор вместо цикла над массивом
  • Мне уже неудобно с текущим значением memory_limit (а количество записей связано сувеличение. Я бы предпочел не держать память, чтобы побаловать недостаток дизайна).
  • использование $tuple в качестве промежуточного значения вместо непосредственного сохранения в $list[] не меняет ни скорость, ни памятьused

Пример записи кортежа

Запись генерируется функцией процессора, она выглядит следующим образом:

  1. У меня есть словарь в каждой записи $, который может иметь или не иметь все ключи, начинающиеся с «Evt_» («Данные события»).Процессор обеспечивает присутствие всех ключей, при необходимости добавляя ключи со значениями NULL после словаря.
  2. Процессор также добавляет еще несколько ключей из других словарей, определенных на основе ключей Evt.Эти словари полны, возможно, все их значения имеют значения по умолчанию.В частности: если Evt_IdeOut имеет значение NULL, то все значения Out_ * будут получены из $ Out_Default и т. Д.Данные Out_ * относятся к географическому положению, Vnd_ * - это атрибуты, Age и Usr относятся к полевому агенту и сообщающему пользователю.Поля _Ipa - это IP-адреса, _Dat - даты в итальянском формате (д / м / ГЧ: i).

(Как вы могли догадаться, это сопоставление для LEFT JOIN).

Один пример полного кортежа $, возвращаемого процессором:

array(56) {
  ["recid"]=>
  int(2019022020175919710)
  ["Evt_IdeHea"]=>
  int(2019022020175919710)
  ["Evt_IdeVnd"]=>
  string(13) "REDACTED....."
  ["Evt_IdePdc"]=>
  string(11) "REDACTED..."
  ["Evt_IdeRcp"]=>
  string(28) "REDACTED...................."
  ["Evt_IdeOut"]=>
  string(40) "REDACTED................................"
  ["Evt_Dat"]=>
  string(10) "20/02/2019"
  ["Evt_IdeAge"]=>
  string(11) "REDACTED..."
  ["Evt_Des"]=>
  string(20) "Some text string at most 60 char long"
  ["Evt_Not"]=>
  string(39) "Some text string at most 512 char long"
  ["Evt_Con"]=>
  string(0) ""
  ["Evt_IdeMrc"]=>
  int(99999)
  ["Evt_Lck"]=>
  int(0)
  ["Evt_Qua"]=>
  int(0)
  ["Evt_VarUte"]=>
  string(11) "REDACTED"
  ["Evt_VarTot"]=>
  int(1)
  ["Evt_VarIpa"]=>
  string(12) "127.0.0.1"
  ["Evt_VarDat"]=>
  string(16) "20/02/2019 20:17"
  ["Evt_SttRcd"]=>
  int(0)
  ["Evt_CreUte"]=>
  string(11) "REDACTED"
  ["Evt_CreIpa"]=>
  string(12) "192.168.999.42"
  ["Evt_CreDat"]=>
  string(16) "20/02/2019 20:17"
  ["Out_IdeRcp"]=>
  string(28) "REDACTED...................."
  ["Out_IdePdc"]=>
  string(11) "REDACTED..."
  ["Out_IdeHea"]=>
  string(40) "REDACTED................................"
  string(40) "REDACTED................................"
  ["Out_Des"]=>
  string(20) "REDACTED (MAX 50)..."
  ["Out_Att"]=>
  string(8) "REDACTED"
  ["Out_Reg"]=>
  string(8) "PIEDMONT"
  ["Out_Pro"]=>
  string(2) "CN"
  ["Out_Not"]=>
  NULL
  ["Out_Lng"]=>
  string(9) "8.0000000"
  ["Out_Lat"]=>
  string(10) "44.7000000"
  ["Out_Ind"]=>
  string(13) "STREETADDRESS"
  ["Out_Cit"]=>
  string(4) "Alba"
  ["Out_Cap"]=>
  string(5) "12051"
  ["Out_VarUte"]=>
  string(3) "Sys"
  ["Out_VarTot"]=>
  int(942)
  ["Out_VarIpa"]=>
  string(7) "0.0.0.0"
  ["Out_VarDat"]=>
  string(16) "20/02/2019 23:44"
  ["Out_SttRcd"]=>
  int(0)
  ["Out_CreUte"]=>
  string(3) "Sys"
  ["Out_CreIpa"]=>
  string(7) "0.0.0.0"
  ["Out_CreDat"]=>
  string(16) "26/07/2016 23:44"
  ["Vnd_IdeHea"]=>
  string(13) "REDACTED....."
  ["Vnd_Lin"]=>
  string(2) "L2"
  ["Vnd_Des"]=>
  string(18) "REDACTED.........."
  ["Vnd_VarUte"]=>
  string(6) "SYSTEM"
  ["Vnd_VarTot"]=>
  int(1)
  ["Vnd_VarIpa"]=>
  string(7) "0.0.0.0"
  ["Vnd_VarDat"]=>
  string(16) "04/06/2016 10:16"
  ["Vnd_SttRcd"]=>
  int(0)
  ["Vnd_CreUte"]=>
  string(6) "SYSTEM"
  ["Vnd_CreIpa"]=>
  string(7) "0.0.0.0"
  ["Vnd_CreDat"]=>
  string(16) "16/03/2016 14:21"
  ["Age_Des"]=>
  string(14) "REDACTED......"
  ["Usr_Des"]=>
  string(17) "REDACTED........."
}

Итак, я начинаю с около 31K записей только с ключами Evt_ и заканчиваю тем же количеством записей со всемиключи выше.Сериализованная версия записи варьируется от 1819 до 4120 байт, при средней длине около 2350, поэтому размеры в приведенной выше записи довольно типичны.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...