Python 与 dict() 相比,OrderDict 有问题

Python OrderDict sputtering as compared to dict()

这个让我完全莫名其妙。

asset_hist = []
for key_host, val_hist_list in am_output.asset_history.items():
    for index, hist_item in enumerate(val_hist_list):
        #row = collections.OrderedDict([("computer_name", key_host), ("id", index), ("hist_item", hist_item)])
        row = {"computer_name": key_host, "id": index, "hist_item": hist_item}
        asset_hist.append(row)

此代码与注释掉的集合行完美配合。但是,当我注释掉 row = dict 行并从 collections 行中删除注释时,事情变得非常奇怪。大约有 400 万行生成并附加到 asset_hist。

所以,当我使用 row=dict 时,整个循环在大约 10 毫秒内完成,快如闪电。我用有序字典的时候等了10多分钟还是没有完成。现在,我知道 OrderDict 应该比 dict 慢一点,但它在最坏的情况下应该慢 10 倍左右,根据我的数学计算,它实际上在这个函数中慢了大约 100,000 倍。

我决定在最低循环中打印索引以查看发生了什么。有趣的是,我注意到控制台输出出现断断续续的情况。索引在屏幕上打印得非常快,然后在继续之前停止大约 3-5 秒。

am_output.asset_history 是一个字典,有一个键,主机,每一行都是一个字符串列表。例如

am_output.asset_history = {"host1": ["string1", "string2", ...], "host2": ["string1", "string2", ...], ...}

编辑:使用 OrderedDict 进行溅射分析

此 VM 服务器上的总内存:只有 8GB...需要进行更多配置。

循环次数

184796(~5 秒等待,~60% 内存使用)

634481(~5 秒等待,~65% 内存使用)

1197564(~5 秒等待,~70% 内存使用)

1899247(~5 秒等待,~75% 内存使用)

2777296(~5 秒等待,~80% 内存使用)

3873730 (LONG WAIT...等了20分钟就放弃了!,88.3%的内存使用率,进程还在运行ning)

等待发生的位置随着每个运行而改变。

编辑: 运行 再次出现,这次它停在 3873333,接近它之前停止的位置。它在形成行后停止,同时尝试追加......我没有注意到最后一次尝试但它也在那里......问题在于追加线,而不是行线......我仍然莫名其妙。这是它在长时间停止之前生成的行(将行添加到打印语句)...更改主机名以保护无辜者:

3873333: OrderedDict([('computer_name', 'bg-fd5612ea'), ('id', 1), ('hist_item', "sys1 Normalizer (sys1-4): Domain Name cannot be determined from sys1 Name 'bg-fd5612ea'.")])

正如您自己的测试所证明的那样,您 运行 内存不足。即使在 CPython 3.6 上(其中纯 dict 实际上是有序的,尽管还不是语言保证),OrderedDictdict 相比具有显着的内存开销;它仍然是用边带链表实现的,以保持顺序并支持简单的迭代,用 move_to_end 重新排序,等等。你可以通过检查 sys.getsizeof 来判断(确切的结果会因 Python 版本和构建位宽,32 位与 64 位):

>>> od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
>>> d = {**od}
>>> sys.getsizeof(od)
464   # On 3.5 x64 it's 512
>>> sys.getsizeof(d)
240   # On 3.5 x64 it's 288

忽略存储的数据,此处 OrderedDict 的开销几乎是普通 dict 的两倍。如果你正在制作 400 万个这样的项目,在我的机器上,将增加超过 850 MB 的开销(在 3.5 和 3.6 上)。

可能是您系统上所有其他程序的组合,加上您的 Python 程序,超出了分配给您机器的 RAM,您陷入了交换抖动。特别是,每当 asset_hist 必须扩展新条目时,它可能需要分页其中的大部分(由于缺乏使用而被分页),并且每当循环垃圾收集 运行 触发时(默认情况下,大约每 70,000 次分配和释放就会发生一次完整的 GC),所有 OrderedDicts 都会被调回以检查它们是否仍在循环之外被引用(您可以检查 GC 运行s是禁用循环 GC via gc.disable()) 的主要问题。

鉴于您的特定用例,我强烈建议避免同时使用 dictOrderedDict。即使是 dict 的开销,即使是 Python 3.6 上的更便宜的形式,当您一遍又一遍地拥有一组恰好三个固定键时,开销也有点极端。相反,use collections.namedtuple 专为可通过名称或索引引用的轻量级对象而设计(它们的行为类似于常规 tuples,但也允许将每个值作为命名属性访问),这将显着减少内存程序成本(即使内存不是问题也可能加快程序速度)。

例如:

from collections import namedtuple

ComputerInfo = namedtuple('ComputerInfo', ['computer_name', 'id', 'hist_item'])

asset_hist = []
for key_host, val_hist_list in am_output.asset_history.items():
    for index, hist_item in enumerate(val_hist_list):
        asset_hist.append(ComputerInfo(key_host, index, hist_item))

使用上的唯一区别是您将 row['computer_name'] 替换为 row.computer_name,或者如果您需要所有值,您可以像常规 tuple 一样解压它,例如comphost, idx, hist = row。如果你暂时需要一个真正的 OrderedDict(不要为所有东西存储它们),你可以调用 row._asdict() 来获得一个 OrderedDictnamedtuple 具有相同的映射,但是通常不需要。内存节省是有意义的;在我的系统上,三元素 namedtuple 将每个项目的开销降低到 72 字节,甚至不到 3.6 dict 的三分之一,不到 3.6 OrderedDict 的六分之一(并且三个元素 namedtuple 在 3.5 上仍然是 72 个字节,其中 dict/OrderedDict 在 3.6 之前更大)。它可能会节省更多; tuples(和 namedtuple 通过扩展)被分配为单个连续的 C struct,而 dict 和 company 至少有两个分配(一个用于对象结构,一个用于或更多用于结构的动态调整大小的部分),每个都可能支付分配器开销和对齐成本。

无论哪种方式,对于您的 400 万行场景,使用 namedtuple 将意味着支付(超出值的成本)总开销约为 275 MB,而 915 (3.6) - 1100 (3.5) dict 的 MB 和 OrderedDict 的 1770 (3.6) - 1950 (3.5) MB。当您谈论 8 GB 系统时,减少 1.5 GB 的开销是一项重大改进。