【问题标题】:Why is a deep copy so much slower than a shallow copy for lists of the same size?对于相同大小的列表,为什么深拷贝比浅拷贝慢得多?
【发布时间】:2019-01-31 19:17:10
【问题描述】:

我一直在开发一个性能关键的应用程序,该应用程序需要经常复制二维整数列表并修改副本(我正在实现极小极大算法)。

我注意到在具有相同数量元素的列表上,副本和深度副本之间的性能存在巨大差异,我想了解我的想法是否正确。

要重现我的问题,请运行以下代码:

import numpy as np

np.random.seed(0)
lst1 = np.random.randint(100, size=1000 * 1000).tolist()
lst2 = np.random.randint(100, size=(1000, 1000)).tolist()

现在,对下面的语句进行计时,您应该会看到与我类似的计时。

%timeit copy.copy(lst1)
%timeit lst1.copy()
%timeit copy.deepcopy(lst2)

5 ms ± 49.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
5.47 ms ± 551 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.61 s ± 112 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

lst1lst2 都有一百万个元素,但可靠地复制前者比具有相同数量元素的嵌套列表快 200 倍。我认为这与制作嵌套列表的深层副本可能需要一些缓慢的递归实现有关,所以我尝试了

%timeit copy.deepcopy(lst1)
1.43 s ± 90.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 

而且时间仍然显示出大幅放缓。我检查了docs,但没有提供太多解释。然而,从时间上看,我怀疑deepcopy 也在复制每个 int,创建新的整数。但这似乎是一件很浪费的事情。

我的想法是对的吗? list.copy 和浅拷贝不在这里做什么 deepcopy?

我见过deepcopy() is extremely slow,但似乎这个问题是在寻求替代方案而不是解释(我不清楚)。

【问题讨论】:

  • 这可能确实很浪费,但这就是deepcopy 所做的:它复制一切。它不知道你只想复制列表。
  • 它不会复制所有内容(它不会复制不可变的内置类型),但它会检查所有内容并维护所有可见对象的缓存
  • 谢谢你们的cmets,伙计们。请考虑充实它们作为答案。在我的代码中访问列表元素时,似乎我唯一的选择是切换到 1D 实现并实现一些逻辑以将 2D 索引转换为 1D 索引。
  • 如果您确切地知道您的列表的形状,您可能可以实现自己的简单复制操作,即:[sub.copy() for sub in nested_list]。这会更快

标签: python python-3.x copy deep-copy


【解决方案1】:

deepcopy 没有复制整数。反正也没办法。

deepcopy 很慢,因为它需要处理深度复制的全部复杂性,即使这被证明是不必要的。这包括将它找到的每个对象分派到适当的复印机,即使复印机结果是basically just be lambda x: x。这包括维护一个备忘录字典并跟踪复制的每个对象,以处理对相同对象的重复引用,即使没有。这包括对 listsdicts 等数据结构的特殊复制处理,因此在尝试复制具有递归引用的数据结构时不会进入无限递归。

无论是否有回报,所有这些都必须完成。都是很贵的。

另外,deepcopy 是纯 Python。那没有帮助。将 deepcopy 与执行非常相似的工作的 pickle.loads(pickle.dumps(whatever)) 进行比较,pickle 由于 C 实现而轻松获胜。 (在 Python 2 上,将 pickle 替换为 cPickle。)pickle 仍然很难利用输入的已知结构实现,但:

In [15]: x = [[0]*1000 for i in range(1000)]

In [16]: %timeit copy.deepcopy(x)
1.05 s ± 5.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [17]: %timeit pickle.loads(pickle.dumps(x))
78 ms ± 4.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [18]: %timeit [l[:] for l in x]
4.56 ms ± 108 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

【讨论】:

    【解决方案2】:

    在编程中,深拷贝相当于某物的物理拷贝。它是原始对象的实际副本。在大多数编程工具中,您可以随意使用它,修改它而不影响原始对象。 然而,另一方面,浅拷贝是对原始对象的引用。如果更改它,它也会影响原始对象。 简而言之,由于深拷贝是原始对象的实际副本,因此比仅指向原始对象的浅拷贝更重。

    浅拷贝:您可以拥有一张新家具的照片,并了解它的真实外观。您可以轻松携带图片。

    深拷贝:你可以去家具店,看看真正的家具。您可能不方便随身携带,可能需要一些帮助才能将其带回家。

    【讨论】:

    • “但是,另一方面,浅拷贝是对原始对象的引用。如果更改它,它也会影响原始对象。” 这不是真的。 a = []; import copy; b = copy.copy(a); a.append('foo'); print('b:',b,'a:',a).
    【解决方案3】:

    https://docs.python.org/2/library/copy.html

    浅拷贝和深拷贝之间的区别仅与复合对象(包含其他对象的对象,如列表或类实例)有关:

    1. 浅拷贝构造一个新的复合对象,然后(在可能的范围内)向其中插入对原始对象的引用。
    2. 深拷贝构造一个新的复合对象,然后递归地将原始对象中找到的对象的副本插入其中

    因此,浅拷贝将有效地创建一个新列表并使用对原始列表中每个元素的引用来填充它。因为原始列表中的每个元素本身都是一个列表,所以只存储对它的引用比创建新副本要快得多。 Deepcopy 在如何复制每个元素方面做了一些聪明的事情,以避免错误。但本质上,您无需了解这一点即可知道为什么浅拷贝比深拷贝快....

    【讨论】:

      猜你喜欢
      • 2012-04-12
      • 2015-01-13
      • 2011-09-05
      • 1970-01-01
      • 1970-01-01
      • 2010-09-16
      • 1970-01-01
      • 2014-05-25
      • 1970-01-01
      相关资源
      最近更新 更多