Настройка (пожалуйста, потерпите меня)
У меня есть этот цикл в 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
Пример записи кортежа
Запись генерируется функцией процессора, она выглядит следующим образом:
- У меня есть словарь в каждой записи $, который может иметь или не иметь все ключи, начинающиеся с «Evt_» («Данные события»).Процессор обеспечивает присутствие всех ключей, при необходимости добавляя ключи со значениями NULL после словаря.
- Процессор также добавляет еще несколько ключей из других словарей, определенных на основе ключей 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, поэтому размеры в приведенной выше записи довольно типичны.