设定操作的奇怪表现

时间:2017-02-06 21:55:23

标签: python performance set

所有这些结果都是通过CPython 3.5.2获得的。

我注意到set类的某些操作有奇怪的表现。

我已经测量了执行仅包含整数的两个集的并集所需的时间。当然,这个时间取决于套装的尺寸。令人惊讶的是,它还取决于整数的“密度”。这是一个情节:

plot of the time needed to compute a set union

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 & values2perform_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集是用哈希表实现的,所以只要它们的哈希值很好地传播,整数的密度就不重要了。

这些不同表演的起源是什么?

1 个答案:

答案 0 :(得分:2)

这里有两个主要因素:

  1. 你正在制作不同规模的作品;对于密集输入,绝大多数值都会重叠,因此最终会产生更小的输出。
  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缓存帮助尽可能多)来为新值找到存储桶。
  3. 演示存储桶冲突问题:

    >>> 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