Hi. I'm new to Racket and I'm learning functional programming. I was reading about lazy evaluation and memoize and I was testing it on this program.
#lang lazy
;; #lang racket/base
;; (require memo)
;; (define/memoize (efib n)
(define (efib n)
(if (<= n 1)
(list-ref (list 2 8) n)
(+ (* (efib (- n 1)) 4)
(efib (- n 2)))))
(for ([i 10000])
(displayln (efib i)))
This might be a very noob question but when I think about it, it seems like memo should be faster because it's caching all the previous values that were returned. On the other hand, the actual results make me think I've misunderstood how lazy evaluation works. Of course, using neither does take noticeably long.
Using racket/base
and (require memo)
: 4997268μs
Using lazy
and no memo: 299663μs
Using both lazy
and memo: 472486μs
Lazy by itself is the fastest. Combining it with memo is slower. Using only memo is a little slower than that. What is going on here? I would also appreciate some links because I couldn't find any resources that properly answered my question. Also would this be different for much larger projects?
Thanks.
Does the #lang lazy
variant do what you want? Does it print anything at all to the screen?
Here are my thoughts:
- If you print a lot to the screen, IO will become the bottleneck. If you want to contrast the performance between many programs, try to print only a few values. E.g. print the sum
(displayln
(apply +
(for/list ([i 30])
(efib i))))
- With that (30 iterations),
#lang lazy
takes 1.50s. memo
takes 0.13s.
The issue with your earlier #lang lazy
program is that the whole (for ...)
is delayed, so essentially nothing is computed.
- From a quick glance of the
memo
documentation (it’s a user package, so I’m not familiar with it), it supports a lot of features that you might not need. If you write your own memoizer, it could even be faster.
#lang racket/base
(define (memoize f)
(define mem (make-hash))
(λ args
(hash-ref! mem args (λ () (apply f args)))))
(define-syntax-rule (define/memoize (f args ...) body ...)
(define f (memoize (λ (args ...) body ...))))
(define/memoize (efib n)
(if (<= n 1)
(list-ref (list 2 8) n)
(+ (* (efib (- n 1)) 4)
(efib (- n 2)))))
(apply + (for/list ([i 30]) (efib i)))
takes 0.03s.
2 Likes
What if you use Racket time
and look at all three timing values, including the one for GC?
I ask because my first hunch would be that memoization means using a hash-table behind the scenes, with (IIUC) as many as 10,000 entries. The allocation and lookup times could become non-trivial. There could even be enough memory pressure that it causes more-frequent major garbage collections?
Caching can be tricky. I think someone said it's one of the two hardest problems in computing, along with (b) naming things and 3. off-by-one-errors?
EDIT: I wrote that while @sorawee was writing, and posted before noticing theirs -- which is better thought out.
1 Like
@Greg Hendershott
What is the off-by-one error?
That I said there would be 2 things but listed 3.
One source for related silliness seems to be: TwoHardThings
1 Like
So I changed stuff around and here's my updated code. I'm printing the sum here (it ends up being a really large number).
#lang racket/base
;;#lang lazy
;;(require memo)
(define (memoize f)
(define mem (make-hash))
(λ args
(hash-ref! mem args (λ () (apply f args)))))
(define-syntax-rule (define/memoize (f args ...) body ...)
(define f (memoize (λ (args ...) body ...))))
(define/memoize (efib n)
(if (<= n 1)
(list-ref (list 2 8) n)
(+ (* (efib (- n 1)) 4)
(efib (- n 2)))))
(displayln
(apply +
(for/list ([i 10000])
(efib i))))
This time, using just lazy or using lazy with custom memoize takes a long time.
Using (require memo): 632554μs
Using (require memo) and lazy: 666103μs
Using custom memoize: 281063μs
Using just a custom memoizer is the fastest and interestingly, it is very close to my original result for lazy without memo (299663μs). I should read more about how lazy actually works. Thanks for the help!
1 Like