保持冷静,继续缓存
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
我和一位朋友就过早优化进行了一次很棒的对话。
话题自然而然地延伸开来,我们开始讨论缓存和记忆化。
我们每个人对这件事的看法都截然不同,但我们都认同的一点是,表现非常重要。
他问我能否用通俗易懂的方式解释我的想法,正如巴尼·斯廷森常说的那样,挑战接受!
那么在开始之前,让我们先来谈谈什么是记忆化以及我们为什么需要它。
什么是记忆化?
记忆化是一种优化技术,主要用于避免对相同的输出结果重复计算。
简而言之,这意味着我们的软件运行速度会更快。
我们为什么要使用记忆化?
我们应该使用记忆化来获得更好的性能和更快的结果。
例如,如果我们使用任何客户端 JavaScript 代码,就不太可能阻塞主线程并导致 UI 卡顿,而没有人喜欢这种情况 ¯\_ (ツ)_ /¯。
别说了!让我看看代码!
你说得对;我知道在继续阅读之前,我想先看到一些情节发展。
假设我们有一个简单的函数“add”;add 函数接受两个数字并返回它们相加的值;
const add = (a, b) => {
return a + b;
};
在这个函数中,每次调用时都会重新计算a+b
。 这并非一项“耗时”的计算。因此,我们不太可能对此类操作使用记忆化,但如果需要的话,也可以这样做。
const cachedAdd = memoizer(add);
cachedAdd(2,3); // 5 Not Cached
cachedAdd(2,3); // 5 Cached
cachedAdd(2,3); // 5 Cached
这一切都很好,但是“memoizer”到底是如何工作的呢?
我们来看看能否创建一个简单的通用“记忆器”高阶函数,以便我们可以重复使用。
/**
* Cache function results for given params
*
* @param {function} func
* @returns {function(): (*)}
*/
function memoizer(func) {
const cache = {};
return function() {
const key = JSON.stringify(arguments);
if (cache[key] !== undefined) {
return cache[key];
}
const result = func(...arguments);
cache[key] = result;
return result;
};
}
编写这个函数的方法有很多,但我们一步一步地来看这个实现。
“memoizer”接收一个函数作为参数,它使用参数对象,并将其字符串化以创建键。
得到键后,该函数会检查缓存对象中是否存在该键;如果存在,则返回缓存结果,操作完成。
如果不存在,它会计算该值,将其保存到缓存中,然后返回。
记忆化以时间换取空间。
我知道你在想什么,“我不确定这是否值得这么麻烦。”
把钱给我看看。
我们来看一些运行结果。
为了展示以下内容,我将使用著名的斐波那契数列函数。
斐波那契数列是这样的数列:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
下一个数字是它前面两个数字相加得到的;
我们可以这样实现这个函数:
const fibonacci = n => {
if (n <= 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const getFibonacci = (limit = 1) => {
const arr = [];
for (let i = 0; i <= limit; i++) {
arr.push(fibonacci(i));
}
return arr;
};
我们可以这样调用该函数:
getFibonacci(30); // will result [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...]
让我们在限制值为 30 时运行一次基准测试:
console.time("fibonacci");
for (let i = 0; i <= 100; i++) {
getCachedFibonacci(30);
}
console.timeEnd("fibonacci");
第一次运行这段代码,耗时 193.097 毫秒;
问题在于,如果运行 100 次,性能不会提升,反而可能更差。
例如,这段代码运行 100 次总共耗时 18357.116 毫秒,这简直糟糕透了。
我们看看能不能做得更好?
我们将使用之前编写的记忆化函数来创建一个新的缓存斐波那契函数:
const cachedFibonacci = memoizer(fibonacci);
const getCachedFibonacci = (limit = 1) => {
const arr = [];
for (let i = 0; i <= limit; i++) {
arr.push(cachedFibonacci(i));
}
return arr;
};
console.time("cachedFibonacci");
for (let i = 0; i <= 100; i++) {
getCachedFibonacci(30);
}
console.timeEnd("cachedFibonacci");
这一次,我们将得到不同的结果。
第一次运行,结果与之前相同,耗时约 193.509 毫秒,但从第二次及以后,该函数的平均耗时仅为 0.027 毫秒;
100 次迭代的总耗时为 199.988 毫秒。
👑每次迭代的速度都提高了 7000 倍。
我知道你在想什么;并非所有问题都符合斐波那契数列;
我必须强调,记忆化并非万能灵药,也并非适用于所有场景。
但另一方面,如果使用得当,它确实是提升应用程序性能的强大工具。
我应该创建自己的记忆化函数吗?
当然,你可以自己实现,但如果你想使用开源、经过充分测试、文档齐全的记忆化函数,以下是一个简短的列表:
如果您对此事有任何疑问或想法,我很乐意听听。同时,请保持冷静👑 Cache On。
文章来源:https://dev.to/polakshahar/keep-calm-and-cache-on-48d2