python 多处理特有的内存管理

python multiprocessing peculiar memory management

我有一个简单的多处理代码:

from multiprocessing import Pool
import time 

def worker(data):
    time.sleep(20)

if __name__ == "__main__":
    numprocs = 10
    pool = Pool(numprocs)
    a = ['a' for i in xrange(1000000)]
    b = [a+[] for i in xrange(100)]

    data1 = [b+[] for i in range(numprocs)]
    data2 = [data1+[]] + ['1' for i in range(numprocs-1)]
    data3 = [['1'] for i in range(numprocs)]

    #data = data1
    #data = data2
    data = data3


    result = pool.map(worker,data)

b 只是一个大列表。 data 是传递给 pool.map 的长度 numprocs 的列表,所以我希望 numprocs 进程被分叉并且 data 的每个元素被传递给其中一个那些。 我测试了 3 个不同的 data 对象:data1data2 具有几乎相同的大小,但是当使用 data1 时,每个进程都会获得同一对象的副本,而当使用data2,一个进程获得全部 data1,而其他进程仅获得一个“1”(基本上什么都没有)。 data3基本是空的,用来衡量fork进程的基本开销成本。

问题: data1data2 之间使用的整体内存有很大不同。我测量最后一行 (pool.map()) 使用的额外内存量,我得到:

  1. data1:~8GB
  2. data2:~0.8GB
  3. data3: ~0GB

1) 和 2) 不应该相等,因为传递给子级的数据总量是相同的。这是怎么回事?

我在 Linux 机器上从 /proc/meminfo 的 Active 字段测量内存使用情况(总计 - MemFree 给出相同的答案)

您看到的是 pool.map 的一个副作用,它必须 pickle data 中的每个元素才能将其传递给您的子进程。

泡菜的时候发现data1data2大很多。这是一个小脚本,可以将每个列表中每个元素的总 pickle 大小相加:

import pickle
import sys
from collections import OrderedDict

numprocs = 10
a = ['a' for i in range(1000000)]
b = [a+[] for i in range(100)]

data1 = [b+[] for i in range(numprocs)]
data2 = [data1+[]] + ['1' for i in range(numprocs-1)]
data3 = [['1'] for i in range(numprocs)]
sizes = OrderedDict()
for idx, data in enumerate((data1, data2, data3)):
    sizes['data{}'.format(idx+1)] = sum(sys.getsizeof(pickle.dumps(d))
                                            for d in data)

for k, v in sizes.items():
    print("{}: {}".format(k, v))

输出:

data1: 2002003470
data2: 200202593
data3: 480

可以看到,data1的pickled总大小是data2的十倍左右,正好符合两者内存占用的数量级差异。

之所以 data2 泡菜这么小,是因为 Cilyan 在评论中提到的;当您创建每个 data 列表时,您实际上是在制作浅表副本:

>>> data2[0][2][0] is data2[0][0][0]
True
>>> data2[0][2][0] is data2[0][3][0]
True
>>> data2[0][2][0] is data2[0][4][0]
True

现在,data1 也在制作浅拷贝:

>>> data1[0][0] is data1[1][0]
True
>>> data1[0][0] is data1[2][0]
True

关键区别在于 data2,所有浅拷贝都在可迭代对象 (data2[0]) 的一个顶级元素内。因此,当 pool.map 对该元素进行 pickle 时,它​​可以将所有浅表副本删除到一个单独的子列表中,然后只 pickle 那个子列表,以及描述该子列表如何嵌套到 data2。对于 data1,浅拷贝跨越 data1 的不同顶层元素,因此 pool.map 单独腌制它们,这意味着重复数据删除丢失了。所以,当你 unpickle data1 时,浅表副本消失了,每个元素都有一个唯一但相等的子列表副本。

比较这两个示例,其中 data1data2 与您的列表相同:

>>> before_pickle1 = data1[0]
>>> before_pickle2 = data1[1]
>>> before_pickle1 is before_pickle2
False
>>> before_pickle1[0] is before_pickle2[0]
True  # The lists are the same before pickling
>>> after_pickle1 = pickle.loads(pickle.dumps(before_pickle1))
>>> after_pickle2 = pickle.loads(pickle.dumps(before_pickle2))
>>> after_pickle1[0] is after_pickle2[0]
False  # After pickling, the lists are not the same
>>> before_pickle1 = data2[0]

>>> before_pickle1[0][0] is before_pickle1[1][0]
True
>>> after_pickle1 = pickle.loads(pickle.dumps(before_pickle1))
>>> after_pickle1[0][0] is after_pickle1[1][0]
True # The lists are still the same after pickling

这模拟了相同的酸洗 pool.map。使用 data1,由于浅拷贝而进行的所有重复数据删除都丢失了,这使得内存使用率更高。