使用冻结集上的元组作为字典的键是否存在性能差异?

时间:2012-05-01 13:37:44

标签: python

我有一个脚本,它使用由两个变量组成的键对字典进行多次调用。我知道我的程序将以相反的顺序再次遇到这两个变量,这使得将密钥存储为元组是可行的。 (为行和列创建具有相同标签的矩阵)

因此,我想知道在字典密钥的冻结集上使用元组是否存在性能差异。

4 个答案:

答案 0 :(得分:11)

在快速测试中,显然它的差异可以忽略不计。

python -m timeit -s "keys = list(zip(range(10000), range(10, 10000)))" -s "values = range(10000)" -s "a=dict(zip(keys, values))" "for i in keys:" "  _ = a[i]"
1000 loops, best of 3: 855 usec per loop

python -m timeit -s "keys = [frozenset(i) for i in zip(range(10000), range(10, 10000))]" -s "values = range(10000)" -s "a=dict(zip(keys, values))" "for i in keys:" "  _ = a[i]"
1000 loops, best of 3: 848 usec per loop

我真的会选择代码中其他地方最好的东西。

答案 1 :(得分:8)

没有做过任何测试,我有一些猜测。对于frozenset s,cpython stores the hash计算完毕后;此外,迭代任何类型的集合会产生额外的开销,因为数据被稀疏地存储。在一个2项集合中,这会对第一个哈希造成显着的性能损失,但可能会使第二个哈希非常快 - 至少在对象本身是相同的时候。 (即不是一个新的但等效的冻结集。)

对于tuple s,cpython不存储哈希值,而是存储calculates it every time。因此,对于frozensets来说,重复散列可能略微更便宜。但对于这么短的元组,可能几乎没有区别;甚至可能非常短的元组会更快。

Lattyware目前的时间安排与我在这里的推理方式相当吻合;见下文。

为了测试我对哈希新旧对抗的不对称性的直觉,我做了以下几点。我相信时间上的差异完全是由于额外的哈希时间。顺便说一句,这是非常微不足道的:

>>> fs = frozenset((1, 2))
>>> old_fs = lambda: [frozenset((1, 2)), fs][1]
>>> new_fs = lambda: [frozenset((1, 2)), fs][0]
>>> id(fs) == id(old_fs())
True
>>> id(fs) == id(new_fs())
False
>>> %timeit hash(old_fs())
1000000 loops, best of 3: 642 ns per loop
>>> %timeit hash(new_fs())
1000000 loops, best of 3: 660 ns per loop

请注意我之前的时间错误;使用and创建了上述方法避免的时序不对称性。这种新方法在这里产生元组的预期结果 - 可忽略的时间差:

>>> tp = (1, 2)
>>> old_tp = lambda: [tuple((1, 2)), tp][1]
>>> new_tp = lambda: [tuple((1, 2)), tp][0]
>>> id(tp) == id(old_tp())
True
>>> id(tp) == id(new_tp())
False
>>> %timeit hash(old_tp())
1000000 loops, best of 3: 533 ns per loop
>>> %timeit hash(new_tp())
1000000 loops, best of 3: 532 ns per loop

并且,恩典,将预构建的冻结集的哈希时间与预先构造的元组的哈希时间进行比较:

>>> %timeit hash(fs)
10000000 loops, best of 3: 82.2 ns per loop
>>> %timeit hash(tp)
10000000 loops, best of 3: 93.6 ns per loop

Lattyware的结果看起来更像这样,因为它们是新老结果的平均结果。 (它们会在创建字典时对每个元组或冻结集进行两次哈希处理,一次访问它。)

所有这一切的结果是它可能并不重要,除了我们这些喜欢在Python的内部挖掘和测试遗忘的人。

答案 2 :(得分:2)

虽然您可以使用timeit来查明(我鼓励您这样做,如果没有其他原因,除了了解它是如何工作的),最后它几乎肯定无关紧要。

frozenset s专门设计为可以清除,所以如果他们的哈希方法是线性时间我会感到震惊。只有在实时应用程序中需要在很短的时间内完成固定(大量)的查找时,这种微优化才有意义。

更新:查看对Lattyware答案的各种更新和评论 - 需要花费大量的精力(相对而言)来消除混杂因素,并展示其性能两种方法几乎相同。性能命中率不是假定的,并且在您自己的代码中也是如此。

编写代码以便工作,然后分析以查找热点,然后应用算法优化,然后应用微优化。

答案 3 :(得分:1)

最佳答案(Gareth Latty 的)似乎已过时。在 python 3.6 上,frozenset 散列似乎要快得多,但这在很大程度上取决于您要散列的内容:

sjelin@work-desktop:~$ ipython
Python 3.6.9 (default, Nov  7 2019, 10:44:02)

In [1]: import time

In [2]: def perf(get_data):
   ...:     tuples = []
   ...:     sets = []
   ...:     for _ in range(10000):
   ...:         t = tuple(get_data(10000))
   ...:         tuples.append(t)
   ...:         sets.append(frozenset(t))
   ...: 
   ...:     start = time.time()
   ...:     for s in sets:
   ...:         hash(s)
   ...:     mid = time.time()
   ...:     for t in tuples:
   ...:         hash(t)
   ...:     end = time.time()
   ...:     return {'sets': mid-start, 'tuples': end-mid}
   ...: 

In [3]: perf(lambda n: range(n))
Out[3]: {'sets': 0.32627034187316895, 'tuples': 0.22960591316223145}

In [4]: from random import random

In [5]: perf(lambda n: (random() for _ in range(n)))
Out[5]: {'sets': 0.3242628574371338, 'tuples': 1.117497205734253}

In [6]: perf(lambda n: (0 for _ in range(n)))
Out[6]: {'sets': 0.0005457401275634766, 'tuples': 0.16936826705932617}

In [7]: perf(lambda n: (str(i) for i in range(n)))
Out[7]: {'sets': 0.33167099952697754, 'tuples': 0.3538074493408203}

In [8]: perf(lambda n: (object() for _ in range(n)))
Out[8]: {'sets': 0.3275420665740967, 'tuples': 0.18484067916870117}

In [9]: class C:
   ...:     def __init__(self):
   ...:         self._hash = int(random()*100)
   ...:         
   ...:     def __hash__(self):
   ...:         return self._hash
   ...:     

In [10]: perf(lambda n: (C() for i in range(n)))
Out[10]: {'sets': 0.32653021812438965, 'tuples': 6.292834997177124}

其中一些差异在性能环境中足够重要,但前提是散列实际上是您的瓶颈(几乎从未发生过)。

我不确定是什么使冻结集几乎总是在 ~0.33 秒内运行,而元组花费 0.2 到 6.3 秒之间的任何时间。需要明确的是,使用相同的 lambda 重新运行从来没有改变超过 1% 的结果,所以它不像存在错误。

在 python2 中结果不同,两者通常更接近彼此,这可能是 Gareth 没有看到相同差异的原因。