What is function cache

In order to understand this concept, suppose you are developing a weather app. At first, you didn't know how to do it. There happened to be a getChanceOfRain in the npm package that could be called:

import { getChangeOfRain } from 'magic-weather-calculator';

function showWeatherReport() {
  let result = getChangeOfRain();    // 这里调用
  console.log('The change of rain tomorrow is: ', result);
}

It's just that there will be a problem. No matter what you do, just calling this method will consume 100 milliseconds. Therefore, if a user clicks the "Show Weather" button frantically, every time they click the app, there will be no response for a period of time.

showWeatherReport(); // 触发计算
showWeatherReport(); // 触发计算
showWeatherReport(); // 触发计算

This is irrational. In actual development, if you already know the result, then you will not calculate the result one at a time. Reusing the last result is the best choice. This is the function cache. function cache is also the settlement result of the cache function, so there is no need to call the function one at a time.

In the following example, we will call memoizedGetChangeOfRain() . In this method, we will check whether there is a result, instead of calling the getChangeOfRain() method every time:

import { getChangeOfRain } from 'magic-weather-calculator';

let isCalculated = false;
let lastResult;

// 添加这个方法
function momoizedGetChangeOfRain() {
  if (isCalculated) {
    // 不需要在计算一次
    return lastResult;
  }
  
  // 第一次运行时计算
  let result = getChangeOfRain();
  
  lastResult = result;
  isCalculated = true;
  
  return result;
}

function showWeatherReport() {
  let result = momoizedGetChangeOfRain();
  console.log('The chance of rain tomottow is:', result);
}

showWeatherReport() multiple times will only do the calculation the first time, and all others will return the result of the first calculation.

showWeatherReport(); // (!) 计算
showWeatherReport(); // 直接返回结果
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result

This is the function cache. When we say that a function is cached, we don't mean something is done in the javascript language. It is to avoid unnecessary calls when we know the result is unchanged.

Function cache and parameters

General function cache mode:

  1. Check if there is a result
  2. If yes, return this result
  3. If not, calculate the result and save it for later return

However, certain situations need to be considered in actual development. For example: getChangeOfRain() method receives a city parameter:

function showWeatherReport(city) {
  let result = getChanceOfRain(city); // Pass the city
  console.log("The chance of rain tomorrow is:", result);
}

If you simply cache this function as before, you will have a bug:

showWeatherReport('Tokyo');  // (!) Triggers the calculation
showWeatherReport('London'); // Uses the calculated answer

Did you find it? The weather in Tokyo and London is very different, so we cannot directly use the previous calculation results. means that when we use the function cache, we must consider the parameter .

Method 1: Save the last result

The easiest way is to cache the result and the parameters that the result depends on. That's it:

import { getChanceOfRain } from 'magic-weather-calculator';

let lastCity;
let lastResult;

function memoizedGetChanceOfRain(city) {
  if (city === lastCity) { // 检查城市!
    // 城市相同返回上次的结果
    return lastResult;
  }
  
  // 第一次计算,或者参数变了则执行计算
  let result = getChanceOfRain(city);
  
  // 保留参数和结果.
  lastCity = city;
  lastResult = result;
  return result;
}

function showWeatherReport(city) {
  // 参数传递给缓存的参数
  let result = memoizedGetChanceOfRain(city);
  console.log("The chance of rain tomorrow is:", result);
}

Note that this example is slightly different from the first example. It is no longer to directly return the last calculation result, but to compare city === lastCity . If there is a change in the midway city, the result must be recalculated.

showWeatherReport('Tokyo');  // (!) 计算
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('London'); // (!) 重新计算
showWeatherReport('London'); // 使用缓存结果

Although this fixes the bug in the first example, it is not always the best solution. If the parameters are different each time the call is made, the above solution is useless.

showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // (!) 执行计算

Whenever you use function caching, check if it really helps!

Method 2: Keep multiple results

Another thing we can do is keep multiple results. Although we can define a variable for each parameter, such as: lastTokyoResult, lastLondonResult and so on. Using Map seems to be a better way.

let resultsPerCity = new Map();

function memoizedGetChangeOfRain(city) {
  if (resultsPerCity.has(city)) {
    // 返回已经存在的结果
    return resultsPerCity.get(city);
  }
  
  // 第一次获取城市数据
  let result = getChangeOfRain(city);
  
  // 保留整个城市的数据
  resultsPerCity.set(city, result);
  
  return result;
}

function showWeatherReport(city) {
  let result = memoizedGetChangeOfRain(city);
  console.log('The chance of rain tomorrow is:', result);
}

The whole method and the use case that suits us. Because it will only be calculated when the city data is first obtained. When using the same city to get data, it will return the data that has been saved in the Map.

showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('London'); // 使用缓存结果
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('Paris');  // (!) 执行计算

However, this method is not without its disadvantages. Especially when our city parameters continue to increase, the data we save in the Map will continue to increase.

Therefore, this method consumes memory uncontrollably while gaining performance. In the worst case, it will cause the browser tab to crash.

Other methods

There are many other ways between "save only the last result" and "save all results". For example, save the last N results that are used recently, which is also known as LRU, or "least recently used" cache. These are all ways to add other logic outside of the Map. You can also delete delete data after a certain time in the past, just as the browser cache expires after them will delete the same. If the parameter is an object (not the string shown in the example above), we can use WeakMap instead of Map . Modern browsers support it. The advantage of using WeakMap is that the key-value pairs will be deleted when the object as the key does not exist. Function caching is a very flexible technology, you can use different strategies according to the specific situation.

Function cache and function purity

We know that function caching is not always safe.

Assume that the getChangeOfRain() method does not accept a city as a parameter, but directly accepts user input:

function getChangeOfRain() {
  // 显示输入框
  let city = prompt('Where do you live?');
  
  // 其他代码
}

// 我们的代码
function showWeatherReport() {
  let result = getChangeOfRain();
  console.log('The chance of rain tomorrow is:', result);
}

An input box will appear every time the showWeatherReport() We can enter different cities and see different results in the console. But if the getChanceOfRain() method is cached, we will only see an input box! It is not possible to enter a different city.

So the function cache is safe only if that function is a pure function. That is to say: only reads parameters and does not interact with the outside . For a pure function, it doesn't matter if you call it once or use the previous cached result.

This is why in a complex algorithm, the code that only calculates is separated from the code of what to do. Purely computational methods can be safely cached to avoid multiple calls. Those methods cannot do the same.

// 如果这个方法值做计算的话,那么可以被称为纯函数
// 对它使用函数缓存是安全的。
function getChanceOfRain(city) {
  // ...计算代码...
}

// 这个方法要显示输入框给用户,所以不是纯函数
function showWeatherReport() {
  // 这里显示输入框
  let city = prompt('Where do you live?');
  let result = getChanceOfRain(city);
  console.log("The chance of rain tomorrow is:", result);
}

Now you can safely cache getChanceOfRain() city it accepts 060c5fbbc17eb6 as a parameter, instead of popping up an input box. In other words, it is a pure function.

You will still see the input box every time you call showWeatherReport() But the corresponding calculation after the result is obtained can be avoided.

Reuse function cache

If you want to cache many methods, writing a cache for each method is a bit repetitive. This can be automated, and it can be done in one method.

We use the first example to demonstrate:

let isCalculated = false;
let lastResult;

function memoizedGetChanceOfRain() {
  if (isCalculated) {
    return lastResult;
  }
  
  let result = getChanceOfRain();
  lastResult = result;
  isCalculated = true;
  
  return result;
}

Then we put these steps in a method memoize

function memoize() {
  let isCalculated = false;
  let lastResult;
  
  function memoizedGetChanceOfRain() {
    if (isCalculated) {
      return lastResult;
    }
    
    let result = getChanceOfRain();
    lastResult = result;
    isCalculated = true;
    return result;
  }
}

We want to make this method more useful, not just to calculate the probability of rain. So we need to add a method parameter called fn .

function memoize(fn) { // 声明fn参数
  let isCalculated = false;
  let lastResult;
  
  function memoizedGetChanceOfRain() {
    if (isCalculated) {
      return lastResult;
    }
    let result = fn(); // 调用传入的方法参数
    lastResult = result;
    isCalculated = true;
    return result;
  }
}

Finally memoizedGetChanceOfRain() rename memoizedFn and return:

function memoize(fn) {
  let isCalculated = false;
  let lastResult;
  
  return function memoizedFn() {
    if (isCalculated) {
      return lastResult;
    }
    
    let result = fn();
    lastResult = result;
    isCalculated = true;
    return result;
  }
}

We got a caching function that can be reused.

Now our first example can be changed to:

import { getChanceOfRain } from 'magic-weather-calculator';

let memoizedGetChanceOfRain = memoize(getChanceOfRain);

function showWeatherReport() {
  let result = memoizedGetChanceOfRain();
  console.log('The chance of rain tomorrow is:', result);
}

isCalculated and lastResult still there, but in the memoize method. In other words, they are part of the closure. We can use the memoize method anywhere, each time it is cached independently.

import { getChanceOfRain, getNextEarthquake, getCosmicRaysProbability } from 'magic-weather-calculator';

let momoizedGetChanceOfRain = memoize(getChanceOfRain);
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);

memoize here is to generate a cached version of the method. This way we don't have to write so many repetitive codes every time.

review

Now we can quickly review it. Function caching is a way to make your program run faster. If there is a piece of code that only performs calculations (pure functions), then this piece of code can avoid unnecessary repeated calculations for the same result through the function cache.

We can cache the last N results, or all the results. These require you to make a choice based on the actual situation.

memoize is not difficult for you to implement the 060c5fbbc18035 method yourself, and colleagues also have some packages to help you do this. There is an implementation of Lodash

The core part is basically like this:

import { getChanceOfRain } from 'magic-weather-calculator';

function showWeatherReport() {
  let result = getChanceOfRain();
  console.log('The chance of rain tomorrow is:', result);
}

Will become:

import { getChanceOfRain } from 'magic-weather-calculator';

let isCalculated = false;
let lastResult;

function memoizedGetChanceOfRain() {
  if (isCalculated) {
    return lastResult;
  }
  let result = getChanceOfRain();
  lastResult = result;
  isCalculated = true;
  return result;
}

function showWeatherReport() {
  let result = memoizedGetChanceOfRain();
  console.log("The chance of rain tomorrow is:", result);
}

Reasonable use of function cache will bring actual performance improvement. Of course, be careful about the complexity and potential bugs that may be introduced.

Remarks

The original text is here: https://whatthefork.is/memoization


小红星闪啊闪
914 声望1.9k 粉丝

时不我待