Very fast recursions with memoization

Languages with hash-tables can support an extremely useful form of optimization called memoization. Memoized functions reduce the number or frequency of function calls by caching the results of previous calls, and the best way to store these results is in a hash-table. This can very dramatically speed up a program that must make very many calls to the same function, and the efficiency of the memoized function increases as the cache grows.

;; lisp again....
(defvar *facttable* (make-hash-table :test 'equal))
(setf (gethash 3 *facttable*) 6)
(defun factl (n)
"Memoized factorial function"
 (cond ((= n 0) 1)
        ((equal (gethash n *facttable*) nil) 
          (let* ((x (* n (factl (- n 1))))) 
           (setf (gethash n *facttable*) x) x))
        (t (gethash n *facttable*))
  )
 )

You are sacrificing space for speed, since hash-table lookups are faster than some calculations. Recursive functions, especially deep recursions, benefit most from memoization. Here is an example run on a raspberry pi 2B, not a fast computer

(defun fib(n)
"Fibonacci sequence"
 (cond
     ((< n 2) 1)
     ( t (+ (fib (- n 1)) (fib (- n 2))))
  )
 )

(time (fib 35))
real time : 4.183 secs       ; Over 4 seconds!
run time  : 4.179 secs
gc count  : 68 times
consed    : 955544688 bytes
14930352                     ; 35^th Fibonacci number

You probably think that we are talking small potatoes here, how much faster can a memoized function be?

(defparameter *fibm* (make-hash-table :test 'equal))
(defun fibm(n) 
"Memoized Fibonacci sequence"
(cond
     ((< n 2) 1)
     ((equal (gethash n *fibm*) nil)
     (let* ((x (+ (fibm (- n 1)) (fibm (- n 2))))) 
           (setf (gethash n *fibm*) x) x)) ; add to table, return x
           (t (gethash n *fibm*))))

(time (fibm 200))
real time : 0.000 secs    ; so fast it does not register
run time  : 0.000 secs
gc count  : 1 times
consed    : 21552 bytes
453973694165307953197296969697410619233826

Now you see why we used lisp, fib(200) would be beyond the ability of other programming languages without the use of a multiple precision library. On the R-pi took (fibm 20000) less than 0.029 seconds.
I want to stress that we used the same algorithm in both functions

    \[f(n)=f(n-1)+f(n-2), \qquad f(1)=1, \quad f(2)=1\]

In the un-memoized version, a very large tree of redundant calculations is performed.
To compute f(6) we branch and find f(5) and f(4). To compute f(5) we must compute f(4) and f(3), so right away you see we are computing f(4) twice. In the memoized, there are no redundant calculations. Once f(m) is computed it is stored and looked up if needed rather than re-calculated.

We did everything by hand in the lisp example, let’s try this in julia, using the Memoize library package.

using Memoize
@memoize function ufib(n)
n < 2 ? 1 : ufib(n-1) + ufib(n-2)
end
@time ufib(35)
# 0.000006 seconds
# 14930352
# pretty fast, not lisp-fast but fast
@time ufib(200)
# 0.000167 seconds (991 allocations: 37.812 KiB)
# 3721511182311577122,  clearly wrong, the largest integer julia supports is
typemax(Int64)
9223372036854775807




What happened? Julia has no native support for integers larger than Int64, so it just checks out without any warning or indication that it is doing so. Using BigInt or big”100″ are any other arbitrary precision trick fails. Julia changes more rapidly than its documentation can keep up with, so perhaps this issue will be resolved in the near future.

Home 2.0
error: Content is protected !!