php - PHP循环中的大量内存使用:找到了一个调整,但正在寻找更多
问题描述
设置(请多多包涵)
我在 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 Kb。
因此,我以分配 175 M 的内存开始循环,我希望最终会吞噬另外 30K x 1K = 30M。但是假设十倍,它将是大约 300 M。
相反,在循环memory_get_peak_usage()
报告 670 M 被烧毁之后,如果我将 memory_limit 保持在 730 M 以下,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 M。我已经确认serialize()
每个元组的表示大小为 2.5 Kb。因此,即使 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
)。是的,我正在对 PHP 对象应用“格式化并重新安装”voodoo 方法。
实际上,内存消耗从 688M 减少到 511M,而返回的数据保持不变(我将它们转储到 JSON 文件并md5sum
在结果上运行)。
出乎意料的是,通过两个额外的调用,脚本也变得快了 2-5% 左右。
这告诉我,一定有很多我不知道的幕后内存管理正在进行。此外,我一定只是触及了表面(511 M 与 175 + 75 = 250 M 相差甚远,这仍然超过了基本必需品,但我会接受)。可能存在 - 很可能存在 - 更多的内存和速度在某处被浪费了。
有趣的旁注:不出所料,$keys = unserialize(serialize($keys));
虽然不影响内存使用,但显着提高了速度array_intersect_keys
- 我敢说另外 2%(在 150 秒的平均命令行响应时间上大约 3 秒)。
问题
不知道有没有办法进一步提高内存效率?我可以尝试一些深奥的 PHP.INI 设置吗?
也许最重要的是,这是一个已知的错误(我在 PHP 更改日志上没有发现任何东西),并且可能在 7.3 中修复(或由于内存策略更改而无关),所以不值得追求?
- 我不能使用迭代器代替数组上的循环
- 我已经对当前的 memory_limit 感到不舒服(并且记录的数量会增加。我不想继续占用内存来纵容设计缺陷)。
- 使用
$tuple
作为中间值而不是直接存储到$list[]
不会显着改变速度或使用的内存
样本元组记录
记录由处理器函数生成,如下所示:
- 我在每个 $record 中都有一个字典,它可能有也可能没有以“Evt_”(“事件数据”)开头的所有键。处理器确保所有键都存在,并根据需要在字典后面附加 NULL 值键。
- 处理器还从其他字典中添加了几个键,这些键是根据 Evt 键确定的。这些字典是完整的,可能它们的所有值都有默认值。具体来说:如果 Evt_IdeOut 具有 NULL 值,则所有 Out_* 值都将来自 $Out_Default,依此类推。Out_* 数据指的是地理位置,Vnd_* 是该地理位置的属性,Age 和 Usr 指的是现场代理和报告用户。_Ipa 字段是 IPAddresses,_Dat 是意大利语格式的日期 (d/m/YH:i)。
(您可能已经猜到,这是 LEFT JOIN 的映射)。
处理器返回的完整 $tuple 的一个示例是:
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........."
}
因此,我从仅具有 Evt_ 键的大约 31K 记录开始,并以具有上述所有键的相同数量的记录结束。记录的序列化版本范围从 1819 到 4120 字节,平均长度在 2350 左右,因此上面记录中的大小非常典型。
解决方案
推荐阅读
- java - 如何在 Java 中使用 Swing 修复元素的显示
- python - 如何比较日期记录并将新列添加到数据框作为标准
- c++ - 带有 if 语句的嵌套 for 循环的时间复杂度
- python - 尝试为 dokan_profile_settings 周围的自动化创建 CSV 文件,但在引用变得混乱时遇到问题
- google-material-icons - 材料图标问题:Apache Royale 上没有图片图标
- python - 使用 Python 组合在选项卡中包含图表的单独 Excel 工作表
- kubernetes - Knative:混淆服务名称和路由
- python - 使用 PyTorch 在多个 GPU 上并行进行 Tensor Inverse
- vuejs2 - VueJS | 数据正确显示问题(v-if、v-else)- 外部服务器
- angular - NgRx effect 只监听最后返回的 observable,忽略任何之前返回的 observable