1

TL;博士

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

背景

我正在与一个多处理工作人员池一起工作,每个工作人员都将处理来自输入文件的大量数据。我想看看我的内存占用如何根据输入文件的大小进行缩放。为此,我在 Valgrind 下使用 memcheck 和此 SO answer中描述的技术运行我的脚本。(从那以后我了解到 Valgrind 的 Massif 工具更适合这个,所以我将使用它来代替。)

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

我在 Red Hat Linux 上使用 CPython 2.7.6,并像这样运行 memcheck:

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

代码和输出

import multiprocessing

def mem_user():
    tmp = 'a'*1
    return

pool = multiprocessing.Pool(processes=1)
pool.apply_async(mem_user)

pool.close()
pool.join()

堆摘要(每个进程一个):

总堆使用量:45,193 分配,32,392 释放,7,221,910 字节分配
总堆使用:44,832 分配,22,006 释放,7,181,635 字节分配

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

总堆使用量:44,835 次分配,22,009 次释放,分配 27,181,763 字节
总堆使用量:45,195 次分配,32,394 次释放,分配 17,221,998 字节

问题

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

4

1 回答 1

2

这个问题与如何实现无关fork。您可以自己查看multiprocessing调用os.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'and 10。那是因为它使用了一个非常窄的窥视孔优化器。除了它不知道您是否还在'a'同一函数中的其他地方使用这一事实之外,它不想从co_consts列表中间删除一个值并返回并更新所有其他字节码以使用移位的-上指数。


我实际上不知道为什么孩子最终增长了 20000000 字节而不是 10000000。大概它以自己的模块副本结束,或者至少是代码对象,而不是使用从父节点共享的副本。但是如果我尝试这样做print id(f.__code__)或其他任何事情,我会在父母和孩子中得到相同的值,所以......</p>

于 2014-09-30T23:53:03.960 回答