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
实际上是有序的,尽管还不是语言保证),OrderedDict
与 dict
相比具有显着的内存开销;它仍然是用边带链表实现的,以保持顺序并支持简单的迭代,用 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),所有 OrderedDict
s 都会被调回以检查它们是否仍在循环之外被引用(您可以检查 GC 运行s是禁用循环 GC via gc.disable()
) 的主要问题。
鉴于您的特定用例,我强烈建议避免同时使用 dict
和 OrderedDict
。即使是 dict
的开销,即使是 Python 3.6 上的更便宜的形式,当您一遍又一遍地拥有一组恰好三个固定键时,开销也有点极端。相反,use collections.namedtuple
专为可通过名称或索引引用的轻量级对象而设计(它们的行为类似于常规 tuple
s,但也允许将每个值作为命名属性访问),这将显着减少内存程序成本(即使内存不是问题也可能加快程序速度)。
例如:
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()
来获得一个 OrderedDict
与 namedtuple
具有相同的映射,但是通常不需要。内存节省是有意义的;在我的系统上,三元素 namedtuple
将每个项目的开销降低到 72 字节,甚至不到 3.6 dict
的三分之一,不到 3.6 OrderedDict
的六分之一(并且三个元素 namedtuple
在 3.5 上仍然是 72 个字节,其中 dict
/OrderedDict
在 3.6 之前更大)。它可能会节省更多; tuple
s(和 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 的开销是一项重大改进。
这个让我完全莫名其妙。
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
实际上是有序的,尽管还不是语言保证),OrderedDict
与 dict
相比具有显着的内存开销;它仍然是用边带链表实现的,以保持顺序并支持简单的迭代,用 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),所有 OrderedDict
s 都会被调回以检查它们是否仍在循环之外被引用(您可以检查 GC 运行s是禁用循环 GC via gc.disable()
) 的主要问题。
鉴于您的特定用例,我强烈建议避免同时使用 dict
和 OrderedDict
。即使是 dict
的开销,即使是 Python 3.6 上的更便宜的形式,当您一遍又一遍地拥有一组恰好三个固定键时,开销也有点极端。相反,use collections.namedtuple
专为可通过名称或索引引用的轻量级对象而设计(它们的行为类似于常规 tuple
s,但也允许将每个值作为命名属性访问),这将显着减少内存程序成本(即使内存不是问题也可能加快程序速度)。
例如:
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()
来获得一个 OrderedDict
与 namedtuple
具有相同的映射,但是通常不需要。内存节省是有意义的;在我的系统上,三元素 namedtuple
将每个项目的开销降低到 72 字节,甚至不到 3.6 dict
的三分之一,不到 3.6 OrderedDict
的六分之一(并且三个元素 namedtuple
在 3.5 上仍然是 72 个字节,其中 dict
/OrderedDict
在 3.6 之前更大)。它可能会节省更多; tuple
s(和 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 的开销是一项重大改进。