【问题标题】:Python dict.setdefault uses more memory? [closed]Python dict.setdefault 使用更多内存? [关闭]
【发布时间】:2012-08-12 12:28:05
【问题描述】:

我正在编写一些涉及类似这样的 Python 代码

values = {}
for element in iterable:
    values.setdefault(element.name, []).append(element)

因为我之前可以对输入进行排序,所以我也是这样实现的

values = {}

cur_name = None
cur_list = None

for element in iterable:
    if element.name != cur_name:
        values[cur_name] = cur_list
        cur_name = element.name
        cur_list = []
    cur_list.append(element)
if cur_list:
    values[cur_name] = cur_list
del values[None]

这里的输入已经按element.name排序。

第二种方法比第一种方法快得多,而且使用的内存也更少。

这是什么原因?

还是我在第二种方法中犯了某种错误?

【问题讨论】:

  • setdefault 是标准字典的完美方法。它不能使用“更多内存”。它只是在其字典上执行。这是你的问题吗?
  • @lazyr 是的,它们是按element.name排序的。
  • 嗨,我还测试了(修改过的)变体,没有来自 Ashwini Chaudhary 答案的 setdefault,它也比你的 setdefault 更快! gist.github.com/3368186

标签: python memory dictionary setdefault


【解决方案1】:

您的原始代码每次循环都会创建一个列表,然后大部分时间就会被丢弃。它还进行多个字典查找(查找方法setdefault 是字典查找,然后方法本身进行字典查找以查看对象是否已设置,如果未设置,则执行另一个以存储值)。 .name.append() 也是字典查找,但它们仍然存在于修改后的代码中。

for element in iterable:
    values.setdefault(element.name, []).append(element)

修改后的代码仅在名称更改时查找字典,因此它从每个循环中删除了两次字典查找和一个方法调用。这就是它更快的原因。

关于内存使用,当列表增长时,有时可能需要复制数据,但如果可以扩展内存块,则可以避免这种情况。我的猜测是,创建所有这些未使用的临时列表可能会使内存更加碎片化并强制执行更多副本。换句话说,Python 实际上并没有使用更多内存,但它可能有更多已分配但未使用的内存。

当您觉得需要 setdefault 时,请考虑改用 collections.defaultdict。除非需要,否则可以避免创建列表:

from collections import defaultdict
values = defaultdict(list)
for element in iterable:
    values[element.name].append(element)

这可能仍会比您的第二个代码慢,因为它没有利用您对名称全部分组的知识,但对于一般情况,它比 setdefault 更好。

另一种方法是使用itertools.groupby。像这样的:

from itertools import groupby
from operator import attrgetter
values = { name: list(elements) for name,elements in
    groupby(elements, attrgetter('name')) }

这利用了排序并将所有内容简化为单个字典理解。

【讨论】:

  • 我试过defaultdict,但与臃肿的方法相比,它使用了太多的内存。
【解决方案2】:

我可以想到第二种方法更快的几个原因。

values.setdefault(element.name, []).append(element)

在这里,您将为每个 element 创建一个空列表,即使您永远不会使用它。您还为每个元素调用setdefault 方法,这相当于一次哈希表查找和一次可能的哈希表写入,加上调用该方法本身的成本,这在python 中并非微不足道。最后,正如其他人在我发布此答案后指出的那样,您正在为每个 element 查找一次 setdefault 属性,即使它总是引用相同的方法。

在第二个示例中,您避免了所有这些低效率。您只是根据需要创建尽可能多的列表,并且除了所需的list.append 之外,您无需调用任何方法即可完成所有操作,其中穿插着少量的字典分配。您实际上还用简单的比较 (element.name != cur_name) 替换了哈希表查找,这是另一个改进。

我希望你也能获得缓存的好处,因为你在向列表中添加项目时不会到处乱跳(这会导致很多cache misses),而是一次处理一个列表时间>。这样,相关内存可能位于非常靠近 CPU 的缓存层中,因此处理速度更快。不应低估这种影响——从 RAM 中获取数据比从 L1 缓存中读取数据要慢两个数量级(或约 100 倍) (source)。

当然排序会增加一点时间,但 python 拥有世界上最好和最优化的排序算法之一,全部用 C 编码,所以它不会超过上面列出的好处。

我不知道为什么第二种解决方案更节省内存。正如 Jiri 指出的那样,它可能是不必要的列表,但我的理解是这些应该由垃圾收集器立即收集,所以它应该只增加很小的内存使用量 - 大小一个空列表。也许是因为垃圾收集器比我想象的更懒。

【讨论】:

  • 我在一个文本文件上运行程序,并在 Ubuntu 的系统监视器中观察了内存使用情况。第一个占用了大约 3GB 的内存,我真的可以看到内存使用量的增加。但是,我看不到第二个的内存使用有太大变化。
  • 可能是创建空列表的原因导致内存效率降低?我想是这样。至少对于大型列表而言。
【解决方案3】:

您的第一个版本有两个效率低下的部分:

  1. 在循环中调用和取消引用 values.setdefault。您可以在循环之前调用values_setdefault = values.setdefault,它可以加快速度。

  2. 正如其他答案所建议的那样,为列表中的每个元素创建新的空列表非常缓慢且内存效率低下。我不知道如何避免它并立即使用 setdefault。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    • 2013-09-25
    • 2013-04-13
    • 1970-01-01
    • 2013-02-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多