我非常喜欢解释的一种编程概念是有一个令人生畏的名字,但一旦你学会了它,实际上是一个相当简单的概念。这就是我对记忆化的感觉。
不是记忆-记忆化。让我们先看看维基百科是如何描述记忆化的[1]。
在计算机领域,记忆化或备忘化是一种优化技术,主要用于加快计算机程序的速度,通过存储昂贵的函数调用的结果,并在相同的输入再次出现时返回缓存的结果。
我通常觉得维基百科的描述很密集,而且乍一看有点没有帮助,但实际上我觉得这个描述超级有帮助!
基本思路
假设你有一个函数,它接受一些输入,做一些计算上很昂贵的事情,然后返回一个输出。你调用这个函数一次,这很昂贵。如果你用同样的输入再次调用这个函数,为什么要再次进行昂贵的计算?就用一个缓存的结果来代替吧
一个使用JavaScript的简单例子
让我们创建一个超级低效的函数,在JavaScript中对一个数字进行平方运算。我们将其称为inefficientSquare 。
function inefficientSquare(num) {
let result = 0;
for (let i = 0; i < num; i++) {
for (let j = 0; j < num; j++) {
result++;
}
}
return result;
}
这个函数会对一个数字进行平方运算,但是效率非常低。让我们用这个函数对数字10,000进行平方运算,并使用节点性能计时API计算所需时间。
const { performance } = require('perf_hooks');
const start = performance.now();
inefficientSquare(10000);
const finish = performance.now();
console.log(finish - start);
// 115.8375
因此,我花了大约115毫秒来运行,但根据你的环境,可能会更长或更短。重要的是,这是一个相当昂贵的运行函数。
把它记下来!
我们知道,每次我们对一个数字进行平方时,都会得到相同的结果。因此,它是一个很好的记忆化的候选者。如果我们计算了一次数字的平方,为什么还要再做一次昂贵的计算呢?让我们把计算结果保存起来,如果我们需要它的话。
让我们创建一个memoizedSquare 函数,返回一个函数。这一次,我们将跟踪我们已经看到的结果。
function inefficientSquare(num) {
let result = 0;
for (let i = 0; i < num; i++) {
for (let j = 0; j < num; j++) {
result++;
}
}
return result;
}
function memoizedSquare() {
const cache = {};
return function (num) {
if (cache[num] === undefined) {
cache[num] = inefficientSquare(num);
}
return cache[num];
};
}
现在,我们可以看到,当我们用相同的值多次调用memoized函数时,我们的性能是如何提高的(显著地)。
const squareFn = memoizedSquare();
// Call the first time: ineffience
const start = performance.now();
squareFn(10000);
const finish = performance.now();
console.log(finish - start);
// 117.0957
// Call the second time: speedy!
const start2 = performance.now();
squareFn(10000);
const finish2 = performance.now();
console.log(finish2 - start2);
// 0.005
而这是有道理的--第二次我们用参数10000 调用squareFn ,我们只是从我们的cache 对象中检索值,而不是再次进行计算。
这就是记忆化!
制作一个通用记忆器
让我们做一个一般的备忘器,它也可以用于其他函数。
function memoizer(fn) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (cache[key] === undefined) {
cache[key] = fn(...args);
}
return cache[key];
};
}
我们可以通过将其应用于我们的inefficientSquare 函数来证明这个通用记忆器的工作。
function inefficientSquare(num) {
let result = 0;
for (let i = 0; i < num; i++) {
for (let j = 0; j < num; j++) {
result++;
}
}
return result;
}
const squareFn = memoizer(inefficientSquare);
const start = performance.now();
squareFn(10000);
const finish = performance.now();
console.log(finish - start);
// 121.0662
const start2 = performance.now();
squareFn(10000);
const finish2 = performance.now();
console.log(finish2 - start2);
// 0.010
很好!我们现在有了一个memoizer ,该函数将另一个函数作为参数,并将其结果备忘化。
这个泛化记忆器的局限性
这个通用记忆器的最大限制是,我们使用JSON.stringify 把我们的参数变成一个对象键。JSON.stringify 是相当有限的:它只能对某些原始和对象类型进行字符串化,所以如果我们有不能被字符串化的参数,这个简单的方法是不够的
如果你在生产中需要一个备忘器,我建议使用像fast-memoize这样经过严格审查的库,它对那些不容易被字符串化的数据类型使用其他更复杂的备忘策略。
关于函数纯洁性的说明
要使用记忆化,你的函数应该是纯洁的如果你不熟悉纯函数的概念,它们基本上有两个限制:纯函数应该在给定相同的输入时有相同的输出,而且纯函数不应该有副作用。
总结
Memoization可能听起来是一个令人生畏的概念,但实际上它只是存储了一个昂贵的计算结果,以便在必要时使用。