【问题标题】:Performance - why is a prime-generating algorithm with range much faster than using prime list?性能 - 为什么具有范围的素数生成算法比使用素数列表快得多?
【发布时间】:2019-05-29 23:49:07
【问题描述】:

我写了两个结构几乎相同的代码,

def prime_gen1(Limit = 10000):
    List = [2,]
    for x in range(3,Limit):
        for y in List:
            if not x%y:
                break
        if not x%y:
            continue
        else:
            List.append(x)
            yield x

def prime_gen2(Limit = 10000):
    from math import floor
    for x in range(3,Limit):
        for y in range(2, floor(x**0.5)+2):
            if not x%y:
                break
        if not x%y:
            continue
        else:
            yield x

>>> list(prime_gen1(20000)) == list(prime_gen2(20000))
True
>>> def time1(number):
    st = time()
    list(prime_gen1(number))
    end = time()
    return end - st

>>> def time2(number):
    st = time()
    list(prime_gen2(number))
    end = time()
    return end - st

一个与另一个做相同的工作,但后者实际上工作得更快。我想知道为什么会这样。

从逻辑上 - 或非逻辑上,我认为用素数检查会超过另一种方式,在这种情况下 - 通过 3 和数字根之间的数字进行检查。但是时间检查反之亦然,检查所有数字的工作速度要快得多 -大约5次。 它的性能越来越不同,

>>> time1(200000)
8.185129404067993
>>> time2(200000)
0.4998643398284912

第二种方法胜过它。这有什么不同?

【问题讨论】:

  • 尝试使用List = {2} 而不是List = [2,] 并告诉我们。
  • 旁注:您可以使用int() 获得正浮点数的下限。另外,测试到int(x ** 0.5) + 1就足够了,不需要去int(x ** 0.5) + 2。
  • @fcracker79:列表只被遍历,所以set 并没有改变。
  • 你首先检查每个素数下面的所有素数;但第二个通过平方根以下的所有数字检查每个素数。这是一个更强大的优化。素数给你一个对数因子,但平方根在复杂性上失去了一个平方根因子。 n^2 比 (n log n)^1.5 差很多(在 n 产生的素数中)。您可以通过测量empirical orders of growth(如log(n2/n1) / log(t2/t1))或绘制运行时间与问题大小的对数图来检测这一点。
  • @Daniel 如果集合没有按特定顺序枚举,那么使用集合实际上会更糟,因为任何任意数字都更有可能具有较小的素因子,因此必须在增加时测试因子订购。

标签: python python-3.x performance-testing primes


【解决方案1】:

列表版本比只检查数字的平方根的检查更多

对于 200000 的限制,平方根是 ~447 小于 200000 的素数有 17983 个

只需添加您执行 x%y 检查的次数,例如

def prime_gen1(Limit = 10000):
    List = [2,]
    modulo_checks = 0
    for x in range(3,Limit):
        for y in List:
            modulo_checks += 1
            if not x%y:
                break
        if not x%y:
            continue
        else:
            List.append(x)
            yield x
    print(modulo_checks)

def prime_gen2(Limit = 10000):
    from math import floor
    modulo_checks = 0
    for x in range(3,Limit):
        for y in range(2, floor(x**0.5)+2):
            modulo_checks += 1
            if not x%y:
                break
        if not x%y:
            continue
        else:
            yield x
    print(modulo_checks)

现在限制 200000,版本 1 进行 162416226 检查,第二个检查 7185445

如果您为列表循环添加提前中断,则列表版本会明显更快(1799767 检查 0.24 秒与 7185445 检查 0.64 秒的两倍)

...
    sq_root = floor(x ** 0.5) + 2
    for y in List:
        modulo_checks += 1
        if not x % y or y > sq_root:
            break
...

如果您想比较算法速度,请删除数学导入

【讨论】:

  • 不使用math.floor()可以使用int(),限制可以设置为int(x ** 0.5) + 1;我们关心的最大可能除法器是平方根,而不是下一个数字。
  • 接下来,您可以完全删除if not x%y: continue 行,并将else: 块附加到for 循环。它只会在for 循环没有看到break 时执行。
  • 是的,但那是题外话,代码风格也不正确,可以进行重构,素筛算法更快等等......问题只是“为什么这段代码比这个更快” - 我尝试对原始内容进行尽可能小的更改以显示或回答问题,因为重命名和重构可能会分散注意力,尤其是对于没有经验的程序员
  • 当然,我只是指出 Python 语法可以帮助解决更多(较小的)低效率问题。
  • 别担心,我只是想解释一下为什么我不会将您的建议纳入我的回答中
【解决方案2】:

一些更好的时机

%timeit list(prime_gen1(10**5))
2.77 s ± 204 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit list(prime_gen2(10**5))
219 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

您在第二个算法中采取了许多优化步骤,但在第一个算法中却没有:这是您的第一个算法的一些问题。

def prime_gen1(Limit = 10000):
    List = [2,]
    for x in range(3,Limit):  # we're checking all even numbers too
        for y in List:  # only need to check up to sqrt(x)!!
            if not x%y:
                break
        if not x%y:  # why are we checking again? Use a for-else construct
            continue
        else:
            List.append(x)  # just return the list at the end
            yield x  # when wrapped in list just copies List

这是第一个算法的优化版本(不是生成器,因为列表中的生成器毫无意义):

def memeff_primelist(n):
    if n <= 2:
        return []
    primes = []  # Add 2 in at the end, don't need to check non-even
    for i in range(3, int(n), 2):
        for p in primes:
            if i % p == 0:  # non-prime
                break
            if p * p > i:  # no factors bigger than sqrt(i)!!!
                primes.append(i)
                break
        else:
            primes.append(i)  # only for i == 3
    primes.insert(0, 2)
    return primes

%timeit memeff_primelist(10**5)
88.9 ms ± 16.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

【讨论】:

  • 请参阅Fastest way to list all primes below N 以获得更好的时机; Robert William Hanks' primes2() 可以在我的机器上以很大的优势击败您的版本(您的筛子在我的机器上达到 75.7 ms ± 1.61 ms 每个循环(平均值±标准偏差。7 次运行,每个循环 10 个循环),primes2() 使其达到 3.07每个循环 ms ± 84.7 µs(平均值 ± 标准偏差。7 次运行,每次 100 个循环),快 20 倍)。 numpy 版本以微秒为单位。
  • sqrt 是一种优化,是的,但是使用 Eratosthenes 筛代替 Trial Division 是一种完全不同的算法。
猜你喜欢
  • 2023-03-29
  • 1970-01-01
  • 2021-07-07
  • 2020-03-20
  • 2013-06-03
  • 2016-11-10
  • 2021-12-12
  • 2021-04-09
  • 2015-10-14
相关资源
最近更新 更多