C++ 一步一步实现LRU

什么是LRU

LRU(Least recently used),它其实是一个Queue,当其中某个元素被使用时,就把该元素从从队列中重新拿回到队首,如此进行下去,最不经常被访问的元素,就很容易被剔除队列。以此,LRU中保存的始终都是最常被访问的元素了,淘汰的都是不常访问的元素。
LRU的兄弟还有LFU (Least frequently used),原理和LRU恰好相反。

什么场景使用LRU

LRU是最常接触的缓存策略,想想操作系统中学习的页面置换算法。LRU就是常用的页面置换算法之一,操作系统的内存就是通过LRU的策略进行页的换出和换入,LRU算法是比较接近理想的一种页面置换算法,这种算法既充分利用了内存中页面调用的历史信息,又正确反映了程序的局部问题。
LRU也同样在工程代码中经常出现,使用的场景也经常为淘汰算法,缓存策略等等。例如Android使用LRU策略进行实现内存缓存,Redis中也有一个近似LRU的算法,感兴趣的小伙伴可以去深挖一下,这里就不多做剖析了。

如何实现一个LRU

1. LRU中保存的元素

template<typename K, typename V>
class Entry final
{
public:
    K key;
    V value;
};

保存的元素最好使用模板,K用来索引元素,V为真正的元素类型

2. LRU中使用链表

template<typename K, typename V>
using Iter = typename std::list<Entry<K, V>>::iterator;

template<typename K, typename V>
class List final
{
public:
    List() : capacity_(0) {}

    ~List()
    {
        lst_.clear();
    }

    uint get_capacity() const
    {
        return capacity_;
    }

    Iter<K, V> push_front(Entry<K, V> &entry)
    {
        ++capacity_;
        return lst_.insert(lst_.begin(), entry);
    }

    Iter<K, V> move_to_front(Iter<K, V> &it)
    {
        auto v = (*it);
        lst_.erase(it);
        return lst_.insert(lst_.begin(), v);
    }

    bool remove(Iter<K, V> &it)
    {
        if (lst_.size() > 0 && capacity_ > 0)
        {
            --capacity_;
            lst_.erase(it);
            return true;
        }

        return false;
    }

    Iter<K, V> back()
    {
        if (lst_.size() > 0 && capacity_ > 0)
            return (--lst_.end());

        return lst_.end();
    }

    bool is_end(Iter<K, V> it) const
    {
        return it != lst_.end();
    }

private:
    uint capacity_;
    list<Entry<K, V>> lst_;
};

在LRU里要使用这个链表的目的是什么呢?答案是保存元素的容器。这个容器的实现有很多种,我看到过很多种不同的容易来管理元素的(vector, unordered_map等),追究其根本,核心功能就是使用这个数据结构进行元素位置的调整。因为当元素被访问时,整个LRU需要将这个元素重新放置在队首。为满足这个需求,大家可以分析一下是不是链表的操作要略强一些,还有没有更优的数据结构在存放(可以牺牲空间)。

3. LRU的实现

template<typename K, typename V>
class LRUQueue final
{
public:
	// 构造函数,默认容量1024
    explicit LRUQueue(uint fixedCapacity = 1024)
            : fixed_capacity_(fixedCapacity)
    {}
	// 默认析够
    ~LRUQueue() = default;

	// 插入元素
    void insert(const K &key, const V &value);

	// 获取元素,并调整元素位置
    V get(const K &key);

	// 从LRU中手动移除
    bool remove(const K &key);

private:
	// 从list和map中移除元素
    inline void delete_(Iter<K, V> &it);

private:
    uint fixed_capacity_;
    std::unordered_map<K, Iter<K, V>> lru_map_;
    List<K, V> lru_list_;
};

简单描述一下实现原理

  • LRUQueue`中成员主要两个
    • List<K, V> lru_list_: LRU中缓存元素的列表
    • std::unordered_map<uint, Iter<K, V>> lru_map_;这个Map保存LRU元素的key和在ListIterator,方便在List中快速定位和进行操作。
  • 整个LRUQueue类包含如下几个成员方法:
    • void insert(const K &key, const V &value) : 向LRU中插入元素
    • V get(const K &key): 从LRU中获取Key的元素
    • bool remove(const K &key): 从LRU中手动移除元素,返回true表示元素在LRU中并成功移除,否则返回false。这个方法在LRU的实现中可有可无,根据需求自行使用。

LRU优化

上述的LRU结构是否足够优雅,效率是否达到了最佳,我们来一起探讨一下。优化主要方向围绕时间效率展开,针对插入insert和读取get进行讨论:

分析LRUQueue::get()

V LRUQueue::get(const K &key)
{
    auto it = lru_map_.find(key);
    if (it == lru_map_.end())
        return V();

    auto new_it = lru_list_.move_to_front(it->second);
    lru_map_[key] = new_it;

    return new_it->value;
}

Iter<K, V> List::move_to_front(Iter<K, V> &it)
{
    lst_.erase(it);
    return lst_.insert(lst_.begin(), (*it));
}

这个方法中有两个主要的问题:

  1. 返回的类型为模板V,这就要求实际的类具有拷贝构造函数,如果key不在LRU中,返回的V()还要求具备构造函数;
  2. 存储元素的容器list在将元素重新插入到队首时,做了两步操作先erase元素,再将元素insert到队首,有没有简单的方法来进行迁移。

针对这两个问题,分别给出修复的方案:

  1. 针对返回V时要求具备构造和拷贝构造的问题,有两个解决方案,一个是返回ListInterator,另一个是容器中保存std::shared_ptr<V>
  2. 因为list没有方法直接完成将某个元素重新放到队首的方法,只能先eraseinsert。并且整个操作的过程还要使用Interator。优化的方法,可以使用Linux底层的数据结构list_head或者自己写一个简单的保存了头指针的双向链表数据结构。

分析LRUQueue::insert()

void insert(const K &key, const V &value)
{
    auto it = lru_map_.find(key);
    if (it != lru_map_.end())
    {
        lru_map_[key] = lru_list_.move_to_front(it->second);
        return;
    }

    auto entry = Entry<K, V>{key, value};
    lru_map_[key] = lru_list_.push_front(entry);
    
    if (lru_list_.get_capacity() > fixed_capacity_)
    {
        auto end = lru_list_.back();
        delete_(end);
    }
}

插入LRU的问题和解决:

  1. 插入的操作需要根据业务需求进行设计。例如,重复插入是否报错,应不应该更新元素;
  2. Map一定要保存ListInterator吗,是否可以保存双向链表的指针本身;
  3. 当超过容量限制时,操作删除还是基于Interator进行,可以优化成指针的直接操作,或者彻底废弃删除接口

其他问题

  1. 整个LRU不是线程安全的,希望线程安全的话,需要LRU中添加锁
  2. Entry数据结构不需要再重复保存key值.

最终实现

#ifndef MYUTILS_LRUQUEUE_H
#define MYUTILS_LRUQUEUE_H

#include <string>
#include <map>
#include <list>
#include <unordered_map>
#include <memory>

#include "DisableCopyAndAssign.h"

using std::list;
using std::string;

template<typename K, typename V>
class Entry
{
public:
    typedef std::shared_ptr<V> ValuePtr;
    explicit Entry(K key, V *ptr) : key(key), value(ptr) {}

    ~Entry()
    {
        next = nullptr;
        pre = nullptr;
    }

    K key;
    ValuePtr value;
    Entry *next = nullptr;
    Entry *pre = nullptr;
};

template<typename K, typename V>
class List final
{
public:
    typedef Entry<K, V> EntryType;

    // 禁用拷贝构造和等号运算符
    DISABLE_COPY_AND_ASSIGN(List);

    List() = default;

    ~List()
    {
        clear();
    }

    void clear()
    {
        EntryType *temp;
        auto ptr = first_->next;
        while (ptr != last_)
        {
            temp = ptr->next;
            first_->next = temp;
            temp->pre = first_;
            delete ptr;
            ptr = temp;
        }

        delete ptr;
        size_ = 0;
        first_ = nullptr;
        last_ = nullptr;
    }

    template <class...Args>
    EntryType *emplace_front(Args&&... args)
    {
        auto entry = new EntryType(std::forward<Args>(args)...);
        return emplace_front(entry);
    }

    EntryType *emplace_front(EntryType *entry)
    {
        if(size_ == 0)
        {
            first_ = entry;
            last_ = entry;
            first_->next = last_;
            last_->pre = first_;
            size_ = 1;
        }
        else
        {
            entry->pre = last_;
            entry->next = first_;
            first_->pre = entry;
            last_->pre = entry;
            first_ = entry;
            ++size_;
        }
        return entry;
    }

    template <class...Args>
    EntryType *emplace_back(Args&&... args)
    {
        auto entry = new EntryType(std::forward<Args>(args)...);
        return emplace_back(entry);
    }

    EntryType *emplace_back(EntryType *entry)
    {
        if(size_ == 0)
        {
            first_ = entry;
            last_ = entry;
            first_->next = last_;
            last_->pre = first_;
            size_ = 1;
        }
        else
        {
            last_->next = entry;
            entry->pre = last_;
            last_ = entry;
            first_->pre = last_;
            size_++;
        }
    }

    K pop_front()
    {
        auto ptr = first_;
        first_ = first_->next;
        auto key = ptr->key;
        delete ptr;
        if(!first_)
            last_ = nullptr;
        --size_;
        return key;
    }

    K pop_back()
    {
        auto ptr = last_;
        last_ = last_->pre;
        auto key = ptr->key;
        delete ptr;
        if(!last_)
            first_ = nullptr;
        --size_;
        return key;
    }


    void move_to_front(EntryType *entry)
    {
        if(!entry || empty() || size_ == 1 || entry == first_)
             return;
        if(entry == last_)
            entry->pre->next = nullptr;
        else
        {
            entry->pre->next = entry->next;
            entry->next->pre = entry->pre;
        }
        // 下面emplace_front会重新将计数+1
        --size_;
        emplace_front(entry);
    }

    EntryType *front() const
    {
        return first_;
    }

    EntryType *back() const
    {
        return last_;
    }

    uint64_t size() const
    {
        return size_;
    }

    bool empty() const
    {
        return size_ == 0;
    }

    template <typename FUNCTION>
    void for_each(FUNCTION &&func)
    {
        auto ptr = first_;
        while (ptr)
        {
            func(ptr->value);
            ptr = ptr->next;
        }
    }

private:
    uint64_t size_ = 0;
    EntryType *first_ = nullptr;
    EntryType *last_ = nullptr;
};

template<typename K, typename V>
class LRUQueue final
{
public:
    typedef std::shared_ptr<V> ValuePtr;

    // 禁用拷贝构造和等号运算符
    DISABLE_COPY_AND_ASSIGN(LRUQueue);

    explicit LRUQueue(uint fixedCapacity = 1024) : fixed_capacity_(fixedCapacity) {}

    ~LRUQueue() = default;

    template <class...Args>
    void insert(const K &key, Args&&... args)
    {
        auto it = lru_map_.find(key);
        if (it != lru_map_.end())
        {
            // 插入重复元素,只更新位置
            lru_list_.move_to_front(it->second);
            return;
        }

        auto entry = new Entry<K, V>(key, new V(std::forward<Args>(args)...));
        lru_map_[key] = lru_list_.emplace_front(entry);

        if (lru_list_.size() > fixed_capacity_)
            lru_map_.erase(lru_list_.pop_back());
    }

    ValuePtr get(const K &key)
    {
        auto it = lru_map_.find(key);
        if (it == lru_map_.end())
            return nullptr;

        lru_list_.move_to_front(it->second);
        return it->second->value;
    }

private:
    uint fixed_capacity_;
    std::unordered_map<K, Entry<K, V> *> lru_map_;
    List<K, V> lru_list_;
};

#endif

链接:github仓库地址,欢迎点星

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值