- 原文地址:How I wrote the world's fastest JavaScript memoization library
- 原文做者:Caio Gondim
- 译文出自:掘金翻译计划
- 译者:薛定谔的猫
- 校对者:GangsterHyj,sunui
在本文中,我将详细介绍如何实现 fast-memoize.js,它是世界上最快的 JavaScript 记忆化(memoization)实现,每秒能进行 50,000,000 次操做。
咱们会详细讨论实现的步骤和决策,而且给出代码实现和性能测试做为证实。javascript
fast-memoize.js 是开源项目,欢迎你们给我留言和建议。html
不久前,我尝试了 V8 中一些即将发布的特性,以斐波那契算法为基础作了一些基准测试实验。
实验之一就是比较斐波那契算法的记忆化版本和普通实现,结果代表记忆化版本有着巨大的性能优点。前端
意识到这一点,我又翻阅了不一样的记忆化库的实现,并比较了它们的性能(由于……呃,为何不呢?)。记忆化算法自己很是简单,然而我震惊地发现不一样实现之间性能差别巨大。java
这是什么缘由呢?react
在翻阅 lodash 和 underscore 的源码时,我发现默认状况下,它们只能记忆化接受一个参数的函数。因而我就很好奇,可否实现一个足够快而且能够接受多个参数的版本呢?(或许能够开发出 npm 包给全世界的开发者使用呢?)android
下文中,我将详细介绍实现它的步骤,以及实现过程当中所作的决策。ios
引自 Haskell 语言 wikigit
『记忆化是保存函数执行结果,而不是每次从新计算的一种技术。』github
换句话说,记忆化就是对于函数的缓存。 它只适用于肯定性算法,对于相同的输入老是生成相同的输出。算法
为了便于理解和测试,咱们把这个问题拆分红几个小问题。
我将这个算法分解为 3 个小问题:
如今咱们就要分别以不一样的方式实现这 3 个部分,测试它们的性能,选择其中最快的方式,最后将它们结合起来就是咱们最终的算法了。
这样作的目标就是让计算机为咱们解除重担!
如前文所述,缓存保存了以前的计算结果。
为了抽象实现细节,咱们须要建立一个相似于 Map 的接口:
经过(定义接口)这种方式,只要咱们实现了这个接口,就能够修改缓存内部的实现,而不影响外部使用。
每次执行记忆化函数,咱们须要作的就是:检查对应输入的输出是否已经被计算过。
所以最合理的数据结构是哈希表。它可以在 O(1) 时间复杂度检查某个值是否存在。 从底层看,一个 JavaScript 对象就是一个哈希表(或相似的结构),因此咱们能够将输入做为哈希表的 key,将输出做为它的 value。
// Keys 表明斐波那契函数的输入
// Values 表明函数执行结果
const cache = {
5: 5,
6: 8,
7: 13
}复制代码
为实现缓存,我分别尝试了:
如下是这些实现的性能测试。本地运行,请执行命令 npm run benchmark:cache
。不一样版本实现的源码能够在项目的 GitHub 页面找到。
在参数是非字面量时,这个版本会有问题,由于转化为字符串时并不惟一。
functionfoo(arg) { returnString(arg) }
foo({a: 1}) // => '[object Object]'
foo({b: 'lorem'}) // => '[object Object]'复制代码
这就是为何咱们还须要一个序列化器,用它来生成参数的指纹(惟一标识,译者注)。它的速度越快越好。
序列化器基于给定的输入输出一个字符串。它必须是一个肯定性算法,意味着对相同的输入,老是给出相同的输出。
序列化器生成的字符串用做缓存的key,表明记忆化函数的输入。
JSON.stringify
是实现它性能最佳的方式,比其它方式的都好 -- 这也很容易理解,由于 JSON.stringify
是原生的。
我尝试使用 bound JSON.stringify
(bar = foo.bind(null)
,此时 bar.name
为 bound foo
,译者注),但愿经过减小一次变量查找来提升性能,但很遗憾没有效果。
想在本地执行,能够执行命令 npm run benchmark:serializer
,实现的具体代码能够在项目的 GitHub 页面找到。
还剩最后一个部分:策略。
策略使用了序列化器和缓存,将二者结合起来。对 fast-memoize.js 来讲,策略是我花时间最多的部分。即便很是简单的算法,每个版本迭代都有一些性能提高。
如下是我前后尝试的方式:
咱们来逐个介绍它们。我会以尽可能简化的代码,来介绍每种方式背后的想法。若是某些细节我没有解释清楚,你想要深刻探究一下,能够在项目的 GitHub 页面中找到每一个版本的代码。
本地运行,请执行命令 npm run benchmark:strategy
。
这是我第一次尝试,也是最简单的版本。步骤是:
true
,从缓存中读取结果false
,计算,而且将结果保存到缓存中使用第一个版本,咱们能够达到每秒 650,000 次操做。这个版本是后面优化版本的基础。
改善性能的一个有效方法是优化热路径(hot path,指执行频率最高的路径,译者注)。对咱们的代码来讲,热路径就是接受一个基本类型参数的函数,这种状况下咱们不须要对参数序列化。
arguments.length === 1
&& 参数为基本类型是
,无需序列化参数,由于基本类型自己就能够做为缓存的keytrue
,从缓存中读取结果false
,计算,而且将结果保存到缓存中经过避免执行没必要要的序列化操做,咱们能够获得更快的执行结果(对热路径而言)。如今能够达到每秒 5,500,000 次了。
function.length
返回一个已定义函数的形参个数,咱们能够利用这个性质避免动态检查函数的实参个数(即避免 arguments.length === 1
的条件判断,译者注),并为单参数函数和非单参数函数分别提供不一样的策略。
functionfoo(a, b) {
return a + b
}
foo.length // => 2复制代码
省去了这一次条件判断,咱们(的实现)性能又有了一点提高,能够达到每秒 6,000,000 次操做。
我以为大多数时间都花费在了变量查找上(但没有量化数据支持),起初我也没有好的想法去改善。灵机一动,我忽然想到可使用 bind
方法,经过偏函数应用的方法将变量注入到函数中。
functionsum(a, b) {
return a + b
}
const sumBy2 = sum.bind(null, 2)
sumBy2(3) // => 5复制代码
这种方式能够将函数的某些参数固定下来。我用就它把原函数,缓存,和序列化器固定下来。就用它来试试吧!
哇!效果很是好。我不知道如何进一步改进,但我对这个版本的测试结果已经很满意了。这个版本能够达到每秒 20,000,000 次操做。
上面咱们把记忆化分解为了 3 个部分。
对每一个部分,咱们将其中 2 个部分固定,更换其他一个测试其性能。经过这种单变量测试,咱们能更加确信每次改变的效果--因为 GC 形成的不肯定性停顿,JS代码的性能并不彻底肯定。
V8 会更根据函数的调用频率、代码结构等因素,作不少运行时优化。
为了确保咱们将这 3 部分组合起来时不会错过大量性能优化的机会,咱们尝试全部可能的组合。
一共 4 种策略 x 2 种序列化器 x 4 种缓存 = 32 种不一样的组合。本地运行,请执行命令 npm run benchmark:combination
。下面是性能最好的 5 种组合:
图例:
事实证实咱们上面的分析是对的。最快的组合是:
有了上面的算法,是时候把它同最流行的库作一个性能上的比较了。本地运行,请执行命令 npm run benchmark
。结果以下:
fast-memoize.js是最快的,几乎是第二名的 3 倍,每秒 27,000,000次操做。
V8有一个很新的、未发布的优化编译器 TurboFan。
咱们如今就应该用它测试一下,由于 TurboFan(极有可能)很快就会添加到 V8 中。经过给 Node.js 设置 flag --turbo-fan
就能够启用它。本地运行,请执行命令npm run benchmark:turbo-fan
。如下是启用后的测试结果:
性能几乎翻倍,如今达到接近每秒 50,000,000 次。
看起来最新的 TurboFan 编译器能够极大的优化咱们最终版本的 fast-memoize.js。
以上就是我建立这个世界上最快的记忆化库的过程。分别实现各个部分,组合它们,而后统计每种组合方案的性能数据,从中选择最优的方案。(使用 benchmark.js )。
但愿这个过程对其余开发者有所帮助。
fast-memoize.js 是目前最好的 #JavaScrip 库, 而且我会努力让它一直是最好的。
并不是是由于我聪明绝顶, 而是我会一直维护它。 欢迎给我提交 Pull requests。
正如前 V8 工程师 Vyacheslav Egorov 所言,在虚拟机上测试算法性能很是棘手。若是你发现测试中的错误,请在 GitHub 上提交 issue。
这个库也同样,若是你发现任何问题请提交 issue(若是带上错误用例我会很感激)。带有改进建议的 Pull Requests 我将感激涕零。
若是你喜欢这个库,欢迎 star。这是对咱们开源开发者的鼓励哦。
有任何问题,欢迎评论!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。