Python深拷贝使用的内存比实际需要的要多。

5

最近在使用 copy.deepcopy 时,遇到了奇怪的内存使用情况。

我有以下代码示例:

import copy
import gc
import os

import psutil
from pympler.asizeof import asizeof
from humanize import filesize


class Foo(object):
    __slots__ = ["name", "foos", "bars"]

    def __init__(self, name):
        self.name = name
        self.foos = {}
        self.bars = {}

    def add_foo(self, foo):
        self.foos[foo.name] = foo

    def add_bar(self, bar):
        self.bars[bar.name] = bar

    def __getstate__(self):
        return {k: getattr(self, k) for k in self.__slots__}

    def __setstate__(self, state):
        for k, v in state.items():
            setattr(self, k, v)


class Bar(object):
    __slots__ = ["name", "description"]

    def __init__(self, name, description):
        self.name = name
        self.description = description

    def __getstate__(self):
        return {k: getattr(self, k) for k in self.__slots__}

    def __setstate__(self, state):
        for k, v in state.items():
            setattr(self, k, v)

def get_ram():
    return psutil.Process(os.getpid()).memory_info()[0]

def get_foo():
    sub_foo = Foo("SubFoo1")
    for i in range(5000):
        sub_foo.add_bar(Bar("BarInSubFoo{}".format(i), "BarInSubFoo{}".format(i)))
    foo = Foo("Foo")
    foo.add_foo(sub_foo)
    for i in range(5000):
        foo.add_bar(Bar("BarInFoo{}".format(i), "BarInFoo{}".format(i)))

    return foo

def main():
    foo = get_foo()
    foo_size = asizeof(foo)

    gc.collect()
    ram1 = get_ram()

    foo_copy = copy.deepcopy(foo)

    gc.collect()
    ram2 = get_ram()
    foo_copy_size = asizeof(foo_copy)
    print("Original object size: {}, Ram before: {}\nCopied object size: {}, Ram after: {}\nDiff in ram: {}".format(
        filesize.naturalsize(foo_size), filesize.naturalsize(ram1), filesize.naturalsize(foo_copy_size),
        filesize.naturalsize(ram2), filesize.naturalsize(ram2-ram1)
    ))

if __name__ == "__main__":
    main()

我的尝试是测试在 copy.deepcopy 之前和之后程序使用的内存量。为此,我创建了两个类。我预期在调用 deepcopy 后,内存使用量会增加与原始对象大小相等的数量。但奇怪的是,我得到了以下结果:

Original object size: 2.1 MB, Ram before: 18.6 MB
Copied object size: 2.1 MB, Ram after: 24.7 MB
Diff in ram: 6.1 MB

从结果可以看出,内存使用量的差异大约为复制对象大小的300%。

**这些结果是在Windows 10 64位上使用Python 3.8.5获得的。

我尝试了什么?

  • 在Python2.7中运行此代码示例,结果更奇怪(复制对象大小的500%以上):
Original object size: 2.3 MB, Ram before: 34.3 MB
Copied object size: 2.3 MB, Ram after: 46.2 MB
Diff in ram: 11.9 MB
  • 在Linux上同时使用Python3.8和Python2.7运行代码得到了相同的结果。
  • __getstate__中返回元组列表而不是字典可以得到更好的结果,但仍与我的预期相差较远。
  • Foo对象中使用列表而不是字典也可以得到更好的结果,但仍与我的预期相差较远。
  • 使用pickle.dumpspickle.loads来复制对象可以得到相同的结果。

有什么想法吗?


1
一些可能已经被deepcopy考虑在内了,因为它会保留一个已访问对象的缓存,以避免陷入无限循环。对于这种情况,您应该编写自己的高效复制函数。deepcopy的编写是为了能够处理任意输入,而不一定是为了效率。 - juanpa.arrivillaga
复制增加了6.1 MB的RAM。创建原始文件增加了多少RAM? - superb rain
1个回答

5

部分原因可能是由于deepcopy会保留已访问对象的缓存以避免陷入无限循环(我相信这是一个set)。对于这种情况,您应该编写自己的高效复制函数。 deepcopy编写时旨在处理任意输入,而不一定是为了效率。

如果您需要一个高效的拷贝函数,可以自己编写。这足以实现深层拷贝,示例如下:

import copy
import gc
import os

import psutil
from pympler.asizeof import asizeof
from humanize import filesize


class Foo(object):
    __slots__ = ["name", "foos", "bars"]

    def __init__(self, name):
        self.name = name
        self.foos = {}
        self.bars = {}

    def add_foo(self, foo):
        self.foos[foo.name] = foo

    def add_bar(self, bar):
        self.bars[bar.name] = bar

    def copy(self):
        new = Foo(self.name)
        new.foos = {k:foo.copy() for k, foo in self.foos.items()}
        new.bars = {k:bar.copy() for k, bar in self.bars.items()}
        return new

class Bar(object):
    __slots__ = ["name", "description"]

    def __init__(self, name, description):
        self.name = name
        self.description = description

    def copy(self):
        return Bar(self.name, self.description)

def get_ram():
    return psutil.Process(os.getpid()).memory_info()[0]

def get_foo():
    sub_foo = Foo("SubFoo1")
    for i in range(5000):
        sub_foo.add_bar(Bar("BarInSubFoo{}".format(i), "BarInSubFoo{}".format(i)))
    foo = Foo("Foo")
    foo.add_foo(sub_foo)
    for i in range(5000):
        foo.add_bar(Bar("BarInFoo{}".format(i), "BarInFoo{}".format(i)))

    return foo

def main():
    foo = get_foo()
    foo_size = asizeof(foo)

    gc.collect()
    ram1 = get_ram()

    foo_copy = foo.copy()

    gc.collect()
    ram2 = get_ram()
    foo_copy_size = asizeof(foo_copy)
    print("Original object size: {}, Ram before: {}\nCopied object size: {}, Ram after: {}\nDiff in ram: {}".format(
        filesize.naturalsize(foo_size), filesize.naturalsize(ram1), filesize.naturalsize(foo_copy_size),
        filesize.naturalsize(ram2), filesize.naturalsize(ram2-ram1)
    ))

if __name__ == "__main__":
    main()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接