[Haskell-cafe] Re: Genuine Eratosthenes sieve [Was: Optimization
fun]
Melissa O'Neill
oneill at cs.hmc.edu
Mon Feb 19 13:14:23 EST 2007
Sorry, I'm a little late to this thread, but the topic of
> sieve [] = []
> sieve (x:xs) = x : sieve [y | y <- xs, y `mod` x /= 0]
(as posted by Rafael Almeida) is somewhat dear to my heart, as I
wrote a paper about it last summer and sent it in to JFP. Still
waiting for a reply though.
Let's go back to the beginning with the classic complaint and the
excuses...
Creighton Hogg wrote:
> So a friend and I were thinking about making code faster in
> Haskell, and I was wondering if there was a way to improve the
> following method of generating the list of all prime numbers. It
> takes about 13 seconds to run, meanwhile my friend's C version took
> 0.1.
Here come the excuses, like this one from apfelmus,
> While Haskell makes it possible to express very complicated
> algorithms in simple and elegant ways, you have to expect to pay a
> constant factor (roughly 2x-10x) when competing against the same
> algorithm in low-level C.
and this one from Nicolas Frisby,
> I have yet to need my Haskell to perform well
Matthew Brecknell came up with something much faster, namely
> primes :: [Int]
> primes = 2 : filter isPrime [3,5..] where
> f x p r = x < p*p || mod x p /= 0 && r
> isPrime x = foldr (f x) True primes
FWIW, another way to write this code (without needing to think about
how "fold bails early") is
primes = 2: [x | x <−[3,5..], all (\p −> x `mod` p > 0)
(factorsToTry x)]
where
factorsToTry x = takeWhile (\p −> p*p <= x) primes
Both of these algorithms best the "sieve" we began with and run
quickly, but as you can see (possibly more clearly from my
rephrasing), this algorithm is not actually the Sieve of Eratosthenes
-- it's actually a classic naive primes algorithm which checks a
number for primality by trying to divide it by every prime up to its
square root.
But that's okay, because our initial algorithm ISN'T THE REAL SIEVE
EITHER. Markus Fischmann hits the nail on the head when he says
> The characteristics of a sieve is, that it uses the already found
> primes to generate a list of non-primes that is then removed from a
> list of candiates for primeness.
But then we get distracted by a discussion about avoiding division.
It's true that the real sieve avoids division, but it is NOT true
that every algorithm that avoids division is the sieve. The thread
ends with this algorithm from Yitzchak Gale:
> -- Delete the elements of the first list from the second list,
> -- where both lists are assumed sorted and without repetition.
> deleteOrd :: Ord a => [a] -> [a] -> [a]
> deleteOrd xs@(x:xs') ys@(y:ys')
> | x > y = y : deleteOrd xs ys'
> | x < y = deleteOrd xs' ys
> | otherwise = deleteOrd xs' ys'
> deleteOrd _ ys = ys
>
> sieve (x:xs) = x : sieve (deleteOrd [x+x,x+x+x..] xs)
> sieve _ = []
>
> primes = sieve [2..]
Which seems reasonable, until you realize that every prime p we come
up with is still "considered" by k deleteOrd filters, where k is the
number of primes that preceeded it.
So, let's recap: the original algorithm is beautiful and simple, but
it is NOT the actual Sieve of Eratosthenes, NOT because it uses
modulus, but because fundamentally, at the highest level, it is a
different algorithm. At the risk of beating a dead horse, let's see
why it's not the real sieve.
What makes the sieve an efficient algorithm are the details of *what*
gets "crossed off", *when*, and *how many times*. Suppose that we are
finding the first 100 primes (i.e., 2 through 541), and have just
discovered that 17 is prime. We will begin crossing off at 289 (i.e.,
17 * 17) and cross off the multiples 289, 306, 323, ... , 510, 527,
making fifteen crossings off in total. Notice that we cross off 306
(17 * 18), even though it is a multiple of both 2 and 3 and has thus
already been crossed off twice. (The starting point of 17 * 17 is a
pleasing, but actually *minor*, optimization for the *genuine* sieve.)
The algorithm is efficient because each composite number, c, gets
crossed off f times, where f is the number of unique factors of c
less than sqrt(c). The average value for f increases slowly, being
less than 3 for the first 10^12 composites, and less than 4 for the
first 10^34.
None of the algorithms we've discussed correspond to the above
algorithm. It is not merely that they don't perform "optimizations",
such as starting at the square of the prime, or that some of then use
a divisibility check rather than using a simple increment. Rather, at
a fundamental level, they all work differently than the real sieve.
Following our earlier example, after finding that 17 is prime, the
"phony" algorithm will check to see if 19 is divisible by 17 (in the
case of Yitzchak's algorithm, divisibility is checked by comparing
against 17*2), followed by 23, 29, 31, ... , 523, 527, checking a
total of ninety-nine numbers for divisibility by 17. In fact, even if
it did (somehow) begin at 289, it would examine all forty-five
numbers that are not multiples of the primes prior to 17 (289, 293,
307, ... , 523, 527). It is also worth realizing that the set of
numbers examined by the phony algorithm is not a superset of the ones
examined by the real sieve (as we saw, the real algorithm crossed off
306, which the phony algorithm skips). In general, the speed of the
phony algorithm depends on the number of primes that are *not*
factors of each composite, whereas the speed of the real algorithm
depends on the number of (unique) primes that *are*.
Hopefully at this point I've piqued your interest. Maybe you're
wondering what our original sieve algorithm is, if it isn't the real
sieve and the mod issue is just a red herring...? Maybe you're
wondering what the the real sieve looks like, coded in Haskell. Or
maybe you're still not convinced about any of it. If so, I invite
you to read a draft of my JFP paper, available at:
http://www.cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf
Best Regards,
Melissa.
More information about the Haskell-Cafe
mailing list