【问题标题】:dictionary search & insert optimization字典搜索和插入优化
【发布时间】:2018-06-11 06:57:22
【问题描述】:

当字典增加到几千个键时,我在使用字典时遇到了严重的速度下降。

我正在处理一个包含约 1,000,000 行数据的文件,我正在使用字典构建一个类似图形的数据结构

这是我的瓶颈函数

def create_edge(node_a, node_b, graph):
    if node_a not in graph.keys():
        graph[node_a] = {node_b: 1}
    elif node_b in graph[node_a].keys():
        graph[node_a][node_b] += 1
    else:
        graph[node_a][node_b] = 1

create_edge 将创建从 node_anode_b 的边,或者将它们之间的现有边的权重加 1。

由于我的节点由字符串唯一 id 标识,因此我使用字典进行存储,假设搜索是否存在键,并且插入平均需要 O(1)。

如果我注释掉 create_edge,我每秒可以处理大约 20,000 条记录,create_edge 作为我管道的一部分,每秒大约可以处理 20 条记录。

前 100 条记录大约需要 500 毫秒来处理。 当字典大小增加到 10,000 左右时 - 处理 100 条记录大约需要 15,000 毫秒,每个记录进程平均调用 create_edge 大约 4 次 - 因此,当字典大小为 10,000 时,对 create_edge 的 400 次调用需要 15 秒。

首先,这些运行时有意义吗?对我来说似乎很重要,如果我错了,请纠正我。

其次,我们将不胜感激优化字典使用以获得更好的运行时间的建议。

我希望字典大小至少为 100,000 以完成对全部 1,000,000 条记录的处理。


编辑:结论

你在钱上是对的,在这里犯了两个菜鸟错误.. :)

keys() 调用显着增加了复杂性,将其从恒定时间变为每次边插入的多时间(平方),将 if node in graph.keys() 替换为 if node in graph 会在约 300 毫秒内产生 100 条记录的恒定处理时间。

第二个错误是 virtualenv 配置,这让我相信我在使用 python3 而我实际上是在使用 python2。

python3 确实将 keys() 代码优化为恒定时间搜索,这对运行时间有好处,但对于正确的代码风格则不太好。

非常感谢您的帮助。


我在删除 keys() 调用后执行了运行时间比较。

# graph = {}
python version: 3.6.3
start time 11:44:56
Number of records: 1029493
graph created, 1231630 nodes
end time 11:50:35
total ~05:39

# graph = defaultdict(lambda : defaultdict(int))
python version: 3.6.3
start time 11:54:52
Number of records: 1029493
graph created, 1231630 nodes
end time  12:00:34
total ~05:42

# graph = {}
python version: 2.7.10
start time 12:03:25
Number of records: 1029493
graph created, 1231630 nodes
end time 12:09:40
total ~06:15

【问题讨论】:

  • 这是什么 Python 版本?
  • 你是否在使用 Python 2?
  • 因为.keys() 得到一个键的List,而不是字典。因此,在create_edge 内部,您正在执行两次O(n) 操作以从转换为列表的整个字典中搜索两次。你会想做if node_a in graph,见this question注意 如下面的 cmets 所述,这仅适用于使用 Python 2 而非 3 时。
  • @SpencerWieczorek 这就是我的怀疑,因为它是一个著名的 Python 反模式,但在 Python 3 中,它返回具有 O(1) 成员资格测试的键的 view .
  • @AlanSTACK 不,它确实返回一个迭代器,尽管dict_keys 对象是可迭代的。不相信我?试试next({}.keys())

标签: python


【解决方案1】:

在测试dict 中是否存在密钥时,只需使用key in d,而不是key in d.keys()。提取密钥来测试成员资格首先否定了使用dict 的好处。

尝试以下方法:

def create_edge(node_a, node_b, graph):
    if node_a not in graph:
        graph[node_a] = {node_b: 1}
    elif node_b in graph[node_a]:
        graph[node_a][node_b] += 1
    else:
        graph[node_a][node_b] = 1

请注意,keys() 根本没有被调用。这应该比你现在做的快很多。

请注意,在 Python 2 中,keys() 检查将比在 Python 3 中慢得多,因为在 Python 2 中,keys() 创建了整个键集的列表。它在 Python 3 中的工作方式有所不同,但即使在 Python 3 中直接检查成员资格,而不使用 keys(),也会更快。

【讨论】:

  • 关于 keys() 部分的很好解释,但没有 defaultdict/Counter 仍然会很慢。
  • @Jean-FrançoisFabre A defaultdict 会更快,但代码中最大的问题是它使用了keys(),特别是如果 OP 使用的是 Python 2。使用 defaultdict 可能通过避免冗余的密钥检查,最多可以将速度提高三倍,但它确实具有消除错误调用keys() 的可能性。
  • @Jean-FrançoisFabre 请出示支持该声明的基准。
  • @Jean-FrançoisFabre Mine 不同意,defaultdict+Counter 花费的时间几乎是 OP(固定)方式的两倍:ideone.com/DvufOu
  • @StefanPochmann 有趣。我猜defaultdict 的效率比我想象的要低。
【解决方案2】:

你不能只使用带有 defaultdict(int) 的 defaultdict,如下所示:Python: defaultdict of defaultdict?

from collections import defaultdict

graph = defaultdict(lambda : defaultdict(int))

graph['a']['b'] += 1
graph['a']['b'] += 1
graph['a']['c'] += 1

graph

返回:

defaultdict(<function __main__.<lambda>>,
            {'a': defaultdict(int, {'b': 2, 'c': 1})})
# equal to: {'a': {'b': 2, 'c': 1}}

【讨论】:

  • 这实际上是否比 OP 快(当然,在修复了他们的 .keys() 错误之后)。你能分享你的基准测试结果吗?
  • 由于我不是坐在数据集上,我认为 OP 可以做到。
  • @StefanPochmann 它可能会快得多。上下文切换(调用create_edge)非常昂贵。
  • @PatrickHaugh 我看不出有什么大的不同,看看底部的时间:ideone.com/SajEsd。这实际上是使用该功能添加边缘。如果我在内联进行,则在我的测试中,OP 的方式会明显更快ideone.com/DvufOu
  • 一定会尝试使用 lambdas 和 defaultdict 并发布结果,谢谢!
【解决方案3】:

我尝试了几种方法,这是一种似乎有效的方法。此方法使用计数器首先计算所有出现次数,然后构建字典。感谢@Stefan Pochmann 提供基准脚本。我使用的一个来自ideone.com/ckF0X5

我使用的是 Python 3.6,结果在我的电脑上进行了测试。

from timeit import timeit
from collections import defaultdict, Counter
from random import shuffle
from itertools import product

def f():   # OP's method modified with Tom Karzes' answer above.
    d = {}
    for i, j in edges:
        if i not in d:
            d[i] = {j: 1}
        elif j in d[i]:
            d[i][j] += 1
        else:
            d[i][j] = 1

def count_first(): 
    d = dict()
    for (v, w), c in Counter(edges).items():
        if v not in d:
            d[v] = {w: c}
        else:
            d[v][w] = c
    # Alternatively, (Thanks to Jean-François Fabre to point it out.)
    # d = defaultdict(lambda : defaultdict(int)) 
    # for (v, w), c in Counter(edges).items(): 
    #     d[v][w] = c

edges = list(product(range(300), repeat=2)) * 10
shuffle(edges)

# %timeit f()
270 ms ± 23.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# %timeit count_first()
180 ms ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

声明者:我从 ideone.com 获得的 count_first() 的结果比这里的 OP 的答案 f() 慢。

Stefan Pochmann 做了一个基准实验来比较 Python 2 和 3 中的不同方法。他在 Python 2 中的结果可以在here 找到。对于 Python 3,请检查 this。感谢他并感谢他的代码审查。

【讨论】:

  • d = defaultdict(lambda : defaultdict(int)) for v, c in Counter(edges).items(): d[v[0]][v[1]] = c 的速度稍快
  • 有趣的想法,这个预先计算。不过,为什么不c = Counter(edges)?是的,对于我的基准测试,我使用了 Python 2,因为这就是 OP 所使用的(Ideone 也支持 Python 3)。我只是用更多的方法进行了一维测试。在 Python 2 中,defaultdict 获胜,随后是 dict,而Counter 则慢了很多:ideone.com/PFK7yX。在 Python 3 中,情况类似,只是将数据提供给 Counter 构造函数从非常慢变为最快:ideone.com/nCNaxp。我想知道为什么...
  • 啊,对。 Counter 的那部分变得更快,因为它是用 C 实现的。
  • @Jean-FrançoisFabre 他们的速度似乎大致相同。 (共享相同的均值和标准差。来自我的机器)但是代码更干净!
  • @StefanPochmann 感谢您的提醒和基准实验。更新了代码。
猜你喜欢
  • 2014-02-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-25
  • 1970-01-01
  • 1970-01-01
  • 2011-08-28
  • 1970-01-01
  • 2020-02-05
相关资源
最近更新 更多