见 CMU 15-445 Project #0 - C++ Primer 中的准备工作。
LRU-K的提出是为了更好地解决传统LRU中的缓存污染问题。所谓缓存污染,是指突发的不常用数据的访问可能会挤出缓存中的常用数据,从而导致缓存的效果下降。而LRU-K通过增加了一个历史队列,保证了不常用数据的突发访问不会干扰缓存队列中的常用数据缓存。
根据课程文档的描述,历史队列需要通过FIFO来进行缓存淘汰,而缓存队列则需要根据 k-distance
(可是雪啊,飘进双眼~)来进行缓存淘汰,也即依据倒数第k次访问的时间戳来进行淘汰。因此缓存队列使用的并非传统的LRU,一个缓存队列中的页框尽管刚刚被访问,但它仍有可能被立即淘汰。
比如当k值为 3
时有访问序列 1->1->1->2->2->2->1
,此时 页框1
和 页框2
均被放入缓存队列,如果按照传统LRU,页框1
刚刚被访问,因此会淘汰 页框2
,但如果按照LRU-K,页框1
的倒数第三次访问更早,因此正确的淘汰页框应该是 页框1
。
LRUKReplacer部分成员变量及页框实体Frame的定义
class LRUKReplacer {
public:
class Frame {
public:
Frame(std::list<frame_id_t>::iterator iter, size_t timestamp) : iter_(iter) {
timestamp_list_.emplace_back(timestamp);
}
public:
size_t access_count_ = 1; // 该页框被访问的次数,与timestamp_list_的长度一致
bool evictable_ = true; // 该页框是否可以被驱逐
std::list<size_t> timestamp_list_; // 该页框的历史访问时间戳序列,长度最大值为k_
std::list<frame_id_t>::iterator iter_; // 该页框对应的frame_id在历史队列或缓存队列中的迭代器,用于提高查找效率
};
...
private:
size_t current_timestamp_ = 0; // 当前时间戳
size_t curr_size_ = 0; // 当前可驱逐的页框总数
size_t replacer_size_; // 内存所能容纳的最大页框数
size_t k_; // LRU-K中的K值
std::mutex latch_; // 互斥锁
std::list<frame_id_t> history_list_; // 历史队列,使用FIFO进行淘汰处理
std::list<frame_id_t> cache_list_; // 缓存队列,根据k-distance进行淘汰处理
std::unordered_map<frame_id_t, Frame> id_frame_map_; // 映射页框号和其对应的页框实体
};
RecordAccess
void RecordAccess(frame_id_t frame_id) {
std::scoped_lock<std::mutex> locker(latch_);
/* frame_id不能超过replacer_size_,由此来限制页框总数。 */
if (frame_id > static_cast<int>(replacer_size_)) {
return;
}
/* 如果frame_id不在id_frame_map_中,表明是首次访问,需要将frame_id插入到历史队列队尾,同时新建页框实体并建立与frame_id的映射;
* 否则需要根据+1后的访问次数与k_的关系进行相应处理。 */
auto cur_pair = id_frame_map_.find(frame_id);
if (cur_pair == id_frame_map_.end()) {
history_list_.emplace_back(frame_id);
Frame new_frame(std::prev(history_list_.end()), current_timestamp_);
id_frame_map_.emplace(frame_id, new_frame);
curr_size_++;
} else {
/* 增加访问次数 */
cur_pair->second.access_count_++;
/* 如果访问次数小于k次,只需要扩充访问时间序列;
* 如果访问次数等于k次,需要扩充访问时间序列,然后将frame_id从历史队列删除,并根据倒数第k次的访问时间插入到缓存队列相应位置;
* 如果访问次数大于k次,需要更新访问时间序列,并根据倒数第k次的访问时间将frame_id移动到缓存队列相应位置。 */
if (cur_pair->second.access_count_ < k_) {
/* 扩充访问时间序列 */
cur_pair->second.timestamp_list_.emplace_back(current_timestamp_);
} else if (cur_pair->second.access_count_ == k_) {
/* 扩充访问时间序列 */
cur_pair->second.timestamp_list_.emplace_back(current_timestamp_);
/* 将frame_id从历史队列删除 */
history_list_.erase(cur_pair->second.iter_);
/* 根据倒数第k次的访问时间将frame_id插入到缓存队列相应位置 */
size_t k_timestamp = cur_pair->second.timestamp_list_.front();
auto iter = cache_list_.begin();
while (iter != cache_list_.end() && k_timestamp > id_frame_map_.at(*iter).timestamp_list_.front()) {
iter++;
}
cur_pair->second.iter_ = cache_list_.insert(iter, frame_id); // 插入后要修改对应的迭代器
} else {
/* 更新访问时间序列 */
cur_pair->second.timestamp_list_.pop_front();
cur_pair->second.timestamp_list_.emplace_back(current_timestamp_);
/* 根据倒数第k次的访问时间将frame_id移动到缓存队列相应位置 */
cache_list_.erase(cur_pair->second.iter_);
size_t k_timestamp = cur_pair->second.timestamp_list_.front();
auto iter = cache_list_.begin();
while (iter != cache_list_.end() && k_timestamp > id_frame_map_.at(*iter).timestamp_list_.front()) {
iter++;
}
cur_pair->second.iter_ = cache_list_.insert(iter, frame_id); // 插入后要修改对应的迭代器
}
}
/* 时间增加 */
current_timestamp_++;
}
Evict
auto Evict(frame_id_t *frame_id) -> bool {
std::scoped_lock<std::mutex> locker(latch_);
/* 没有允许驱逐的页框 */
if (curr_size_ == 0) {
return false;
}
/* 如果历史队列不为空,优先按FIFO从历史队列中驱逐 */
if (!history_list_.empty()) {
for (auto iter = history_list_.begin(); iter != history_list_.end(); iter++) {
if (id_frame_map_.at(*iter).evictable_) {
*frame_id = *iter;
id_frame_map_.erase(*iter);
history_list_.erase(iter);
curr_size_--;
return true;
}
}
}
/* 如果没能从历史队列中驱逐,则从缓存队列中按照依据k-distance驱逐 */
if (!cache_list_.empty()) {
for (auto iter = cache_list_.begin(); iter != cache_list_.end(); iter++) {
if (id_frame_map_.at(*iter).evictable_) {
*frame_id = *iter;
id_frame_map_.erase(*iter);
cache_list_.erase(iter);
curr_size_--;
return true;
}
}
}
return false;
}
SetEvictable
void SetEvictable(frame_id_t frame_id, bool set_evictable) {
std::scoped_lock<std::mutex> locker(latch_);
auto cur_pair = id_frame_map_.find(frame_id);
/* frame_id不存在 */
if (cur_pair == id_frame_map_.end()) {
return;
}
/* 根据evictable_的初始值和目标值修改curr_size_ */
if (cur_pair->second.evictable_ && !set_evictable) {
curr_size_--;
} else if (!cur_pair->second.evictable_ && set_evictable) {
curr_size_++;
}
cur_pair->second.evictable_ = set_evictable;
}
Remove
void Remove(frame_id_t frame_id) {
std::scoped_lock<std::mutex> locker(latch_);
auto cur_pair = id_frame_map_.find(frame_id);
/* 对应页框不存在 */
if (cur_pair == id_frame_map_.end()) {
return;
}
/* 对应页框不可驱逐 */
if (!cur_pair->second.evictable_) {
return;
}
/* 根据对应页框的引用次数从历史队列或缓存队列中将其删除,同时也要删除id_frame_map_中的映射 */
if (cur_pair->second.access_count_ >= k_) {
cache_list_.erase(cur_pair->second.iter_);
} else {
history_list_.erase(cur_pair->second.iter_);
}
id_frame_map_.erase(cur_pair);
curr_size_--;
}
Size
auto Size() -> size_t {
std::scoped_lock<std::mutex> locker(latch_);
return curr_size_;
}
提交后的结果如下,LRU-K的所有测试用例全部通过:
由于默认提供的本地测试用例过于简单,很多问题都无法排查,因此这里分享两个个稍微复杂一点的测试用例:
TEST(LRUKReplacerTest, EvictTest1) {
LRUKReplacer lru_replacer(3, 3);
frame_id_t frame_id;
ASSERT_EQ(lru_replacer.Size(), 0);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(1);
ASSERT_EQ(lru_replacer.Size(), 2);
lru_replacer.RecordAccess(3);
lru_replacer.Evict(&frame_id);
ASSERT_EQ(frame_id, 3);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 1);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(3);
lru_replacer.RecordAccess(1);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 1);
lru_replacer.RecordAccess(3);
lru_replacer.RecordAccess(3);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 2);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 3);
}
TEST(LRUKReplacerTest, EvictTest2) {
LRUKReplacer lru_replacer(3, 3);
frame_id_t frame_id;
ASSERT_EQ(lru_replacer.Size(), 0);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(1);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(2);
lru_replacer.RecordAccess(3);
lru_replacer.RecordAccess(3);
lru_replacer.RecordAccess(3);
lru_replacer.RecordAccess(2);
ASSERT_EQ(lru_replacer.Size(), 3);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 1);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 2);
lru_replacer.Evict(&frame_id);
EXPECT_EQ(frame_id, 3);
}
参考 :
https://blog.csdn.net/Altair_alpha/article/details/127745308
https://blog.csdn.net/AntiO2/article/details/128439155