- 原文地址: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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。