一部の機能は非常に高速に実行されるため、システムに負担をかけますが、繰り返し呼び出しても問題はありません。 一部は非常に「重い」ため、そのような関数を呼び出すたびに、コンピューティングリソースの深刻なオーバーヘッドが発生します。 費用が正当化されれば、計算は最適化され、どこにも行けなくなります。 しかし、繰り返し呼び出し中に、関数が以前の呼び出し中に実行されたのと同じ計算を実行する場合(または非常に頻繁に)はどうでしょうか。 これを使用して生産性を向上できますか?
階乗計算とキャッシング
階乗計算関数は、リソースを集中的に使用する関数の例であり、ほぼ確実に、いくつかの呼び出し中に同じ計算の一部を何度も実行します。 これにより、キャッシングによる最適化の機会が開かれます。
たとえば、数値の階乗を計算して返す
factorial
関数は次の
factorial
です。 実装の詳細に触れない場合、次のようになります。
function factorial(n) { // : n * (n-1) * (n-2) * ... (2) * (1) return factorial }
次のように呼び出します:
factorial(50)
。 予想通り、彼女は数値50の階乗を見つけて返します。これはすべて良いのですが、今度は彼女の助けを借りて数値51の階乗を見つけましょう。コンピューターが再び計算を実行し、必要なものを見つけます。 ただし、再度呼び出すと、関数は以前に実行された多くの計算を実行することに気付くかもしれません。 関数を最適化してみましょう。 値を
factorial(50)
、関数を再度呼び出さずに
factorial(51)
進む方法を考えてみましょう。 階乗の計算式に従うと、
factorial(51)
は
factorial(50) * 51
と同じであることが
factorial(50) * 51
ます。
ただし、このアプローチでは、パフォーマンスを向上させることはできません。 つまり、最初に
factorial()
関数内で、
factorial()
50を見つけるために計算の完全なチェーンが実行され、次に何が起こったのか51が乗算されます。つまり、同様の関数を使用すると、数値51の階乗計算はすべての乗算のように見えます1から51までの数字。
factorial()
関数が前の呼び出しで実行された計算の結果を記憶し、パフォーマンスを高速化するために次の呼び出しでそれらを使用できればいいでしょう。
メモ化の基本
以前の関数呼び出しの結果を保存するという質問に答えると、メモ化のアイデアに到達します。 これは、関数が結果を保存(またはキャッシュ)するために使用できる手法です。 一般的な用語で、達成したいことを理解できたので、 メモ化のより厳密な定義を次に示します。
メモ化-繰り返し計算を防ぐために、関数の実行結果を保存します。 これは、コンピュータープログラムの速度を上げるために使用される最適化方法の1つです。
簡単に言えば、メモ化は記憶であり、何かを記憶に保存することです。 メモ化を使用する関数は、通常、より高速に動作します。同じパラメータで再度呼び出されると、一部の計算を実行する代わりに、キャッシュから結果を読み取って返すだけだからです。
簡単なメモ化関数は次のようになります。 このコードはCodePenにあるため、すぐに試してみることができます。
// , 10 const add = (n) => (n + 10); add(9); // const memoizedAdd = () => { let cache = {}; return (n) => { if (n in cache) { console.log('Fetching from cache'); return cache[n]; } else { console.log('Calculating result'); let result = n + 10; cache[n] = result; return result; } } } // memoizedAdd const newAdd = memoizedAdd(); console.log(newAdd(9)); // console.log(newAdd(9)); //
メモ化による機能コード分析
上記のコードフラグメントを分析した後、次の結論を導き出すことができます。
-
memoizeAdd
関数は、必要なときに呼び出すことができる別の関数を返します。 これが可能なのは、JavaScriptの関数がファーストクラスのオブジェクトであるため、それらを高階関数として使用し、他の関数を返すことができるためです 。
-
cache
変数はクロージャーで定義されているため、関数呼び出し間でデータを保存できます。
- 重要なことは、メモ化を伴う関数が純粋な関数であることです。 特に、この関数は、以前に呼び出された回数に関係なく、渡された同じ引数に対して同じ値を返します。 したがって、
cache
変数は期待どおりに動作します。
メモ化を伴う関数の作成
上記のコードは正常に機能しますが、メモ機能を使用して関数をそのバージョンに変換する場合はどうでしょうか。 そのような関数を書く方法は次のとおりです。 このコードも、 CodePenにあります。
// , 10 const add = (n) => (n + 10); console.log('Simple call', add(3)); // , // , const memoize = (fn) => { let cache = {}; return (...args) => { let n = args[0]; // if (n in cache) { console.log('Fetching from cache'); return cache[n]; } else { console.log('Calculating result'); let result = fn(n); cache[n] = result; return result; } } } // 'add' const memoizedAdd = memoize(add); console.log(memoizedAdd(3)); // console.log(memoizedAdd(3)); // console.log(memoizedAdd(4)); // console.log(memoizedAdd(4)); //
いいね! 私たちのメモ化機能は、メモ化により他の機能を同等のものに変えることができます。 もちろん、このコードは普遍的ではありませんが、
memoize
関数が任意の数の引数を持つ関数で機能できるように、やり直すことは難しくありません。
これは自分で作成できますが、ライブラリソリューションがあります。
- Lodashには
_.memoize(func, [resolver])
関数があります_.memoize(func, [resolver])
- ES7では、
@memoize
デコレーターを使用できます。
再帰関数のメモ化
memoize
関数の再帰関数、または上記で説明したLodashの
_.memoize
関数を渡そうとすると、再帰関数が自分自身を呼び出し、メモ化機能を追加した後の動作ではなく、何が起こるかが正しく動作しません。 その結果、この状況での
cache
変数はその目的を果たしません。 この問題を解決するために、再帰関数はメモ化を使用して独自のバリアントを呼び出す必要があります。 再帰的階乗関数にメモ化を追加する方法は次のとおりです。 通常のコードはCodePenにあります。
// memoize const memoize = (fn) => { let cache = {}; return (...args) => { let n = args[0]; if (n in cache) { console.log('Fetching from cache', n); return cache[n]; } else { console.log('Calculating result', n); let result = fn(n); cache[n] = result; return result; } } } const factorial = memoize( (x) => { if (x === 0) { return 1; } else { return x * factorial(x - 1); } } ); console.log(factorial(5)); // console.log(factorial(6)); // 6,
このコードを分析して、次の結論を導き出すことができます。
-
factorial
関数は、メモを使用してそのバージョンを再帰的に呼び出します。
- メモ化関数は、階乗計算の結果をキャッシュします。これにより、以降の呼び出しでパフォーマンスが大幅に向上します。 つまり、上記の例では、1から6までの数値を乗算して数値6の階乗を見つける代わりに、前の
factorial(5)
呼び出しによって返された値を6倍するだけで済みます。
メモ化とキャッシュについて
メモ化は一種のキャッシュです。 通常、キャッシングとは、後で使用するために何かを保存するかなり幅広い方法を指します。 たとえば、HTTPキャッシングです。 メモ化は通常、関数の戻り値をキャッシュすることを意味します。
結果:メモ化に頼るべきとき
メモ化の手法は非常に優れていると思われるかもしれませんが、すべての機能の一部になることができ、またそうなるはずですが、実際には使用が制限されています。 メモ化の使用に関する考慮事項を次に示します。
- 関数がメモ化の対象となるためには、関数はクリーンでなければならず、常に同じ引数に応じて同じ値を返します。
- メモ化は、パフォーマンスとメモリ消費のトレードオフです。 メモ化は、入力値の範囲が比較的狭い関数に適しています。これにより、多くのメモリを無駄にせずに、関数を繰り返し呼び出すときに以前に見つかった値を使用できます。
- たとえば、ブラウザコードから特定のAPIにアクセスする場合など、メモ化の独自の実装を使用する必要があるように思われるかもしれません。 ただし、特にHTTPキャッシュを使用してブラウザが自動的にキャッシュするため、これは必要ありません。
- React / Reduxを使用している場合は、 reselectをご覧ください 。 ここでは、メモ化を伴うセレクターが使用されます。 これにより、状態ツリーの対応する部分で変更が発生した場合にのみ計算を実行できます。
- おそらく、メモ化機能は、リソースを大量に消費する複雑な計算が実行される場合に最もよく示されます。 ここで、この手法はソリューションのパフォーマンスを大幅に向上させることができます。 階乗やフィボナッチ数の計算のようなものは良い訓練例ですが、現実の世界ではすべてがもっと面白くて複雑です。
親愛なる読者! 実際のプロジェクトでメモ化の使用例がある場合は、共有してください。 多くの人が彼らについて知りたいと思うでしょう。