[Haskell-cafe] Re: Code and Perf. Data for Prime Finders (was:
Genuine Eratosthenes sieve)
Melissa O'Neill
oneill at cs.hmc.edu
Sun Feb 25 07:51:42 EST 2007
For those enjoying the fun with prime finding, I've updated the
source at
http://www.cs.hmc.edu/~oneill/code/haskell-primes.zip
I've tweaked my code a little to improve its space behavior when
finding primes up to some limit, added an up-to-limit version of the
Naive Primes algorithm, and added Oleg's prime finding code too.
I also got a chance to look at space usage more generally. I won't
reproduce a table here, but the conclusions were more-or-less what
you'd expect. The "unlimited list" algorithms used O(n) space to
find n primes (except for Runciman's algorithm, which appeared to be
much worse), and the "primes up to a limit" algorithms used O(sqrt
(n)) space to find the nth prime.
Both of these are better than the classic C algorithm, which uses O(n
log n) space to find the nth prime. For example, heap profiling
shows that my own O(sqrt(n)) algorithm uses only 91200 bytes to find
the 10^7th prime, whereas the classic C algorithm needs at least
11214043 bytes for its array -- a factor of more than 100 different,
and one that gets worse for larger n.
Lennart Augustsson wrote:
> Another weird thing is that much of the Haskell code seems to work
> with Integer whereas the C code uses int.
Originally, I was comparing Haskell with Haskell, and for that
purpose I wanted to have a level playing field, so going with Integer
everywhere made sense.
> That doesn't seem fair.
Actually, to the extent that any of the comparisons are "fair", I
think this one is too. After all, typical Haskell code uses Integer
and typical C code uses int. I could use arrays in my Haskell code
and never use laziness, but when I program in Haskell, I'm not trying
to exactly recreate C programs, but rather write their Haskell
equivalents. For example, to me, producing a lazy list was essential
for a true Haskell feel. For some people, the "Haskell feel" also
includes treating the language as a declarative specification
language where brevity is everything -- but for me, other things
(like fundamental algorithmic efficiency and faithfulness to the core
ideas that make the Sieve of Eratosthenes an *efficient* algorithm)
are universal and ought to be common to both C and Haskell versions.
But to allow a better comparison with C, I've added a run for an Int
version of my algorithm. With that change, my code is closer to the
speed of the C code. More interestingly, for larger n, I seem to be
narrowing the gap. At 10^6, my code runs nearly 30 times slower than
the classic C version, but at 10^8, I'm only about 20 times slower.
This is especially interesting to me there was some (reasonable
looking) speculation from apfelmus several days ago, that suggested
that my use of a priority queue incurred an extra log(n) overhead,
from which you would expect a worse asymptotic complexity, not
equivalent or better.
Melissa.
Enc. (best viewed with a fixed-width font)
------------------------------------------------------------------
Time (in seconds) for Number of Primes
----------------------------------------------------
Algorithm 10^3 10^4 10^5 10^6 10^7 10^8
------------------------------------------------------------------
C-Sieve 0.00 0.00 0.01 0.29 5.12 88.24
O'Neill (#3) 0.01 0.04 0.55 8.34 122.62 1779.18
O'Neill (#2) 0.01 0.06 0.95 13.85 194.96 2699.61
O'Neill (#1) 0.01 0.07 1.07 15.95 230.11 -
Bromage 0.02 0.39 6.50 142.85 - -
"sieve" (#3) 0.01 0.25 7.28 213.19 - -
Naive (#2) 0.02 0.59 14.70 386.40 - -
Naive (#1) 0.32 0.66 16.04 419.22 - -
Runciman 0.02 0.74 29.25 - - -
Reinke 0.04 1.21 41.00 - - -
Zilibowitz 0.02 2.50 368.33 - - -
Gale (#1) 0.12 17.99 - - - -
"sieve" (#1) 0.16 32.59 - - - -
"sieve" (#2) 0.01 32.76 - - - -
Oleg 0.18 68.40 - - - -
Gale (#2) 1.36 268.65 - - - -
------------------------------------------------------------------
- The dashes in the table mean "I gave up waiting" (i.e., > 500 seconds)
- "sieve" (#1) is the classic example we're all familiar with
- "sieve" (#2) is the classic example, but sieving a list without
multiples of 2,3,5, or 7 -- notice how it makes no real difference
- "sieve" (#3) is the classic example, but generating a lazy-but-
finite list (see below)
- O'Neill (#1) is basically the algorithm of mine discussed in http://
www.cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf, with a few minor tweaks
- O'Neill (#2) is a variant of that algorithm that generates a lazy-
but-finite list of primes.
- O'Neill (#3) is a variant of that algoritm that uses Ints when it
can get away with it.
- Naive (#1) is the the non-sieve-based "divide by every prime up to
the square root" algorithm for finding primes (called SimplePrimes in
the source)
- Naive (#2) is the same algorithm, with a limit on the number of primes
- Runciman is Colin Runciman's algorithm, from his _Lazy Wheel Sieves
and Spirals of Primes_ paper
- Reinke is the ``applyAt'' algorithm Claus Reinke posted here
- Gale (#1) is Yitz Gale's deleteOrd algorithm
- Gale (#2) is Yitz Gale's crossOff algorithm
- Oleg is oleg at pobox.com's algoirthm, as posted to Haskell Cafe
- Zilibowitz is Ruben Zilibowitz's GCD-based primes generator, as
posted on Haskell-Cafe
- Bromage is Andrew Bromage's implementation of the Atkin-Bernstein
sieve. Like O'Neill (#2) and "sieve" (#3), asks for some upper limit
on the number of primes it generates. Unlike O'Neill (#2) and
"sieve" (#3), it uses arrays, and the upper limit causes a large
initial array allocation. Also, unlike the other Haskell algorithms,
it does not produce a lazy list; no output is produced until sieving
is complete
- C-Sieve is a "typical" simple implementation of the sieve in C
found with Google; it skips multiples of 2 and uses a bit array.
Also, obviously, it doesn't produce incremental output.
More information about the Haskell-Cafe
mailing list