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和在List
中Iterator
,方便在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));
}
这个方法中有两个主要的问题:
- 返回的类型为模板V,这就要求实际的类具有拷贝构造函数,如果
key
不在LRU中,返回的V()
还要求具备构造函数; - 存储元素的容器
list
在将元素重新插入到队首时,做了两步操作先erase
元素,再将元素insert
到队首,有没有简单的方法来进行迁移。
针对这两个问题,分别给出修复的方案:
- 针对返回V时要求具备构造和拷贝构造的问题,有两个解决方案,一个是返回
List
的Interator
,另一个是容器中保存std::shared_ptr<V>
- 因为
list
没有方法直接完成将某个元素重新放到队首的方法,只能先erase
再insert
。并且整个操作的过程还要使用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的问题和解决:
- 插入的操作需要根据业务需求进行设计。例如,重复插入是否报错,应不应该更新元素;
Map
一定要保存List
的Interator
吗,是否可以保存双向链表的指针本身;- 当超过容量限制时,操作删除还是基于
Interator
进行,可以优化成指针的直接操作,或者彻底废弃删除接口
其他问题
- 整个LRU不是线程安全的,希望线程安全的话,需要LRU中添加锁
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