为什么Python子进程的本地对象分配会增加主进程的堆大小? TL;博士

为什么Python子进程的本地对象分配会增加主进程的堆大小? TL;博士,python,memory-management,valgrind,memcheck,python-multiprocessing,Python,Memory Management,Valgrind,Memcheck,Python Multiprocessing,根据Valgrind的memcheck工具,如果我在一个函数中分配一个大的局部变量,并使用multiprocessing.Pool().apply_async()启动该函数,那么子进程和主进程的堆大小都会增加。为什么main的堆大小会增加 背景 我使用的是一个多处理工作池,每个工作池将处理输入文件中的大量数据。我想看看我的内存占用是如何根据输入文件的大小进行扩展的。为此,我使用memcheck在Valgrind下运行脚本,使用中描述的技术。(从那以后,我了解到Valgrind的Massif工具更

根据Valgrind的memcheck工具,如果我在一个函数中分配一个大的局部变量,并使用
multiprocessing.Pool().apply_async()
启动该函数,那么子进程和主进程的堆大小都会增加。为什么main的堆大小会增加

背景 我使用的是一个多处理工作池,每个工作池将处理输入文件中的大量数据。我想看看我的内存占用是如何根据输入文件的大小进行扩展的。为此,我使用memcheck在Valgrind下运行脚本,使用中描述的技术。(从那以后,我了解到Valgrind的Massif工具更适合于此,因此我将继续使用它。)

memcheck输出中有一些看起来很奇怪的东西,我想帮助理解

我在Red Hat Linux上使用CPython 2.7.6,并运行memcheck,如下所示:

valgrind--tool=memcheck--suppressions=./valgrind-python.supp python test.py

代码和输出 堆摘要(每个进程一个):

总堆使用率:45193个alloc,32392个frees,7221910个字节分配
总堆使用率:44832个alloc,22006个free,分配了7181635个字节

如果我将
tmp='a'*1
行更改为
tmp='a'*10000000
我将得到以下摘要:

总堆使用率:44835个allocs,22009个frees,27181763个字节分配
总堆使用率:45195个allocs,32394个free,分配17221998个字节

问题
为什么这两个进程的堆大小都会增加?我知道对象的空间是有限的,所以对于其中一个进程来说,较大的堆当然是有意义的。但是我希望子流程有自己的堆、堆栈和解释器实例,所以我不明白为什么子流程中分配的局部变量也会增加main的堆大小。如果它们共享同一堆,那么CPython是否实现了自己版本的fork(),该版本没有为子进程分配唯一的堆空间?

问题与如何实现
fork
无关。您可以自己看到调用,这是
fork
的一个非常薄的包装

那么,到底发生了什么

编译器正在源代码中看到
'a'*10000000
,并将其优化为10000000个字符的文本。这意味着模块对象现在要长10000000字节,而且由于它在两个进程中都被导入,所以它们都会变得更大

要了解这一点:

$ python2.7
>>> def f():
...     temp = 'a' * 10
...
>>> f.__code__.co_consts
(None, 'a', 10, 'aaaaaaaaaa')
>>> import dis
>>> dis.dis(f)
  2           0 LOAD_CONST               3 ('aaaaaaaaaa')
              3 STORE_FAST               0 (temp)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE
请注意,编译器足够聪明,可以将
'aaaaaaaaaa'
添加到常量中,但不够聪明,无法同时删除
'a'
10
。这是因为它使用了一个非常窄的窥视孔优化器。除了不知道您是否在同一个函数的其他地方使用了
'a'
之外,它不想从
co_consts
列表的中间删除一个值,然后返回并每隔一个字节码更新一次,以使用上移的索引



我真的不知道为什么孩子最终会以20000000字节而不是10000000字节的速度增长。可能它最终会得到自己的模块副本,或者至少是代码对象,而不是使用从父级共享的副本。但是如果我尝试打印id(f.uuu code_uuuu)或其他任何东西,我在父级和子级中得到相同的值,因此…

您可以自己查看源代码。调用,它是
fork
的一个非常薄的包装。为了未来读者的利益,这里有进一步的证据证明这个答案是正确的:如果我用文件中的读取替换temp常量,工作进程的堆大小将随文件的大小而缩放,而main的堆大小保持不变。因此,堆不是共享的,并且由于常数非常大,两个进程中的模块和堆大小都在爆炸。
$ python2.7
>>> def f():
...     temp = 'a' * 10
...
>>> f.__code__.co_consts
(None, 'a', 10, 'aaaaaaaaaa')
>>> import dis
>>> dis.dis(f)
  2           0 LOAD_CONST               3 ('aaaaaaaaaa')
              3 STORE_FAST               0 (temp)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE