计算机领域的记忆化(英语:memoization)是一种提高计算机程序执行速度的优化技术。通过存储大计算量函数的返回值,当这个结果再次被需要时将其从缓存中提取,而不再计算来节省计算时间。
这是一种典型的用存储空间换取计算时间的方案。
本文将首先介绍一个简单的用记忆化优化的例子,然后会介绍记忆化在 JavaScript 框架/库中的实际应用。
阶乘
在数学中,正整数的阶乘是所有小于及等于该数的正整数的积。例如5的阶乘是120:
5!=5×4×3×2×1=120
用 JavaScript 编写一个原始的函数如下,它没有用到记忆化:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
如下是一个用了记忆化的例子:
const cache = [1, 1];
function factorial2(n) {
if (cache[n] == undefined) {
cache[n] = n * factorial2(n - 1);
}
return cache[n];
}
通过增加一个变量 cache 用于保存之前计算过的数值,以数组的索引映射到对应的阶乘数值。如此这样,后续调用此函数时如果指定的参数已经计算过则直接读取,无需再次计算。
你可能想知道这两个函数在性能上究竟有什么差异,可以运行如下的测试用例:
var start = performance.now();
for (let i = 0; i < 100000; i++) {
factorial(100)
}
console.log(`无记忆化:${performance.now() - start} ms`)
var start2 = performance.now();
for (let i = 0; i < 100000; i++) {
factorial2(100)
}
console.log(`记忆化:${performance.now() - start2} ms`)
在我的电脑上运行显示如下:
无记忆化:129.80000001192093 ms
记忆化:1.699999988079071 ms
可见记忆化之后能够大大改善程序性能。
记忆化和闭包
上面的 factorial2 函数实现了记忆化,新增的 cache 变量却出现在了全局作用域。此外它的记忆化不具有普遍适用性,无法用于其他需要记忆化的函数里。
此时,可以借助于 JavaScript 语言的闭包特性解决这两个问题。
/**
* @param {Function} fn - 计算数值的函数
* @param {Array} defaultValue - 缓存默认值
* @return {Function}
*/
function factorialMemo(fn, defaultValue) {
const cache = [...defaultValue];
return (n) => { // 最终返回一个可以接受参数的函数
if (cache[n] == undefined) { // 若该入参尚未有缓存
cache[n] = fn(n); // 计算数值并存入缓存
}
return cache[n];// 从缓存中读取
};
}
把 cache 变量放在了factorialMemo 函数中,它可以被视为一个“私有变量”,作用域局限于函数内部,不会污染全局变量。这个函数返回了另一个函数,返回的这个函数可以接收一个参数。
对于阶乘的例子,可以这样使用:
const factorial2 = factorialMemo((n) => {
return n * factorial2(n - 1)
}, [1, 1]);
对于整数连加,可以这样使用:
const add = factorialMemo((n) => {
return n + add(n - 1);
}, [1, 1]);
console.log(add(100)); // 5050
Lodash 源码中的 memoize
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function() {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache);
return memoized;
}
在 lodash 和 underscore 中的做法是每次调用 memoize 方法时创建一个 memoized 函数,把缓存的数据保存在此函数上,最后返回此函数。
参考这种做法,我们可以把 factorialMemo 函数改写为如下:
function factorialMemo(fn, defaultValue) {
const memoize = function(n) {
const cache = memoize.cache;
if (cache[n] === undefined) {
cache[n] = fn(n);
}
return cache[n];
};
memoize.cache = [...defaultValue];
return memoize;
}
使用 _.memoize 实现阶乘如下:
const factorial = _.memoize(function(n) {
return n === 1 ? 1 : factorial(n - 1) * n
});
console.log(factorial(5))
结语
记忆化是一种优化技术,可以避免一些不必要的重复计算,从而提高计算速度。