LRUアルゴリズムとは

LRUとは

LRU の完全な英語名 (Least recent used, least recent used) は、典型的なメモリ管理アルゴリズムです。
メモリ管理のためのページ置換アルゴリズム. メモリ内にあるが使用されていないデータ ブロック (メモリ ブロック) は LRU と呼ばれます. オペレーティング システムは、LRU に属しているデータに応じてメモリからデータを移動し、追加のデータをロードするためのスペースを確保します. 簡単に言えば、最近頻繁にアクセスされたデータは保持率が高くなり、頻繁にアクセスされていないデータは削除されます。 

LRUアルゴリズムは消去アルゴリズムとも呼ばれ、過去のデータアクセス履歴に基づいてデータを消去するアルゴリズムで、「最近アクセスされたデータであれば、将来アクセスされる確率が高くなる」という考え方が核となっています。

LRU: 使用頻度が最も低い、使用頻度が最も低い、主なアプリケーション シナリオはキャッシングであり、キャッシング ルールは次のとおりです。
①.最近使用またはアクセスしたデータを先頭に配置
②.キャッシュヒット(キャッシュデータへのアクセス)が発生した場合、データを先頭に移動 ③.
キャッシュ数が最大に達した場合値、最近アクセスされたデータの削除。   

LRU アプリケーションのシナリオ

①. vue の keep-alive 組み込みコンポーネントは、コンポーネントの切り替え時に状態をメモリに保持し、 DOM のレンダリングの繰り返しを防ぎ、ロード時間とパフォーマンスの消費を削減し、ユーザー エクスペリエンスを向上させます。 
②. 基礎となるメモリ管理、ページ置換アルゴリズム。
③. 一般的なキャッシュ サービス、memcache\redis など。

仮想メモリ

オペレーティングシステムのメモリ管理に関しては、ほとんどのプロセスにリソースを提供するために、容量の小さいメモリをどのように保存して利用するかが常に重要な研究の方向性でした。メモリの仮想ストレージ管理は、現在最も一般的で成功している方法です。メモリが限られている場合、外部メモリの一部を仮想メモリとして拡張し、実メモリには現在の実行時に使用される情報のみを格納します。これにより、メモリの機能が大幅に拡張され、コンピューターの同時実行性が大幅に向上することは間違いありません。仮想ページストレージ管理とは、プロセスが必要とする領域を複数のページに分割し、現在必要なページのみをメモリに格納し、残りのページを外部メモリに配置する管理方法です。 

ただし、長所と短所があります. 仮想ページングストレージ管理は、プロセスが必要とするメモリスペースを削減しますが、実行時間が長くなるという短所もあります: プロセスの実行プロセス中に、.外部メモリ. 情報はすでにメモリにあるものと交換されます. 外部メモリの速度が遅いため, このステップに費やされる時間は無視できません. したがって、可能な限り最適なアルゴリズムを使用して、外部メモリを読み取る回数を減らしてください。

キャッシュ

キャッシングは、データの読み取りパフォーマンスを向上させる技術です. ハードウェア設計とソフトウェア開発において非常に幅広い役割を果たします. 一般的な CPU キャッシュ, データベース キャッシュ, ブラウザ キャッシュなど.
キャッシュのサイズには制限があります.キャッシュがいっぱいになった場合、どのデータを消去し、どのデータを保持する必要がありますか? これには、決定するキャッシュ排除戦略が必要です。

次の 3 つの一般的な戦略があります。
先入れ先出し戦略 (FIFO、先入れ先出し)
最小使用戦略 (LFU、Least Rrequently Used)
最低使用頻度戦略 (LRU、Least Recent Used)

配列と連結リスト

基盤となるストレージ構造の観点から見ると、アレイにはストレージ用の連続したメモリ スペースが必要であり、メモリの要件は比較的高くなります。サイズが 100MB の配列を申請した場合、メモリに十分な大きさの継続的なストレージ領域がない場合、メモリの残りの使用可能な領域が 100MB を超えていても、アプリケーションは失敗します!

連結リストは違い、連続したメモリ空間を必要とせず、ポインタを使って点在するメモリブロック群を直列に接続するので、100MBサイズの連結リストを申請すれば問題ありません!

配列と連結リストの性能比較 

配列と連結リストは、2 つの異なるデータ構造です。それらのメモリ格納方法は異なるため、以下に示すように、挿入、削除、およびアクセス操作の時間の複雑さが異なります。 

配列と連結リストの比較は時間複雑度だけにとどまらず、実際のプログラム開発では、複雑度解析だけではどのデータ構造を使ってデータを格納するかを判断することはできません。

配列はシンプルで使いやすく、実装は連続したメモリ空間を使用します.配列内のデータは、CPUのキャッシュメカニズムの助けを借りて事前に読み取ることができるため、アクセス効率が高くなります. リンクされたリストはメモリに継続的に保存されないため、CPU キャッシュにはあまり適しておらず、効果的に先読みすることができません。

配列の欠点は、そのサイズが固定されていることであり、一度宣言すると、連続したメモリ空間全体を占有します。宣言された配列が大きすぎると、システムに十分な連続メモリ空間が割り当てられず、「メモリ不足」になる可能性があります。より大きなメモリ空間を申請して元の配列をそこにコピーするのは非常に時間がかかります。連結リストにはサイズ制限がなく、動的拡張がサポートされています。これが配列との最大の違いです。

プログラムがメモリ使用量をより要求する場合は、連結リストの各ノードが次のノードへのポインタを格納するための追加のストレージ領域を必要とするため、配列の使用を検討できます。そのため、連結リストのメモリ消費量が大きくなります。 、リンクリストで頻繁に挿入および削除操作を行うと、メモリの適用および解放が頻繁に行われ、メモリの断片化が発生しやすくなります. Go言語の場合、GCが頻繁に発生する可能性があります. したがって、実際の開発では、具体的な開発シナリオに応じて、どのデータ構造を使用するかを検討する必要があります。

FIFO和LRU

FIFO は最も単純なキャッシュ アルゴリズムです. キャッシュの上限を設定します. キャッシュの上限に達すると, 先入れ先出し法に従ってキャッシュが削除され、新しいkvが追加されます.

オブジェクトをキャッシュとして使用し、オブジェクトにレコードを追加する順番に配列をマッチさせて上限に達しているかどうかを判定し、上限に達していれば配列の最初の要素キーを取得し、削除されたオブジェクトのキー値に対応します。

/**
 * FIFO队列算法实现缓存
 * 需要一个对象和一个数组作为辅助
 * 数组记录进入顺序
 */
class FifoCache{
    constructor(limit){
        this.limit = limit || 10
        this.map = {}
        this.keys = []
    }
    set(key,value){
        let map = this.map
        let keys = this.keys
        if (!Object.prototype.hasOwnProperty.call(map,key)) {
            if (keys.length === this.limit) {
                delete map[keys.shift()]//先进先出,删除队列第一个元素
            }
            keys.push(key)
        }
        map[key] = value//无论存在与否都对map中的key赋值
    }
    get(key){
        return this.map[key]
    }
}

module.exports = FifoCache

LRU アルゴリズムの観点は、最近アクセスされたデータは将来アクセスされる可能性が高くなるということです.キャッシュがいっぱいになると、最も関心のないデータが最初に削除されます.

アルゴリズム実装のアイデア: 二重連結リストのデータ構造に基づいて、完全なメンバーがない場合、新しい kv が連結リストの先頭に配置され、キャッシュ内の kv が取得されるたびに、kv が先頭に移動し、キャッシュがいっぱいになったとき、最後の 1 つが最初に削除されます。

双方向リンク リストの特徴には、ヘッド ポインターとテール ポインターがあり、各ノードには、それぞれ前のノードと後続のノードを指す prev (先行) ポインターと next (後続) ポインターがあります。

キーポイント: 二重連結リストの挿入プロセス中のシーケンスの問題に注意してください. 連結リストが連続している間にポインタを最初に処理する必要があり, 最後に元の先頭ポインタが新しく挿入された要素を指します.

class LruCache {
    constructor(limit) {
        this.limit = limit || 10
        //head 指针指向表头元素,即为最常用的元素
        this.head = this.tail = undefined
        this.map = {}
        this.size = 0
    }
    get(key, IfreturnNode) {
        let node = this.map[key]
        // 如果查找不到含有`key`这个属性的缓存对象
        if (node === undefined) return
        // 如果查找到的缓存对象已经是 tail (最近使用过的)
        if (node === this.head) { //判断该节点是不是是第一个节点
            // 是的话,皆大欢喜,不用移动元素,直接返回
            return returnnode ?
                node :
                node.value
        }
        // 不是头结点,铁定要移动元素了
        if (node.prev) { //首先要判断该节点是不是有前驱
            if (node === this.tail) { //有前驱,若是尾节点的话多一步,让尾指针指向当前节点的前驱
                this.tail = node.prev
            }
            //把当前节点的后继交接给当前节点的前驱去指向。
            node.prev.next = node.next
        }
        if (node.next) { //判断该节点是不是有后继
            //有后继的话直接让后继的前驱指向当前节点的前驱
            node.next.prev = node.prev
            //整个一个过程就是把当前节点拿出来,并且保证链表不断,下面开始移动当前节点了
        }
        node.prev = undefined //移动到最前面,所以没了前驱
        node.next = this.head //注意!!! 这里要先把之前的排头给接到手!!!!让当前节点的后继指向原排头
        if (this.head) {
            this.head.prev = node //让之前的排头的前驱指向现在的节点
        }
        this.head = node //完成了交接,才能执行此步!不然就找不到之前的排头啦!
        return IfreturnNode ?
            node :
            node.value
    }
    set(key, value) {
        // 之前的算法可以直接存k-v但是现在要把简单的 k-v 封装成一个满足双链表的节点
        //1.查看是否已经有了该节点
        let node = this.get(key, true)
        if (!node) {
            if (this.size === this.limit) { //判断缓存是否达到上限
                //达到了,要删最后一个节点了。
                if (this.tail) {
                    this.tail = this.tail.prev
                    this.tail.prev.next = undefined
                    //平滑断链之后,销毁当前节点
                    this.tail.prev = this.tail.next = undefined
                    this.map[this.tail.key] = undefined
                    //当前缓存内存释放一个槽位
                    this.size--
                }
                node = {
                    key: key
                }
                this.map[key] = node
                if(this.head){//判断缓存里面是不是有节点
                    this.head.prev = node
                    node.next = this.head
                }else{
                    //缓存里没有值,皆大欢喜,直接让head指向新节点就行了
                    this.head = node
                    this.tail = node
                }
                this.size++//减少一个缓存槽位
            }
        }
        //节点存不存在都要给他重新赋值啊
        node.value = value
    }
}

module.exports = LruCache

おすすめ

転載: blog.csdn.net/qq_34402069/article/details/127757398