首页 > 解决方案 > 为什么实习全局字符串值会导致每个多处理进程使用更少的内存?

问题描述

我有一个 Python 3.6 数据处理任务,该任务涉及预加载一个大字典,用于按 ID 查找日期,以便在后续步骤中由多处理模块管理的子进程池使用。这个过程消耗了盒子上的大部分内存,所以我应用的一个优化是“实习”存储在字典中的字符串日期。正如我预期的那样,这将 dict 的内存占用减少了几个 GB,但它也产生了另一个意想不到的效果。

在应用实习之前,子进程在执行时会逐渐消耗越来越多的内存,我认为这是因为他们不得不将字典从全局内存逐渐复制到子进程的单独分配内存(这是运行在Linux 等受益于 fork()) 的写时复制行为。即使我没有更新子进程中的字典,看起来只读访问仍然可以通过引用计数触发写时复制

我只是希望实习可以减少 dict 的内存占用,但实际上它也阻止了内存使用量在子进程生命周期内逐渐增加

这是我能够构建的一个最小示例,它复制了该行为,尽管它需要一个大文件来加载和填充字典,并且在值中进行足够量的重复以确保实习提供好处。

import multiprocessing
import sys

# initialise a large dict that will be visible to all processes
# that contains a lot of repeated values
global_map = dict()
with open(sys.argv[1], 'r', encoding='utf-8') as file:
  if len(sys.argv) > 2:
    print('interning is on')
  else:
    print('interning is off')
  for i, line in enumerate(file):
    if i > 30000000:
      break
    parts = line.split('|')
    if len(sys.argv) > 2:
      global_map[str(i)] = sys.intern(parts[2])
    else:
      global_map[str(i)] = parts[2]

def read_map():
  # do some nonsense processing with each value in the dict
  global global_map
  for i in range(30000000):
    x = global_map[str(i)]
  y = x + '_'
  return y

print("starting processes")
process_pool = multiprocessing.Pool(processes=10)

for _ in range(10):
  process_pool.apply_async(read_map)

process_pool.close()

process_pool.join()

我运行了这个脚本并进行了监控htop以查看总内存使用情况。

实习? 打印“启动进程”后的内存使用情况 之后的峰值内存使用
7.1GB 28.0GB
是的 5.5GB 5.6GB

虽然我很高兴这种优化似乎一次解决了我所有的内存问题,但我想更好地了解它为什么有效。如果子进程的内存使用量下降到写时复制,那么如果我对字符串进行实习,为什么不会发生这种情况?

标签: pythonlinuxmultiprocessing

解决方案


CPython实现将内部字符串存储在一个全局对象中,该对象是一个常规 Python 字典,其中键和值都是指向字符串对象的指针。

当创建一个新的子进程时,它会获取父进程地址空间的副本,因此它们将使用带有内部字符串的缩减数据字典。

我已经使用下面的补丁编译了 Python,如您所见,两个进程都可以访问带有内部字符串的表:

测试.py:

import multiprocessing as mp
import sys
import _string


PROCS = 2
STRING = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"


def worker():
    proc = mp.current_process()
    interned = _string.interned()

    try:
        idx = interned.index(STRING)
    except ValueError:
        s = None
    else:
        s = interned[idx]

    print(f"{proc}: <{s}>")


def main():
    sys.intern(STRING)

    procs = []

    for _ in range(PROCS):
        p = mp.Process(target=worker)
        p.start()
        procs.append(p)

    for p in procs:
        p.join()


if __name__ == "__main__":
    main()

测试:

# python test.py 
<Process name='Process-1' parent=3917 started>: <https://www.youtube.com/watch?v=dQw4w9WgXcQ>
<Process name='Process-2' parent=3917 started>: <https://www.youtube.com/watch?v=dQw4w9WgXcQ>

修补:

--- Objects/unicodeobject.c 2021-05-15 15:08:05.117433926 +0100
+++ Objects/unicodeobject.c.tmp 2021-05-15 23:48:35.236152366 +0100
@@ -16230,6 +16230,11 @@
     _PyUnicode_FiniEncodings(&tstate->interp->unicode.fs_codec);
 }
 
+static PyObject *
+interned_impl(PyObject *module)
+{
+    return PyDict_Values(interned);
+}
 
 /* A _string module, to export formatter_parser and formatter_field_name_split
    to the string.Formatter class implemented in Python. */
@@ -16239,6 +16244,8 @@
      METH_O, PyDoc_STR("split the argument as a field name")},
     {"formatter_parser", (PyCFunction) formatter_parser,
      METH_O, PyDoc_STR("parse the argument as a format string")},
+    {"interned", (PyCFunction) interned_impl,
+     METH_NOARGS, PyDoc_STR("lookup interned strings")},
     {NULL, NULL}
 };

您可能还想看看shared_memory模块。

参考:


推荐阅读