Ask HN: Why is this Racket code so fast?
I was completing some of the Project Euler problems in Racket and usually see the Fibonacci sequence take a couple seconds to generate. While showing my wife I tried a number greater than 4,000,000 and it was instant, so I tried again and again and again, up to a 1,000,000 digit number that took 70 seconds. 10^10000 is still around 100ms.
Can anyone explain how this code is so quick? Running it on a Mac M1 Pro (2021).
https://github.com/Mithaecus/cp-project-euler/blob/main/answers/0002.rkt
https://github.com/Mithaecus/cp-project-euler/blob/main/answers/_solution.rkt
27 comments
[ 0.25 ms ] story [ 76.9 ms ] threadhttps://cisco.github.io/ChezScheme/csug9.5/use.html
There is no such thing as a “compiled language”: some languages are implemented as compilers and some as interpreters, but nothing prevents writing a compiler for most languages.
Edit: I see someone else describes how it compiles JIT so I see your point. Slightly less magic than I thought :)
You can compile it first with:
And then run it:Also, there is a shortcut to generate only the even Fibonacci numbers. Should reduce your execution time by a bit. Since the even Fibonacci numbers are every 3rd entry, this means you can cut out 2/3rds of the generated values and the conditional checking if a value is even or not.
For instance, in Common Lisp you might do something like this (I'm too rusty on Racket to write it up properly) as a kind of poor man's generator/iterator/stream:
(untested, should work) and then you can repeatedly use apply or funcall (so not as clean as Scheme or Racket as lisp-1s) to generate the sequence) racket/stream just gives you all this without the manual elements that I'm doing here. There are libraries in CL that give you all of this, too, but I figured an illustration of what's happening behind the scenes would be useful.The changed recurrence relation is:
This skips the generation of odd Fibonacci numbers and removes the need for the filtering step (if even then add to accumulator), but it adds in the multiplication which seems to reduce some of the potential performance gains. Once you get to the point that you're dealing with big numbers, addition becomes very expensive and, depending on how multiplication is implemented, you ought to get a larger performance boost as you get to very large numbers.With default settings DrRacket will add extra debug code to the program, which will slow it down.
I often suppose as a rule of thumb that a C program can perform ~10 million for-loop additions per second (assuming the accumulator fits in a register). So to do 500,000 bignum additions/second, each bignum addition would only require on average the time of 20 normal additions+iterations.
Also note that the functions provided are all tail recursive. Racket performs tail-call optimizations which would make these similarly performant to a for loop.