LRU and LFU algorithms (page replacement algorithm)

Posted by whatwhat123 on Sun, 27 Feb 2022 03:47:14 +0100

Difference between LRU and LFU

Both LRU and LFU are page replacement algorithms for memory management.

LRU: Least Recently Used (longest time) elimination algorithm. LRU is the page that has not been used for the longest time.

LFU: Least Frequently Used. LFU is the page that has been used the least times in a period of time.

  • example

    Suppose the period T of LFU method is 10 minutes, the time spent visiting the following pages is exactly 10 minutes, and the memory block size is 3. If the required page order is as follows:

    2 1 2 1 2 3 4

    ---------------------------------------->

    • When page 4 needs to be used, 1, 2 and 3 are stored in the memory block. If there is no page 4 in the memory block, page missing interrupt will occur. At this time, the memory block is full and page replacement is required.
    • If LRU algorithm is adopted, page 1 shall be replaced. Because page 1 has not been used for the longest time, pages 2 and 3 have been used behind it.
    • If LFU algorithm is adopted, page 3 shall be changed. Because during this period, page 1 was visited twice, page 2 was visited three times, and page 3 was visited only once, with the least number of visits in a period of time.

    The key of LRU is to see the length of time from the last use of the page to the replacement. The longer the time, the page will be replaced;

    The key to LFU is to look at the frequency (Times) of pages used in a certain period of time. The lower the frequency, the pages will be replaced.

  • LRU algorithm is suitable for: large files, such as game clients (recently loaded map files);
  • LFU algorithm is suitable for small files and fragmented files, such as system files and application files;
  • LRU consumes less CPU resources and LFU consumes more CPU resources.

LRU (maximum time)

The longest unused algorithm recently, LRU is to eliminate the pages that have not been used for the longest time

function

  1. The cache capacity is a positive integer, and the cache key and value are of type int
  2. Read cache func get (key int):

    • key already exists, return the corresponding value
    • key does not exist, return - 1
  3. Write cache func put(key int, value int):

    • key already exists, modify the corresponding value
    • If the key does not exist, write to this group of caches. If the cache capacity has reached the upper limit before writing, the cache that has not been used for the longest time should be eliminated (emphasis: both read and write caches are considered to be used)

data structure

  • Last usage time of using bidirectional linked list to maintain cache:

    • Convention: the nodes in the positive direction of the linked list (from the head to the tail) are sorted according to the use time - the earlier the nodes are used (i.e. not used for a long time), the closer they are to the tail of the linked list
    • Maintenance: move the linked list node corresponding to the cache to the head of the linked list every time the cache is used; When the cache is eliminated, you only need to delete the tail node
  • Add a map to record the mapping relationship between the key and the linked list node; Solution: if you only use a two-way linked list, you must traverse the linked list every time you judge whether a key exists

  1. cache: map[int]*listNode, key to node mapping; Where listNode data: key, value
  2. List: * listNode, a two-way linked list, maintains the last use time of the cache
  3. Capacity: int, linked list capacity

Pseudo code

  • Read cache

    1. key exists:

      • Delete the cache node in the original linked list and re insert it into the head of the linked list,
      • Return the corresponding value
    2. key does not exist:

      • Return - 1
  • Write cache (update cache)

    1. Key exists:

      • Update the value of the cache node
      • Delete the cache node in the original linked list and re insert it into the head of the linked list
    2. Key does not exist:

      1. Maximum capacity reached:

        • Delete the tail node in the linked list (record the key of the node)
        • Delete the corresponding mapping relationship according to the key recorded in the previous step
        • Construct a new node according to the input parameters:
        • Insert the new node into the head of the linked list
        • Mapping relationship between new key and new node
      2. Capacity below limit:

        • Construct a new node according to the input parameters:
        • Insert the new node into the head of the linked list
        • Mapping relationship between new key and new node

Golang code implementation

// Bidirectional linked list node
type doublyListNode struct {
    key   int
    value int
    prev  *doublyListNode
    next  *doublyListNode
}

// Construct a two-way empty linked list (the first and last points are empty nodes)
func newDoublyList() *doublyListNode {
    headNode := &doublyListNode{}
    tailNode := &doublyListNode{}
    headNode.next = tailNode
    tailNode.prev = headNode
    return headNode
}

// Add node to the head of linked list
func (dl *doublyListNode) addToHead(node *doublyListNode) {
    dl.next.prev = node
    node.next = dl.next
    dl.next = node
    node.prev = dl
}

// Delete nodes in the linked list
func removeNode(node *doublyListNode) {
    node.next.prev = node.prev
    node.prev.next = node.next
}

// LRUCache specific cache
type LRUCache struct {
    cache    map[int]*doublyListNode
    head     *doublyListNode
    tail     *doublyListNode
    capacity int
}

// Constructor build cache container
func Constructor(capacity int) LRUCache {
    dl := newDoublyList()
    return LRUCache{
        cache:    make(map[int]*doublyListNode),
        head:     dl,
        tail:     dl.next,
        capacity: capacity,
    }
}

func (lruCache *LRUCache) Get(key int) int {
    // Get cache according to key
    v, ok := lruCache.cache[key]
    // If there is no cache, - 1 is returned
    if !ok {
        return -1
    }
    // If there is a cache
    removeNode(v)              // Remove the cache
    lruCache.head.addToHead(v) // Add the cache to the head of the bidirectional linked list
    return v.value
}

// Put new cache
func (lruCache *LRUCache) Put(key int, value int) {
    // There is already a cache
    if v, ok := lruCache.cache[key]; ok { // v is the node in the double linked list
        v.value = value            // Update values in linked list nodes
        lruCache.cache[key] = v    // Update mapping relationship in cache
        removeNode(v)              // Remove the cache
        lruCache.head.addToHead(v) // Add the cache to the head of the bidirectional linked list
        return
    }
    // Cache super long obsolete cache
    if len(lruCache.cache) >= lruCache.capacity {
        node := lruCache.tail.prev
        removeNode(node)                 // Delete this node
        delete(lruCache.cache, node.key) // Clear least recently used cache
    }
    newNode := &doublyListNode{
        key:   key,
        value: value,
    }
    lruCache.cache[key] = newNode
    lruCache.head.addToHead(newNode)
}

LFU (minimum times)

function

  1. Cache capacity, cache key and value are natural numbers (can be 0, which will be handled separately in the code)
  2. Read cache func get(key int) int: (same as lru)

    • key already exists, return the corresponding value
    • key does not exist, return - 1
  3. Write cache func put(key int, value int):

    • key already exists, modify the corresponding value
    • If the key does not exist, write to this group of caches. If the cache capacity has reached the upper limit before writing, the cache with the least usage times should be eliminated (remember that its usage times are n);
    • If the number of caches used n is greater than one, the cache that has not been used for the longest time will be eliminated (that is, the lru rule will be followed at this time)

data structure

// The specific cache frequency of LFUCache is the number of times it is used
type LFUCache struct {
    recent   map[int]*doublyListNode // Frequency to the most recently used node in the node with frequency
    count    map[int]int             // Frequency mapping to the number of nodes corresponding to the frequency
    cache    map[int]*doublyListNode // key to node mapping
    list     *doublyList             // Two way linked list, maintaining the usage times (priority) and last usage time of the cache
    capacity int                     // capacity
}

Pseudo code

  • Read cache

    1. Existence: (note that the node frequency is n)

      • If there are other nodes with frequency = n+1, move the node to the front of all nodes with frequency = n+1;
      • Otherwise, if there are other nodes with frequency = n and the current node is not the nearest node, move the node to the front of all nodes with frequency = n;
      • Otherwise, do not move the node (in this case, the node should stay in its current position)
      • Update recent
      • Update count
      • Add node frequency +1
      • Returns the value of the node
    2. Does not exist: returns - 1
  • Write cache

    • key exists

      • Reference read cache - if the key exists, you can modify the corresponding value additionally
    • non-existent:

      • If the current cache capacity has reached the maximum:

        • Eliminate the tail cache node (note that node freq is n)
        • If there are no other nodes with freq = n, set recent to null
        • Update cache
        • Update count
      • Construct a new node: key, value, frequency = 1

        • Whether there are other nodes with frequency = 1:
        • Presence: insert in front of them
        • Does not exist: insert the tail of the linked list
        • Update recent
        • Update cache
        • Update count

Golang code implementation

// Bidirectional linked list
type doublyList struct {
    head *doublyListNode
    tail *doublyListNode
}

// Delete tail node
func (dl *doublyList) removeTail() {
    pre := dl.tail.prev.prev
    pre.next = dl.tail
    dl.tail.prev = pre
}

// Is the linked list empty
func (dl *doublyList) isEmpty() bool {
    return dl.head.next == dl.tail
}

// Bidirectional linked list node
type doublyListNode struct {
    key       int
    value     int
    frequency int // Usage times
    prev      *doublyListNode
    next      *doublyListNode
}

// Insert a node before a node
func addBefore(currNode *doublyListNode, newNode *doublyListNode) {
    pre := currNode.prev
    pre.next = newNode
    newNode.next = currNode
    currNode.prev = newNode
    newNode.prev = pre
}

// LFUCache specific cache
type LFUCache struct {
    recent   map[int]*doublyListNode // Frequency to the most recently used node in the node with frequency
    count    map[int]int             // Frequency mapping to the number of nodes corresponding to the frequency
    cache    map[int]*doublyListNode // key to node mapping
    list     *doublyList             // Two way linked list, maintaining the usage times (priority) and last usage time of the cache
    capacity int                     // capacity
}

func removeNode(node *doublyListNode) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

// Constructor build cache container
func Constructor(capacity int) LFUCache {
    return LFUCache{
        recent:   make(map[int]*doublyListNode),
        count:    make(map[int]int),
        cache:    make(map[int]*doublyListNode),
        list:     newDoublyList(),
        capacity: capacity,
    }
}

func newDoublyList() *doublyList {
    headNode := &doublyListNode{}
    tailNode := &doublyListNode{}
    headNode.next = tailNode
    tailNode.prev = headNode
    return &doublyList{
        head: headNode,
        tail: tailNode,
    }
}

func (lfu *LFUCache) Get(key int) int {
    if lfu.capacity == 0 {
        return -1
    }
    node, ok := lfu.cache[key]
    if !ok { // key does not exist
        return -1
    }
    // key already exists
    next := node.next
    if lfu.count[node.frequency+1] > 0 {
        // There are other caches with n+1 usage times. Move the specified cache to the front of all nodes with n+1 usage times
        removeNode(node)
        addBefore(lfu.recent[node.frequency+1], node)
    } else if lfu.count[node.frequency] > 1 && lfu.recent[node.frequency] != node {
        // There are no other caches with n+1 usage times, but there are other caches with n usage times, and the current node is not the nearest node
        // Moves the specified cache to the front of all nodes that are used n times
        removeNode(node)
        addBefore(lfu.recent[node.frequency], node)
    }
    // Update recent
    lfu.recent[node.frequency+1] = node
    if lfu.count[node.frequency] <= 1 { // There are no other nodes with freq = n, and recent is set to null
        lfu.recent[node.frequency] = nil
    } else if lfu.recent[node.frequency] == node { // If there are other nodes with freq = n and recent = node, move recent backward by one bit
        lfu.recent[node.frequency] = next
    }
    // Number of nodes corresponding to update usage times
    lfu.count[node.frequency+1]++
    lfu.count[node.frequency]--
    // Update cache usage
    node.frequency++
    return node.value
}

// Put new cache
func (lfu *LFUCache) Put(key int, value int) {
    if lfu.capacity == 0 {
        return
    }
    node, ok := lfu.cache[key]
    if ok { // key already exists
        lfu.Get(key)
        node.value = value
        return
    }

    // key does not exist
    if len(lfu.cache) >= lfu.capacity { // When the cache is full, delete the last node and update cache, count and recent (condition) accordingly
        tailNode := lfu.list.tail.prev
        lfu.list.removeTail()
        if lfu.count[tailNode.frequency] <= 1 {
            lfu.recent[tailNode.frequency] = nil
        }
        lfu.count[tailNode.frequency]--
        delete(lfu.cache, tailNode.key)
    }
    newNode := &doublyListNode{
        key:       key,
        value:     value,
        frequency: 1,
    }

    // Insert a new cache node
    if lfu.count[1] > 0 {
        addBefore(lfu.recent[1], newNode)
    } else {
        addBefore(lfu.list.tail, newNode)
    }

    // Update recent, count and cache
    lfu.recent[1] = newNode
    lfu.count[1]++
    lfu.cache[key] = newNode
}

Topics: Python Go Back-end