-
Notifications
You must be signed in to change notification settings - Fork 745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Streams library "sieve" block producing primes is NOT a Sieve of Eratosthenes... #3412
Comments
I'm pretty sure Eratosthenes didn't have a computer, and the odds are he did have slaves to do the actual crossing out of numbers, so he was less interested in algorithmic efficiency than you are. But in any case, if I wanted a finite number of primes, I'd do it iteratively like everyone else, not with streams. The whole point of the demo is that you can get all the primes if you have lazy lists. It would defeat the (pedagogic) object of the exercise to use a more complicated algorithm that hides the beauty of this one. I do wish the streams library implemented lazy lists more efficiently. If you have any words of wisdom about that, it'd be great! |
I did some timing and the difference in performance between the current Stream implementation and my new one where the states are buried inside closures isn't all that much, where it takes something about 12 seconds to list a stream of 1000 numbers; this may be due to JavaScript forming closures being particularly slow, in particular in Google Chrome and Firefox and even twice as slow with Safari, which will make either technique slow as both depend on "thunk" closures... Using my primes algorithm, one will still be able to find the primes up to ten thousand in perhaps about this amount of time as for for when using non-memoizations as forming closures is likely the bottleneck. For the David Turner algorithm, your VM will time out finding primes to this range as it would take hundreds of seconds to complete... |
Ah, thanks! We'll look into the FOR thing. About rings, of course we have to wait until the procedure is called to run it; Snap! isn't a purely functional language, and the result of running the code depends on its environment. We could, I guess, compile the code in the ring to JS, but apart from a couple of experiments, we don't compile the code even when you run it; Snap! is an interpreter. As with any interpreter, we pay a huge performance price for that, but it's necessary to support "liveliness": the user can drag blocks into or out of the ring while it's running, or edit an argument slot. |
That being the case, other than fixing the imperative loops I don't think you can do much about the speed of using streams; once the list-of-n-stream and nth-item-of-stream blocks are fixed, at least when using a proper SoE your users will be able to calculate the primes functionally to ten thousand or a little more in the allowed run time per block. Other than using an imperative list-as-an-array according to the usual algorithms one might be able to push the SoE range a little higher until there are memory constraints... |
By the way, there's no limit on the runtime of a block or script. The fact that the browser time slices us against other tabs doesn't matter; we pick up where we left off the next time we run. |
I suspect that what you're seeing is the result of a memory leak, rather than a timeout problem. Google does seem to enjoy introducing bugs to Chrome that mess up Snap!, but afaik there isn't one that prevents long-running programs. As for the rest, c'mon, you don't have to teach me about asymptotic analysis of algorithms. If I were doing crypto or Bitcoin mining or something, I'd be really worried about runtime for thousands of primes (and I'd be using bignums, so it'd be even slower). But what I'm doing is teaching kids about lazy lists, and for my purposes the only reason for choosing primes as the stream to generate is to make the point that you can apply them to a serious-ish computation, not just the stream of multiples of three or other such trivial computations. It's the same reason there's that paragraph in the TeXBook with an embedded prime generator TeX macro. (And of course if I were doing crypto or Bitcoin mining I wouldn't be doing it in Snap!.) The example, of course, comes from SICP, the best computer science book ever written, so you have a large hill to climb to convince me that it isn't the best way to introduce streams to CS 1 students. :) |
Yes, that's true and I see your point.
Why on earth would you be using bignum's when this sieve would take eons to even sieve to the signed 32-bit number range of a little over two billion (2e9), even if it didn't run out of memory for the huge heap of closures of all the preceding prime number divisions from the filtering? Even about the fastest SoE in the world, Kim Walisch's multi-threaded "primesieve" written in C++, would take in the order of months on a very high end desktop computer to compute just the count of all the primes to about 2e19, the unsigned 64-bit integer number range. Therefore, he doesn't use bignum's but only unsigned 64 bit integers, which are native registers on most CPU's. Although the most advanced sieve algorithms such as his do use larger SIMD registers on modern CPU's, that code mostly provides a boost for the small end of the range that can take advantage of this technology. Just for point of reference, it takes an optimized build of the GHC Haskell version almost a minute to compute the 78,498 primes to a million on my fairly high end desktop computer, and about 40% of the time (and increasing with range) is spent doing garbage collection for the high use of the heap due to the stacked filter closures... In turn, I guess my main point is that you call it the Sieve of Eratosthenes, and I wouldn't have even reacted if you hadn't done that, also mentioning somewhere that you got it from SICP (implying that must make it true); that brings me to my next related point:
I'll grant you that SICP was a very good learning resource for its time and still holds up in many ways; however, that doesn't mean that the author's are never wrong, and in this particular case very wrong. Again, including this simple trial division prime number sieve wouldn't have been wrong if they had just said that was what it was and even better, taken the opportunity to point out its limitations and why its performance is so poor, but calling it a Sieve of Eratosthenes was very wrong as is now well known, especially after the publishing and peer review of Melissa E. O'Neill's article that I linked in the opening post, which carefully explains why it can't be called a Sieve of Eratosthenes, both based on its computational complexity and on comparing its algorithm as compared to a true incremental (functional) Sieve of Eratosthenes. It is regrettable that they made that statement because it has led to generations upon generations who drew much of their knowledge of computer science from SICP as their "holy book" believing that it is an SoE, just as you do.
I have never said I am trying to "climb that hill", just pointing out this single error in the book (and in your reference to the algorithm as Sieve of Eratosthenes)... I really don't have more to say until you've read at least the introduction of the O'Neill article, which it doesn't seem you have... |
could you perhaps take this discussion to the Snap! forum: https://forum.snap.berkeley.edu/t/streams-library-2-0-development-part-2/16664/214 |
I wrote much of Snap! ’s current Streams library, and I’m curious after @GordonBGood ‘s proposal for a recursive |
"New users are limited to one reply per thread"!!! And in trying to edit my post, I deleted it and can't seem to get it back!!!!
In thinking about this problem, I am wondering whether the imperative loops are so slow because they are doing constant conversion between imperative and linked-list versions of the list values used. So in testing I bypassed the use of the imperative loop blocks entirely by using functional recursive "loops"...
Sure, but as my needs were only for an infinite stream version, I haven't done much testing for a stream that might terminate with an empty stream. Thus, the following isn't completely tested:
Ah, sorry - I assumed anyone working on functional algorithms such as streams would also know Haskell; I'll only post Snap! images of blocks then as I don't care for LISP and exported blocks in What format works for posting blocks in this forum? I tried posting images, but the forum doesn't seem to allow that? |
Why I have been mentioned? |
somebody might have mistaken your Github handle with @brianharvey 's usual nick, sorry! |
In the help notes for this "sieve" block included in the demos for the Streams library, you say "It's called SIEVE because the algorithm it uses is the Sieve of
Eratosthenes: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
", yet when one reads the linked Wikipedia article (at least as updated over the last 15 years or so), one finds the following statement: "The widely known 1975 functional sieve code by David Turner[13] is often presented as an example of the sieve of Eratosthenes[7] but is actually a sub-optimal trial division sieve.[2]".
That David Turner sieve is exactly what you have implemented, as expressed in Haskell as follows:
or more as you have written it in Snap! (using Haskell's "filter" instead of the equivalent Snap! "keep"):
This has much worse performance than a real functional "incremental" Sieve of Eratosthenes (SoE) so as to be almost unusable for your Snap! version, taking over 10 seconds on a fairly high end desktop computer to find the primes just to a thousand; it is slow because rather than using SoE sieving by only incrementing the culled composites per base prime by addition of a constant span, it uses trial division (via the "mod" function) to test ALL remaining prime candidates for even division by all found primes - thus, it has an exponential computational complexity rather than the O(n log n (log log n)) computational complexity for the true incremental SoE.
Using the work from the Wikipedia article referenced article, one could write a true incremental SoE using a Priority Queue as O'Neill preferred but that would require the extra work of implementing the Priority Queue using either a binary tree structure or using Snap!'s List's in imperative mode (not functional linked-list mode). In the Epilogue of the article, reference is made to a "Richard Bird list-based version" of the SoE, which is much easier to implement. However, at that time, this sieve still hadn't been optimized to use "infinite tree folding" to reduce the complexity of the base primes composites merging to a computational complexity of about O(n log n) rather than O(n sqrt n) without the tree folding, which is a very significant difference as prime ranges get larger. A refined version of this sieve (sieves odds-only) in Haskell is as follows:
In contrast to the David Turner sieve, instead of progressively sieving composite numbers from the input list by filtering the entire remaining list, this tree folding algorithm rather builds a merged (and therefore ascending ordered) lazy sequence of all of the composite values based on the recursively determined "oddprimes" function (non-sharing) and then the final output sequence of prime numbers is all of the (in this case odd) numbers that aren't in the composites sequence. It is relatively easy to arrange the merged sequence of composites in ascending order because each of the sub lists of culls from each of the base primes is in order but also because each new sub sequence based on the next base prime will start at a new increased value from the last (starting at the square of the base prime). For this fully functional "incremental" algorithm, there is a cost in merging for all of the sub sequences, but the binary tree depth is limited: for instance, when finding the primes up to a million, the base primes sequence only goes up to the square root or a thousand, and there are only 167 odd primes up to a thousand, meaning that the binary tree only has a depth of about eight (two to the power of eight is 256) in the worst case (some merges won't need to traverse to the bottom of the tree)
Now, as all of these sequence states don't refer back to previous states, there is no need to memoize the states as in a "lazy list" or as in your "streams" library, and a simpler Co-Inductive Stream (CIS) can be used, as follows in Haskell:
The above Haskell code is written so as to better understand as this "CIS" implementation is written in non-lazy languages as it doesn't take advantage of Haskell's built-in laziness and bypasses the compiler "strictness analyzer" so may be less efficient that the lazy list version above in spite of not doing the extra work to do memoization of each sequence value...
The above has been translated to Snap! as per this attached file and can be tested with "listncis 168 primes" to show the list of the primes up to a thousand in about a second, and works in a little over ten seconds showing primes up to about ten thousand; it thus has more of a linear execution time with increasing ranges...
I recommend you fix your Streams demo to use an implementation of this algorithm, although as noted above, the algorithm doesn't really required the overhead of the Streams memoization...
The text was updated successfully, but these errors were encountered: