首页 > 解决方案 > 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 中修复(或由于内存策略更改而无关),所以不值得追求?

样本元组记录

记录由处理器函数生成,如下所示:

  1. 我在每个 $record 中都有一个字典,它可能有也可能没有以“Evt_”(“事件数据”)开头的所有键。处理器确保所有键都存在,并根据需要在字典后面附加 NULL 值键。
  2. 处理器还从其他字典中添加了几个键,这些键是根据 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 左右,因此上面记录中的大小非常典型。

标签: phpmemoryoptimization

解决方案


推荐阅读