【问题标题】:"The Genuine Sieve of Eratosthenes" in Python - why is heapq slower than dict?Python中的“Eratosthenes的真正筛子” - 为什么heapq比dict慢?
【发布时间】:2012-11-07 22:37:15
【问题描述】:

M. O'Neill's great paper 之后,我尝试在 Python 中实现一些惰性的、无限版本的 Eratosthenes 筛。我惊讶地发现,论文声称应该运行得更快的基于堆的版本实际上对我来说慢了两倍多。

本文包含两个示例,一个基于 dict,我已将其翻译(来自 Haskell):

from itertools import count
def dict_sieve():
    yield 2
    yield 3
    candidates = count(5, 2)
    composites = {9:{3}}  # map composites to their prime factors

    for candidate in candidates:
        try:
            factors = composites.pop(candidate)
        except KeyError:  # if it's not in the dict, it's prime
            yield candidate
            composites[candidate**2] = {candidate}  # Euler's optimization: start from prime**2
        else:
            for prime in factors:  # go through the prime factors and increment their keys
                try:
                    composites[candidate+prime*2].add(prime)  # use prime*2 because we are ignoring evens
                except KeyError:
                    composites[candidate+prime*2] = {prime}

论文中的第二个例子演示了优先级队列作为数据结构的使用。它还使用惰性列表,而不是简单的增量,为了公平测试,我没有这样做。 (此外,我在惰性列表中使用了 itertools.count 的实例,但我发现它的运行速度稍慢)。

from itertools import count
from heapq import heappush, heapreplace
def heap_sieve():
    yield 2
    yield 3
    candidates = count(5,2)
    composites = [(9, 3)]  # a priority queue of composite/factor pairs, keyed by composite

    for candidate in candidates:
        prime_flag = True
        while composites[0][0] == candidate:  # loop because there may be duplicates
            prime_flag = False  # signal to the if statement below
            composite, prime = composites[0]
            heapreplace(composites, (composite + prime*2, prime))

        if prime_flag:
            yield candidate
            heappush(composites, (candidate**2, candidate))

我对两者进行了计时,还有一个“渴望”版本,此处未复制,它会生成一个低于限制的所有素数的列表:

In [44]: from itertools import islice

In [45]: %timeit list(islice(dict_sieve(), 100000))
    ...: %timeit list(islice(heap_sieve(), 100000))
    ...: %timeit eager_sieve(1299710)  # 1299709 is the 100,000th prime
    ...: 
1 loops, best of 3: 2.12 s per loop
1 loops, best of 3: 4.94 s per loop
1 loops, best of 3: 677 ms per loop

'eager' 版本更快并不奇怪 - 它基本上是在内存使用、必须指定上限的不便和 CPU 时间之间进行权衡。然而,我确实发现 heapq 版本的速度要慢得多,而这篇论文声称它更高效。我的实施有问题吗?或者仅仅是因为我们都知道,dicts 非常快(而heapq 相对较慢)?

【问题讨论】:

  • @senderle 感谢您的编辑。我一直在不知不觉中拼错他的名字!
  • 是的,这是一个很小的编辑,但似乎值得,因为它是一个容易拼错的名字。我必须自己仔细检查拼写!

标签: python performance heap sieve-of-eratosthenes


【解决方案1】:

实际上,基于字典的方法应该比基于堆队列的方法更快。堆插入和替换操作是O(log n),而字典插入和替换操作是O(1)。

确实,我很惊讶地听到该论文的作者声称并非如此。但事实上并非如此。您假设Data.Map 被实现为散列映射,但实际上,它是size balanced binary tree。所以它的性能特征与堆队列的性能特征非常相似。不同之处在于从堆队列中检索最小键是 O(1),这加快了部分筛选代码的速度——但哈希映射更快。

【讨论】:

  • 很好的答案!谢谢你。你知道为什么 Data.Map 不使用哈希表吗?哈希表数据结构是否不适合函数式方法?
  • @poorsod,这是我经常问自己的一个问题,但从来没有给出令人满意的答案。 Data 中有一个 HashTable 类型,所以这似乎不是不可能的。
  • @poorsod:你可以在二叉树中做很多在哈希表中做不到的事情——比如范围查询,或者获取最小/最大元素。另一个因素是,在编写Data.Map 时,我怀疑 Haskell 中的数组支持是否非常好。
猜你喜欢
  • 2011-09-26
  • 2016-04-08
  • 2018-02-11
  • 1970-01-01
  • 1970-01-01
  • 2017-08-31
  • 1970-01-01
  • 2022-01-17
  • 1970-01-01
相关资源
最近更新 更多