[英]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.