【问题标题】:Can I optimize my prime number summing function with generators?我可以使用生成器优化我的素数求和函数吗?
【发布时间】:2019-08-30 00:23:57
【问题描述】:

问题:虽然下面的代码有效,但它需要很长时间才能找到小于 2,000,000 的所有素数之和。

过去的尝试:我尝试实现 while 循环、计数器和许多其他工具来修改代码,但它们最终也修改了我的结果。以前,我只是将数字添加到现有变量中,而不是将它们附加到列表中,但结果是相同的。

我相信生成器函数/表达式会解决问题,但我在实现函数、表达式或两者时遇到了麻烦。

# Prime number determiner 
def is_prime(x):
    for i in range(2, x-1):
        if x % i == 0:
            return False
    else:
        return True

# Function summing all prime numbers between 2 and 2,000,000
for i in range(2, 2000000):
    if is_prime(i) is True:
        primes.append(i)
results = sum(primes)
print(primes)

生成器表达式/函数的先前尝试:

#Generator version of above
def is_prime_gen(x):
     yield (i for i in range(2, x-1) if x % i == 0)
sum_prime += (j for j in range(2, 2000000) if is_prime_gen(j))

预期结果:我不需要处理超快的结果,但我希望它在一两分钟内处理。

奖励:对于任何回复的人,如果您还可以解释您是如何得出结论的,那将对我有所帮助(虽然“经验”是一个有效的解释,但它没有帮助)。

【问题讨论】:

  • 我不知道你的问题完全是重复的,但是有很多素数生成函数in this previous question
  • @Blckknght - 是的,我在发布我的问题之前看到了那个。我认为它在一般意义上的优化方面很有帮助,但我没有在帖子中看到在解决方案中使用生成器的任何地方。也许这是我对生成器的误解的问题,但这似乎是一个可以从它们的实施中受益的完美情况。
  • 如果您出于某种原因想要强调生成器功能,我认为您需要重新表述您的问题。您的问题强调性能(因为您当前的代码太慢),并且您显示的第一个代码块根本不使用生成器函数。
  • 更新了@Blckknght。
  • 想要将性能降低到几分钟并不是很有野心。我不明白为什么使用基于筛子的方法只需几秒钟。生成器似乎不太相关。缺少生成器并不是您的代码滞后的原因。

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


【解决方案1】:

您对生成器函数的关注是the XY problem 的一个示例。您已经决定解决代​​码性能问题的方法是使用生成器,但这实际上并不正确。当您得到与生成器无关的答案时,您会认为它们没有帮助,而我们其他人只是对为什么生成器首先相关感到有些困惑。

让我们检查一下您遇到性能问题的原因。主要问题是您的代码需要O(n) 时间来确定每个数字n 是否为素数。您必须对从两个到您的限制的每个数字执行此操作。这意味着整个算法需要 O(N**2) 时间,其中 N 是要检查的最大数字(例如 200 万)。对于较大的N,您的代码将需要很长时间。

为您的素数使用生成器本身并不会改善这一点。确定每个候选值是否为素数仍然需要很长时间,如果您坚持当前的算法,您仍然需要检查所有相同的数字。充其量就像将质数立即添加到运行总和中一样好,而不是将它们放入列表并在最后求和。也就是说,它可以节省你的记忆,但不能节省时间。

显着提高性能的真正方法是使用工作量更少的更智能算法。有很多好方法可以在更短的时间内找到素数。有些可以实现为生成器,但在这种情况下,一次计算所有素数并使用额外的内存来换取更好的性能可能是一个合理的权衡。

这是因为您的计算机一次可以在内存中保存数十亿个整数。少于几十亿的数字在 Python 中使用大约 28 个字节,因此其中 200 万个数字大约需要 56 MB,再加上列表数据结构大约需要 18 MB。因此,您可以执行内存密集型算法,而无需担心内存使用情况。

这是Sieve of Eratosthenes 算法的一个非常快速的实现,用于在纯 Python 中计算所有小于 N 的素数。该实现最初是由 this answer 中的 Robert Williams Hanks 实现的,但此版本由 Bruno Astrolino 进行了一些调整,以便在 this answer 中的 Python 3.6+ 中更有效地工作。

from itertools import compress

def rwh_primes1v1(n):
    """ Returns  a list of primes < n for n > 2 """
    sieve = bytearray([True]) * (n//2)
    for i in range(3,int(n**0.5)+1,2):
        if sieve[i//2]:
            sieve[i*i//2::i] = bytearray((n-i*i-1)//(2*i)+1)
    return [2,*compress(range(3,n,2), sieve[1:])]

您可能想运行sum(rwh_primes1v1(2_000_000))。在我的计算机上,这大约需要 30 毫秒,而您的代码需要 30 秒(长 1000 倍),N=100_000(少了 20 倍)。我不愿意等待 N=2_000_000 的低效算法需要三个小时左右。

请注意,如果您确实需要一个生成器,因为某些其他原因生成素数,the answers to this question 中有一些很好的无限素数生成器实现。使用它们中的任何一个来解决求和问题都不太可能导致比我上面提供的代码更快的代码,直到你得到如此大的N,以至于你无法一次将整个筛子放入内存中(而且只有一些的生成器将对此有所帮助,有些生成器本身具有显着的内存开销)。

【讨论】:

  • 我认为这应该是公认的答案。它并没有简单地忽略问题中的发电机部分,而是解释了它是如何错位的,然后给出了一个解决方案,将竞争对手(包括我的混合筛/发电机)从水中吹走。
  • @Blckknght 我可以看到我可能正在进入“XY”领域。不过,在您回答之前,我没有看到答案指出为什么生成器不会像我想象的那样有益(关于似乎另有说明的文档)。感谢您的详尽解释并同意约翰·科尔曼的观点。
【解决方案2】:

我认为关键问题是如何快速正确地找到所有素数。还有关于它的many answers。我找到一个如下:

def isprime(n):
    """Returns True if n is prime."""
    if n == 2:
        return True
    if n == 3:
        return True
    if n % 2 == 0:
        return False
    if n % 3 == 0:
        return False

    i = 5
    w = 2

    while i * i <= n:
        if n % i == 0:
            return False

        i += w
        w = 6 - w

    return True

sum = 0
for n in range(2, 20000):
    if isprime(n):
        sum += n

在 range(2, 10000) 时,时间成本为:

0.0043639220002660295  # this answer
0.25401434600007633  # your answer

当涉及到(2, 100000)时,时间成本为:

0.1730230279999887  # this answer
19.639503588000025  # your answer

【讨论】:

  • 感谢您的建议。虽然我同意有许多替代解决方案,但我的问题是关于生成器的实施。
【解决方案3】:
import time
prime = (i for i in range(2, 2000000) if is_prime(i))

def is_prime(num):
    if num == 2:
        return True
    if num == 3:
        return True
    if num % 2 == 0:
        return False
    if num % 3 == 0:
        return False

    i = 5
    w = 2

    while i * i <= num:
        if num % i == 0:
            return False

        i += w
        w = 6 - w

    return True

print(sum(prime))
print(time.perf_counter())

我不是专家,但我认为这应该可行并且很容易理解。 我使用了 ToughMind 共享的改进功能。我的系统需要 15.5 秒来计算总和

【讨论】:

  • 这是有道理的,而且这个解决方案确实加快了速度。关于如何生成其余代码的任何想法,以我在上面的问题中的形式,但与我的原始代码不同,以一种实际有效的方式?
【解决方案4】:

这是一个使用混合引导方法的生成器。它使用一个(不是特别有效的)筛子来识别平方根以下的素数,并在产生它们时存储它们,然后使用这些来对低于n 的剩余奇数进行试除。对于n = 2_000_000,它从不存储超过 700 个数字,因此它的内存占用很小(以更多的处理时间为代价):

import math

def primes(n):
    k = 1 + int(math.sqrt(n))
    #phase 1: sieve to k
    if n >= 2:
        yield 2
        small_primes = [2]
        candidates = [2*i + 1 for i in range(1,(k+1)//2)]
        while len(candidates) > 0:
            p = candidates[0]
            small_primes.append(p)
            candidates = [x for x in candidates if x % p != 0]
            yield p
    #at this stage we have all primes below k
    #loop through remaining odd numbers
    #dividing by these primes
    if k%2 == 0: k +=1
    while k <= n:
        if all(k%p != 0 for p in small_primes): yield k
        k += 2

我没有费心去计时,但sum(primes(2_000_000)) 大约需要 3 秒。我没有费心计时的原因是因为我不想让它与 Blckkght 的代码相比感到尴尬——这表明了非生成器优化的筛子方法的速度有多快。

【讨论】:

    【解决方案5】:

    这个查询的答案归结为您所说的优化。生成器可用于优化 space 的使用。您浪费空间的地方在您的主代码中的这个逻辑中:

    primes.append(i)
    

    您的is_prime() 函数不会浪费空间。生成器仅在序列计算可以提前中止时节省时间,而不是完全创建然后部分使用。这里不是这样。

    这是一个简单的返工,它可以按时间清理您的 is_prime() 实现,并使用 生成器表达式 来避免创建素数列表:

    def is_prime(number):
        if number <= 2 or number % 2 == 0:
            return number == 2
    
        for divisor in range(3, int(number ** 0.5) + 1, 2):
            if number % divisor == 0:
                return False
    
        return True
    
    result = sum(number for number in range(2, 2_000_000) if is_prime(number))
    
    print(result)
    

    这在大约 10 秒内完成任务,完全在您的一两分钟限制内,并且不需要太多代码。这不是最佳的时间方面,只是更好的时间方面和合理的最佳空间方面

    重温

    生成器还有另一种方式可以提供超出我上面描述的时间改进。与is_prime() 不同,它可以随时传递任何数字,生成器可以保证它将使用升序数字,因此它可以简化假设。同样,它可以在调用之间保持状态,这与实现的is_prime() 不同。让我们通过生成素数来重新解决这个问题:

    def prime_generator(limit):
        yield 2
    
        number = 3
    
        while number <= limit:
            for divisor in range(3, int(number ** 0.5) + 1, 2):
                if number % divisor == 0:
                    break
            else:  # no break
                yield number
    
            number += 2
    
    print(sum(prime_generator(2_000_000)))
    

    使用这种方法的各种安排,与我原来的解决方案相比,它最多可提供 5% 的速度。

    筛子

    最后,让我们用筛子解决这个问题。这种方法比上述解决方案使用更多的空间来获得时间方面的性能:

    def sum_primes(limit):  # assumes limit > 1
        sieve = [False, False, True] + [True, False] * ((limit - 1) // 2)
        number = 3
        result = 2
    
        while number <= limit:
            if sieve[number]:
                result += number
    
                for i in range(number * number, limit + 1, number):
                    sieve[i] = False
            number += 2
    
        return result
    
    print(sum_primes(2_000_000))
    

    这在我的系统上不到 1 秒的时间内对素数求和。它比以前基于生成器的解决方案快 15 倍。

    【讨论】:

    • 我对生成器的理解是,它们有效地搁置了一部分循环,同时产生要使用的结果。因此,如果我试图循环浏览 2 百万的列表。素数#s,并在计算中使用每个数字(即总和),这似乎是一个理想的机会。我同意你的观点,我提出的列表选项不是最好的(恰好是我上次实施的 - 因此我提到了我使用的替代方案)。我对您的解决方案的理解是,上面的两个 if 语句和生成器语句会比两个生成器语句更快吗?
    • @onhamae,这可能是使用生成器的机会,但不一定会通过使用生成器来优化。我加入了一个生成器表达式,它会做一些好事。我没有将函数转换为生成器,因为它不会有所作为。如果您真的想优化代码,请改用 筛子
    • 这段代码中有一个生成器,虽然它不是一个函数。表达式 number for number in range(2, 2_000_000) if is_prime(number) 是一个生成器表达式(尽管如果您在顶层声明它,则需要添加括号,而不是作为函数调用的唯一参数)。
    • 如果有人需要文档,here 是我关注生成器(特别是表达式)的理由的一部分。引用的好处是节省内存,“特别是使用 sum() 之类的函数......将可迭代输入减少为单个值”。
    • @onhamae,我已经为我的答案添加了几个额外的解决方案——一个基于生成器的解决方案,我相信你想要的,一个基于筛子的解决方案,用于速度比较。跨度>
    【解决方案6】:

    这是由 Willy Good 创建的一个非常快速的纯 Python 素数生成器,可在评论 here 中找到。就您的特定用例的性能和复杂性而言,这可能有点过头了,但我认为 Python 中的许多 stackoverflow 素数的人都没有意识到这一点。

    def primes235(limit):
        yield 2; yield 3; yield 5
        if limit < 7: return
        modPrms = [7,11,13,17,19,23,29,31]
        gaps = [4,2,4,2,4,6,2,6,4,2,4,2,4,6,2,6] # 2 loops for overflow
        ndxs = [0,0,0,0,1,1,2,2,2,2,3,3,4,4,4,4,5,5,5,5,5,5,6,6,7,7,7,7,7,7]
        lmtbf = (limit + 23) // 30 * 8 - 1 # integral number of wheels rounded up
        lmtsqrt = (int(limit ** 0.5) - 7)
        lmtsqrt = lmtsqrt // 30 * 8 + ndxs[lmtsqrt % 30] # round down on the wheel
        buf = [True] * (lmtbf + 1)
        for i in xrange(lmtsqrt + 1):
            if buf[i]:
                ci = i & 7; p = 30 * (i >> 3) + modPrms[ci]
                s = p * p - 7; p8 = p << 3
                for j in range(8):
                    c = s // 30 * 8 + ndxs[s % 30]
                    buf[c::p8] = [False] * ((lmtbf - c) // p8 + 1)
                    s += p * gaps[ci]; ci += 1
        for i in xrange(lmtbf - 6 + (ndxs[(limit - 7) % 30])): # adjust for extras
            if buf[i]: yield (30 * (i >> 3) + modPrms[i & 7])
    

    Robert William Hanks'最佳纯Python解决方案的速度比较,更紧凑,更易于理解:

    $ time ./prime_rwh2.py 1e7
    664579 primes found < 1e7
    
    real    0m0.883s
    user    0m0.266s
    sys     0m0.047s
    $ time ./prime_wheel.py 1e7
    664579 primes found < 1e7
    
    real    0m0.285s
    user    0m0.234s
    sys     0m0.063s
    

    Willy Good 的解决方案是 mod 30 轮筛,它避免使用/存储 2、3 和 5 的倍数,除非手动生成它们以使其完整。在我的笔记本电脑中的 8G RAM 完全用完并且系统崩溃的情况下,它对我来说非常有用,大约 2.5e9。

    【讨论】:

      【解决方案7】:

      get_sum_of_primes_in_range(0, constant_max_value) 的结果将是一个常数,可以预先计算。

      get_sum_of_primes_in_range(0, n+x) 的结果可以写成get_sum_of_primes_in_range(0, n) + get_sum_of_primes_in_range(n, x)

      通过结合这些东西;您可以为n 的选定值提供一个预先计算的结果表,并且只使用处理时间来查找get_sum_of_primes_in_range(n, x) 部分。

      基本上;而不是做get_sum_of_primes_in_range(0, x);你可以做k = x / 100n = k * 100result = table[k] + get_sum_of_primes_in_range(n, x)并跳过大量的工作;您希望能够跳过的工作量(平均而言)取决于您希望制作该预先计算结果表的大小。

      对于get_sum_of_primes_in_range(n, x),您需要基于“埃拉托色尼筛”的东西(请参阅https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes)。请注意,埃拉托色尼筛法可以使用模数从任意值开始,不必从 0 开始。

      【讨论】:

      • 这似乎是在描述与此处所问问题不同的问题的答案。不需要计算素数的某个子集的总和,只需计算所有素数到一个(可能是常数?)数字。
      • 所以,插入一些实数,这就是我的理解:如果我想得到 02 - 2 百万范围内的素数总和,那么 k = 2000000/100 , n = 2000000?我的数学不好吗?因为这意味着有些东西似乎没有加起来......
      • @onhamae:在我看来是正确的——在这种情况下,您将使用该表来查找 get_sum_of_primes_in_range(0, 2000100) 而不是做任何实际工作,然后您会发现 get_sum_of_primes_in_range(2000000, 200000) 也无济于事;所以你几乎可以立即得到结果。如果你想要从 0 到 2000123 的素数之和,然后是 k = 20001n = 2000100,所以你可以使用表格找到 get_sum_of_primes_in_range(0, 2000100) 没有实际工作,然后做 get_sum_of_primes_in_range(2000100, 2000123),它只有 23 个值,所以它不会几乎是即时的(但不会非常慢)。
      • @onhamae:如果 blckknght 是正确的,并且您只关心找到不超过 2000000 的素数之和,那么您只需要一个 print precomputed_constant,它几乎总是即时的(即使您使用您现有的代码确定一次常量并“剪切并粘贴”该值)。
      【解决方案8】:

      首先,我有数学背景。其次,这使用了 Fermant 的 Little Theroem(虽然我不确定名字,但我忘记了)。我只是用谷歌搜索并花了很多时间编码和调试。在这里!

      '''

      def is_prime():

      a = int(input("Eneter any number"))
      
      for p in range(2, a + 1):
      
          if (a % p == 0):
              isprime = 1
              for j in range(2, (p // 2 + 1)):
      
                  if(p % j == 0):
                      isprime = 0
                      break
      
              if (isprime == 1):
                  print(" %d is a Prime Factor of  %d" %(p, a))
      

      is_prime()

      '''

      祝你有美好的一天!

      【讨论】:

      • 您的数学背景是否解释了为什么这甚至没有错?它不能解决 OP 描述的问题。
      • 这个费马小定理怎么样?它不使用模幂运算。这似乎是一种蛮力试验除法算法,一种比需要做更多的除法的算法。
      猜你喜欢
      • 1970-01-01
      • 2017-07-04
      • 2015-04-28
      • 2015-02-20
      • 1970-01-01
      • 2017-10-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多