所有这些结果都是通过CPython 3.5.2获得的。
我注意到set
类的某些操作有奇怪的表现。
我已经测量了执行仅包含整数的两个集的并集所需的时间。当然,这个时间取决于套装的尺寸。令人惊讶的是,它还取决于整数的“密度”。这是一个情节:
x轴是两组大小的总和(对于每种经验,它们是随机选择的,彼此独立地选择)。 y轴是时间,以秒为单位(以对数刻度)。
密度d
表示通过从总共N
个整数中采样N/d
整数来实例化集合。换句话说,对于密度为0.5,我们采用某个区间的整数的一半,而对于密度为0.1,我们采用一些(较大)区间的整数的十分之一。
这是获取一些结果的最小代码(如果需要,我可以发布我用于绘图的完整代码,但它更长)。
import time
import random
import numpy
def get_values(size, density):
return set(random.sample(range(int(size/density)), size))
def perform_op(size, density):
values1 = get_values(size, density)
values2 = get_values(size, density)
t = time.time()
result = values1 | values2
return time.time()-t
size = 10000000
for density in [0.05, 0.1, 0.5, 0.99]:
times = [perform_op(size, density) for _ in range(10)]
print('density: %.2f, mean time: %.4f, standard deviation: %.4f' % (density, numpy.mean(times), numpy.std(times)))
联:
density: 0.05, time: 0.9846, standard deviation: 0.0440
density: 0.10, time: 1.0141, standard deviation: 0.0204
density: 0.50, time: 0.5477, standard deviation: 0.0059
density: 0.99, time: 0.3440, standard deviation: 0.0020
最快和最慢之间的计算时间大约为3倍,其中集合具有相同大小。 此外,低密度的可变性更大。
有趣的是,对于交叉点(在values1 | values2
函数中替换values1 & values2
除perform_op
),我们也有非常量的表现,但模式不同:
density: 0.05, time: 0.3928, standard deviation: 0.0046
density: 0.10, time: 0.4876, standard deviation: 0.0041
density: 0.50, time: 0.5975, standard deviation: 0.0127
density: 0.99, time: 0.3806, standard deviation: 0.0015
我没有测试其他设置操作。
我不明白为什么会有这样的差异。据我所知,Python集是用哈希表实现的,所以只要它们的哈希值很好地传播,整数的密度就不重要了。
这些不同表演的起源是什么?
答案 0 :(得分:2)
这里有两个主要因素:
int
有一个非常简单的哈希码;它只是int
的价值。所以hash(1234) == 1234
。对于密集输入,这意味着您大多数是连续的哈希码,没有重叠,因为值总是小于set
桶的数量(例如,有100,000个值,您有262,144个桶;当值密集时,您的哈希码范围从0到101,010,因此没有实际的环绕发生模262144)。更重要的是,哈希码在很大程度上是连续的意味着内存以大部分顺序模式访问(帮助CPU缓存获取启发式)。对于稀疏输入,这不适用;你将有许多不相等的值散列到同一个桶中(因为0.05案例的2,000,000个值中的每一个都有7-8个不同的值,当有262,144个桶时,这些值会散列到同一个桶中)。由于Python使用封闭散列(也称为开放寻址),因此具有不相等值的存储桶冲突最终会跳过整个内存(阻止CPU缓存帮助尽可能多)来为新值找到存储桶。演示存储桶冲突问题:
>>> import random
>>> vals = random.sample(xrange(int(100000/0.99)), 100000)
>>> vals_sparse = random.sample(xrange(int(100000/0.05)), 100000)
# Check the number of unique buckets hashed to for dense and sparse values
>>> len({hash(v) % 262144 for v in vals})
100000 # No bucket overlap at all
>>> len({hash(v) % 262144 for v in vals_sparse})
85002 # ~15% of all values generated produced a bucket collision
碰撞的这些值中的每一个都必须绕set
寻找未占用的存储桶,密集值根本不会发生冲突,因此它们可以完全避免这种成本。
如果您想要一个可以修复这两个问题的测试(同时仍使用密集和稀疏输入),请使用float
s(不等于int
值),因为{{1} } hashing尝试将float
等效int
哈希到与float
相同的值。要避免实际上相等的值的不同级别,请从非重叠值中选择输入,因此稀疏与密集不会更改生成的联合的大小。这是我使用的代码,无论密度如何,都会以相当均匀的时间结束:
int