簡體   English   中英

在frozenset上使用元組作為字典的鍵是否有性能差異?

[英]Is there a performance difference in using a tuple over a frozenset as a key for a dictionary?

我有一個腳本,它使用由兩個變量組成的鍵對字典進行多次調用。 我知道我的程序將以相反的順序再次遇到這兩個變量,這使得將鍵存儲為元組是可行的。 (為行和列創建一個具有相同標簽的矩陣)

因此,我想知道在字典鍵的凍結集上使用元組是否存在性能差異。

在快速測試中,顯然它產生的差異可以忽略不計。

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

我真的會選擇代碼中其他地方最好的。

在沒有做過任何測試的情況下,我有一些猜測。 對於frozenset s,cpython 在計算后存儲散列 此外,迭代任何類型的集合都會產生額外的開銷,因為數據存儲稀疏。 在 2 項集中,這對第一個散列施加了顯着的性能損失,但可能會使第二個散列非常快——至少當對象本身相同時。 (即不是新的但等效的frozenset。)

對於tuple s,cpython 不存儲散列,而是每次都計算它 因此,使用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

而且,妙招,將預先構造的frozenset的散列時間與預先構造的元組的散列時間進行比較:

>>> %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 內部結構並測試事物而被遺忘的人。

雖然您可以使用timeit來查找(我鼓勵您這樣做,如果沒有其他原因,只是為了了解它是如何工作的),但最終幾乎可以肯定它無關緊要。

frozenset是專門設計為可散列的,所以如果它們的散列方法是線性時間,我會感到震驚。 只有當您需要在實時應用程序中在很短的時間內完成固定(大量)數量的查找時,這種微優化才有意義。

更新:查看對 Lattyware 答案的各種更新和評論 - 需要大量的集體努力(好吧,相對),去除混雜因素,並表明兩種方法的性能幾乎相同。 性能上的影響不是他們假設的地方,在您自己的代碼中也是如此。

編寫您的代碼以工作,然后分析以找到熱點,然后應用算法優化,然后應用微優化。

最佳答案(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沒有看到相同差異的原因。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM