DEV Community

Igor Irianto
Igor Irianto

Posted on • Updated on • Originally published at irian.to

Start using memoization to reduce computing time in Javascript

One classic CS question is to create Fibonacci sequence. One of the solutions is a recursive function and it looks something like this:

function fib(n) {
  if (n === 0 || n === 1)
    return n;
  else
    return fib(n - 1) + fib(n - 2);
}
Enter fullscreen mode Exit fullscreen mode

A major problem with recursive fibonacci function above is that it is an expensive function. It calls itself too many times. Calling fib(40) took about 30 seconds on my poor 2015 Macbook air (it calls itself 102,334,155 times), fib(45) almost 5 minutes (calls itself 1,134,903,170 times - a billion time).

Good luck calling fib(100).

tearful cry

Is there anything we can do to shorten an expensive function like this?

Enter memoization

Memoization (rhymes with memorization) is a technique in CS to save previous result into a cache so when the function is called again with same argument, it would just return value from the cache and execute the function again. It is useful for expensive functions like fibonacci.

How do we use memoization on fibonacci?

We can use:

const fib = (function() {
  const cache = {};

  function f(n) {
    let value;

    if (n in cache) {
      value = cache[n];
    } else {
      if (n === 0 || n === 1)
        value = n;
      else
        value = f(n - 1) + f(n - 2);

        cache[n] = value;
    }

    return value;
  }

  return f;
})();
Enter fullscreen mode Exit fullscreen mode

(Source: here. All credit for above goes to author).

Try the function above and run fib(40), fib(50), and even fib(100). You'll feel the difference.

How does memoization works?

It stores values on JS object (const cache = {};) so if the same value is called again, it will fetch the value from cache instead of executing the function.

Let's say we want to call fib(5). When fib(5) is called the first time, since cache is empty and it could not find 5 in cache (if (n in cache) is falsy), it executes fibonacci logic (value = f(n - 1) + f(n - 2);) and then saves the result to cache (cache[n] = value;). Now we have a cache for n = 5 - something like this: {5: 5} (btw, value of fib(5) is 5).

The next time we call fib(5) again, it finds ({5: 5}) in cache. Instead of executing fib(5) again, it simply returns the value from cache lookup value = cache[n]; ... return value;. Since our fibonacci is recursive, when we call for fib(5), it automatically fills up the cache with values up to 5. Calling fib(5) creates cache for fib(4), fib(3), etc.

Another example is, say we have just called fib(49) and we want to call fib(50) next. Before we call fib(50), inside our cache, we would have cache values like this:

{
  0: 0,
  1: 1,
  2: 1,
  3: 2,
  ...
  48: 4807526976,
  49: 7778742049
}
Enter fullscreen mode Exit fullscreen mode

We already have values from 0 to 49! All we need to do is to call value = f(n - 1) + f(n - 2); - aka fib(49) + fib(48), which we already have stored in cache! This is how memoized fib(50) returns the result almost instantaneously compared to its non-memoized version.

Sweet! I am going to memoize every function in sight!

Unfortunately, not everything is memoizable. We can only memoize pure functions.

To be a pure function, it must:

  1. Have return value
  2. Does not depend on arguments other than its own argument
  3. Does not mutate values outside of its scope

Pure function is out of this article's scope, but check this short article on pure function.

Other notes

Memoization is awesome. But let's not overuse it. Some things to consider when deciding when to use memoization:

  1. Not all functions are memoizable. Only pure functions are.
  2. Memoizations have high overhead. Remember, we have to create a cache to store many possible arguments for every memoized function.
  3. Memoization is best used on expensive function. Regex calls and recursions are some of them that came into my mind.

That's nice. But we probably would never use Fibonacci in real life. Is there an example of real life use of memoization?

Yup. VueJS utilizes memoization. cached(fn) is a memoization wrapper.

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
Enter fullscreen mode Exit fullscreen mode

And it is being used several times:

const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

export const capitalize = cached((str: string): string => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})

const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})
Enter fullscreen mode Exit fullscreen mode

You can find these function here. (Vue 2.5.0 at the moment of this writing. It might change in the future but you could always go back to previous version).

Happy hacking!

Resources

More readings on memoziation:

On pure function:

Top comments (5)

Collapse
 
thekashey profile image
Anton Korzunov
function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
Enter fullscreen mode Exit fullscreen mode

This is non "memoization" - this is a memory leak. It's actually a really big problem

Collapse
 
iggredible profile image
Igor Irianto

Hey Anton, thanks for stopping by!
I looked at your article - might have missed it, but didn't catch the memory leak part. Would you mind elaborating how is this a case of memory leak?

Collapse
 
thekashey profile image
Anton Korzunov

That was in the part about "One Result" - it's hard to remember more that one (last) result. Let's double check your examples:

fibonacci

function fib(n) {
  if (n === 0 || n === 1)
    return n;
  else
    return fib(n - 1) + fib(n - 2);
}
  • How much cache size need - n
  • How much size it would take - almost nothing
  • Do we need persistent cache? - Well yes, but even non persistent cache would be ok.
function fib(n, cache) {
+  if(!cache) cache = {};
  if (n === 0 || n === 1)
    return n;
  else
+    return cache[n] = (fib(n - 1, cache) + fib(n - 2, cache));
 }

Now every fibonacci run would have it's own unique cache. It would be not so far (faaar not so fast), but a bit more safe.

PS: fibonacci is not the great example for this, and the "safe" could be more sound in the real solutions.

cached

So - first of all caching is not memoization :) Anyway

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
  • What would happen a day after? How many strings this cache would hold?
  • Keep in mind - v8 has an everlasting bug (actually a feature), when a whole string would be "protected" from garbage collecting if some substring is used - bugs.chromium.org/p/v8/issues/deta...
  • So - any "cache" might just ruin your server or SPA after just "some time".

It's still easy to fix the problem (just stringify strings) but in this case you will run into out-of-memory in any way, as long as there is no cache invalidation, eviction or expiration defined.

I hope you remember - cache invalidation is one of 3 most complex problems in the universe, so... still not a big problem - dozens of LRU caches could be found at npm, even I wrote at least 3 😅.

But the problem is - you have to think what exactly you need, why, how would it help, how to control and monitor that new thing...

Have a good day :)

Collapse
 
nicholascloud profile image
Nicholas Cloud

Great article -- for those who use lodash, it has a memoize utility function built in, which is quite handy: lodash.com/docs/4.17.15#memoize

Collapse
 
iggredible profile image
Igor Irianto

Good insight! I don't use lodash that often but it's nice knowing it's available when I need something battle-tested.