高效的大范围随机生成器(python)

高效的大范围随机生成器(python),python,performance,generator,shuffle,Python,Performance,Generator,Shuffle,我正在尝试创建一个生成器,它返回给定范围内的数字,这些数字通过函数foo给定的特定测试。然而,我想在一个随机顺序的数字进行测试。以下代码将实现此目的: from random import shuffle def MyGenerator(foo, num): order = list(range(num)) shuffle(order) for i in order: if foo(i): yield i 问题 此解决方案的问题

我正在尝试创建一个生成器,它返回给定范围内的数字,这些数字通过函数
foo
给定的特定测试。然而,我想在一个随机顺序的数字进行测试。以下代码将实现此目的:

from random import shuffle

def MyGenerator(foo, num):
    order = list(range(num))
    shuffle(order)
    for i in order:
        if foo(i):
            yield i
问题

此解决方案的问题是,有时范围会非常大(
num
的顺序可能是
10**8
及以上)。由于内存中有这么大的列表,此函数可能会变慢。我已尝试使用以下代码避免此问题:

from random import randint    

def MyGenerator(foo, num):
    tried = set()
    while len(tried) <= num - 1:
        i = randint(0, num-1)
        if i in tried:
            continue
        tried.add(i)
        if foo(i):
            yield i

(只要双射不是在一组大大大于
num
次数
index的数字上,那么最好的算法可能取决于
num
的值,那么为什么不在一个生成器中使用两个可选算法呢

您可以将
shuffle
set
解决方案与
num
值的阈值混合使用。这基本上是在一个生成器中组合您的前两个解决方案:

from random import shuffle,randint

def MyGenerator(foo, num):
    if num < 100000 # has to be adjusted by experiments
      order = list(range(num))
      shuffle(order)
      for i in order:
          if foo(i):
              yield i
    else:   # big values, few collisions with random generator 
      tried = set()
      while len(tried) < num:
        i = randint(0, num-1)
        if i in tried:
           continue
        tried.add(i)
        if foo(i):
           yield i
来自随机导入洗牌,randint
def MyGenerator(foo,num):
如果num<100000,则必须通过实验进行调整
顺序=列表(范围(数量))
洗牌(顺序)
对于我,顺序如下:
如果是foo(i):
产量一
否则:#值大,与随机生成器的冲突少
尝试=设置()
而len(尝试)

randint
解决方案(对于
num
的大值)运行良好,因为随机生成器中没有太多重复。

问题基本上是生成范围
0..n-1
内整数的随机排列

幸运的是,这些数字有一个非常有用的属性:它们都有一个不同的值模
n
。如果我们能对这些数字应用一些数学运算,同时注意保持每个数字模
n
不同,就很容易生成一个随机排列。最好的是我们不需要任何我mory需要跟踪我们已经生成的数字,因为每个数字都是用一个简单的公式计算的


我们可以对范围内的每个编号
x
执行的操作示例包括:

  • 加法:我们可以将任何整数
    c
    添加到
    x
  • 乘法:我们可以将
    x
    与任何不与
    n
    共享素数因子的
    m
    相乘
仅在范围
0..n-1
上应用这两个操作已经给出了非常令人满意的结果:

>>> n = 7
>>> c = 1
>>> m = 3
>>> [((x+c) * m) % n for x in range(n)]
[3, 6, 2, 5, 1, 4, 0]
看起来很随意,不是吗

如果我们从一个随机数生成
c
m
,它实际上也是随机的。但请记住,不能保证该算法将生成所有可能的置换,或者每个置换都具有相同的生成概率


实施 实现的困难部分实际上只是生成一个合适的随机
m

import random

# credit for prime factorization code goes
# to https://stackoverflow.com/a/17000452/1222951
def prime_factors(n):
    gaps = [1,2,2,4,2,4,2,4,6,2,6]
    length, cycle = 11, 3
    f, fs, next_ = 2, [], 0
    while f * f <= n:
        while n % f == 0:
            fs.append(f)
            n /= f
        f += gaps[next_]
        next_ += 1
        if next_ == length:
            next_ = cycle
    if n > 1: fs.append(n)
    return fs

def generate_c_and_m(n, seed=None):
    # we need to know n's prime factors to find a suitable multiplier m
    p_factors = set(prime_factors(n))

    def is_valid_multiplier(m):
        # m must not share any prime factors with n
        factors = prime_factors(m)
        return not p_factors.intersection(factors)

    # if no seed was given, generate random values for c and m
    if seed is None:
        c = random.randint(n)
        m = random.randint(1, 2*n)
    else:
        c = seed
        m = seed

    # make sure m is valid
    while not is_valid_multiplier(m):
        m += 1

    return c, m
您的生成器功能可以实现为

def MyGenerator(foo, num):
    for x in random_range(num):
        if foo(x):
            yield x

在Python中获得最佳性能要比低级语言要复杂得多。例如,在C中,通常可以通过移位来替换乘法,在热内环中节省一点点。Python字节码方向的开销消除了这一点。当然,当考虑“Python”的哪一个变体时,这种情况又会发生变化。你的目标是(pypy?numpy?cython?)-你真的必须根据你使用的代码编写代码

但更重要的是安排操作以避免序列化依赖,因为现在所有的CPU都是超标量的。当然,真正的编译器知道这一点,但在选择算法时仍然很重要


在现有答案的基础上获得一点答案的最简单方法之一是使用numpy.arange()生成数据块,并将
((x+c)*m)%n
直接应用于numpy ndarray。可以避免的每个python级别的循环都会有所帮助

如果该函数可以直接应用于numpy ndarray,那就更好了。当然,python中足够小的函数无论如何都会受到函数调用开销的支配


今天最好的快速随机数生成器是。我编写了一个纯python端口,但重点是灵活性和易理解性,而不是速度

Xoroshiro128+是第二好的质量和速度,但信息量较少

Python(和许多其他人)默认选择的Mersenne Twister是最糟糕的选择之一

(还有一种叫做splitmix64的东西,我不太了解它的位置——有人说它比xoroshiro128+好,但它有一个周期问题——当然,你可能想在这里看到它)

默认PCG和xoroshiro128+都使用2N位状态生成N位数字。这通常是可取的,但意味着数字将重复。然而,PCG有其他模式可以避免这种情况

当然,这在很大程度上取决于
num
是否(接近)为2的幂。理论上,可以为任何位宽度创建PCG变体,但由于需要显式掩蔽,目前只实现了不同的字大小。我不确定如何确切地为新的位大小生成参数(可能在论文中?),但只需进行周期/2跳转并验证值是否不同,即可对其进行测试

当然,如果你只给RNG打了200个电话,你可能实际上不需要在数学方面避免重复


或者,您可以使用,它确实存在于每个位大小(尽管请注意,它从不生成全零值(或者等效地,全零值)-
def random_range(n, seed=None):
    c, m = generate_c_and_m(n, seed)

    for x in range(n):
        yield ((x + c) * m) % n
def MyGenerator(foo, num):
    for x in random_range(num):
        if foo(x):
            yield x