查看: 4198| 回复: 14
跳转到指定楼层
上一主题 下一主题
收起左侧

[题目讨论] 关于cache的 eviction policy设计

全局:

注册一亩三分地论坛,查看更多干货!

您需要 登录 才可以下载或查看附件。没有帐号?注册账号

x
有这样一道面试题,觉得很不错,拿过来给大家讨论讨论。
就是设计一个cache的eviction policy,你可以选择是LRU, 还是LFU,还是其他eviction policy. 选择哪个,就按照哪个eviction policy执行。
大家认为这种情况,用哪个pattern比较好?

上一篇:求系统设计网课资源
下一篇:统计网站最近5分钟的访问次数
推荐
magicsets 2018-1-29 14:09:17 | 只看该作者
全局:
我来大概写一点想法..

首先回顾一下LeetCode的LRUCache这道题目所设计的接口:
  1. class LRUCache {
  2. public:
  3.   // Get the value (will always be positive) of the key if the key exists in the
  4.   // cache, otherwise return -1.
  5.   int get(int key);

  6.   // Set or insert the value if the key is not already present. When the cache
  7.   // reached its capacity, it should invalidate the least recently used item
  8.   // before inserting a new item.
  9.   void put(int key, int value);
  10. };
复制代码


当然这个接口非常简陋,至少有两个问题:

(1) Cache和eviction policy(LRU)的逻辑是合并在一起的

这也正是我们这里OOD设计所要解决的问题:将eviction policy解耦(decouple)出来,使得其可以作为一个参数(或者模板参数)被传入Cache类,那么得到的Cache对象就会使用对应的policy。

(2) 没有触及cache设计的一个重要部分:value的生存期的管理

如果value是占用较多内存空间的大对象(而不是LeetCode题中仅占用4个字节的栈区int),或者是C/C++写的代码,一般是由cache管理生存期。
也就是说当evict一个value时,其占用内存应该立刻被回收,而这时所有对该value对象的引用应当立刻失效 —— 在这种情况下要避免代码出bug(非法内存访问),必须由cache对value进行引用计数,且返回值使用"counted reference"(类似于C++ shared pointer)。
不过如果返回的是栈区对象/对象的拷贝/带GC语言下(例如Java)的对象引用,那么不需要考虑引用计数问题,但是这时cache就无法保证内存的使用量被控制在规定范围内。

第(2)点其实是为了说一下eviction policy设计时关于引用计数的接口 —— 下面简化的代码中没有实现这一接口,但是需要了解一下,因为很多实际系统中是有相关内容的,例如PostgreSQL:https://github.com/postgres/postgres/blob/master/src/backend/storage/buffer/bufmgr.c#L139

现在回到(1),下面贴的代码主要包含了:
(a) EvictionPolicy的抽象类
(b) 继承了EvictionPolicy的LRU/LFU实现
(c) 一个EvictionPolicy工厂,使得可以比较容易地添加新的Policy实现
(c) 使用EvictionPolicy实现的Cache类

  1. #include <chrono>
  2. #include <cstddef>
  3. #include <iostream>
  4. #include <list>
  5. #include <memory>
  6. #include <mutex>
  7. #include <set>
  8. #include <stdexcept>
  9. #include <unordered_map>
  10. #include <utility>
  11. #include <vector>

  12. // Eviction policy的抽象
  13. template <typename Entry>
  14. class EvictionPolicy {
  15. public:
  16.   virtual Entry chooseEntryToEvict() = 0;

  17.   virtual void entryCreated(const Entry &entry) = 0;

  18.   virtual void entryReferenced(const Entry &entry) = 0;

  19.   // 如果维护引用计数的话需要这个接口
  20.   // virtual void entryUnreferenced(const Entry &entry) = 0;

  21.   virtual void entryEvicted(const Entry &entry) = 0;
  22. };


  23. // Eviction policy的类型
  24. enum class EvictionPolicyType : int {
  25.   kLRU = 0,
  26.   kLFU,
  27.   kClock,
  28.   // ...
  29. };


  30. // 一个简单的LRU实现,不支持并发访问
  31. template <typename Entry>
  32. class LRUPolicy : public EvictionPolicy<Entry> {
  33. public:
  34.   Entry chooseEntryToEvict() override {
  35.     return list_.back();
  36.   }

  37.   void entryCreated(const Entry &entry) override {
  38.     list_.push_front(entry);
  39.     index_.emplace(entry, list_.begin());
  40.   }

  41.   void entryReferenced(const Entry &entry) override {
  42.     const auto &it = index_.at(entry);
  43.     if (it != list_.begin()) {
  44.       list_.splice(list_.begin(), list_, it, std::next(it));
  45.     }
  46.   }

  47.   void entryEvicted(const Entry &entry) override {
  48.      const auto it = index_.find(entry);
  49.      list_.erase(it->second);
  50.      index_.erase(it);
  51.   }

  52. private:
  53.   using EntryList = std::list<Entry>;
  54.   using EntryListIterator = typename EntryList::iterator;

  55.   std::list<Entry> list_;
  56.   std::unordered_map<Entry, const EntryListIterator> index_;
  57. };


  58. // 一个简单的LFU实现,不支持并发访问
  59. template <typename Entry>
  60. class LFUPolicy : public EvictionPolicy<Entry> {
  61. public:
  62.   Entry chooseEntryToEvict() override {
  63.     return std::get<0>(*list_.begin());
  64.   }

  65.   void entryCreated(const Entry &entry) override {
  66.     const auto ret = list_.emplace(entry, Clock::now(), 1);
  67.     index_.emplace(entry, ret.first);
  68.   }

  69.   void entryReferenced(const Entry &entry) override {
  70.     auto &it = index_.at(entry);
  71.     const std::size_t count = std::get<2>(*it);
  72.     list_.erase(it);
  73.     const auto ret = list_.emplace(entry, Clock::now(), count + 1);
  74.     it = ret.first;
  75.   }

  76.   void entryEvicted(const Entry &entry) override {
  77.     const auto it = index_.find(entry);
  78.     list_.erase(it->second);
  79.     index_.erase(it);
  80.   }

  81. private:
  82.   using Clock = std::chrono::high_resolution_clock;
  83.   using TimePoint = std::chrono::time_point<Clock>;
  84.   using Record = std::tuple<Entry, TimePoint, std::size_t>;
  85.   struct RecordComparator {
  86.     inline bool operator()(const Record &lhs, const Record &rhs) const {
  87.       if (std::get<2>(lhs) != std::get<2>(rhs)) {
  88.         return std::get<2>(lhs) < std::get<2>(rhs);
  89.       }
  90.       if (std::get<1>(lhs) != std::get<1>(rhs)) {
  91.         return std::get<1>(lhs) < std::get<1>(rhs);
  92.       }
  93.       return std::get<0>(lhs) < std::get<0>(rhs);
  94.     }
  95.   };
  96.   using SortedList = std::set<Record, RecordComparator>;
  97.   using SortedListIterator = typename SortedList::iterator;

  98.   SortedList list_;
  99.   std::unordered_map<Entry, SortedListIterator> index_;
  100. };


  101. // Eviction policy的单例工厂
  102. template <typename Entry>
  103. class EvictionPolicyFactory {
  104. public:
  105.   static const EvictionPolicyFactory& Instance() {
  106.     static EvictionPolicyFactory<Entry> instance;
  107.     return instance;
  108.   }

  109.   EvictionPolicy<Entry>* createEvictionPolicy(const EvictionPolicyType type) const {
  110.     switch (type) {
  111.       case EvictionPolicyType::kLRU:
  112.         return new LRUPolicy<Entry>();
  113.       case EvictionPolicyType::kLFU:
  114.         return new LFUPolicy<Entry>();
  115.       default:
  116.         break;
  117.     }
  118.     throw std::runtime_error("Unsupported eviction policy type");
  119.   };
  120. };


  121. // 一个简单的泛型Cache实现
  122. template <typename Key, typename Value>
  123. class Cache {
  124. private:
  125.   using Storage = std::unordered_map<Key, Value>;
  126.   using Entry = const typename Storage::value_type*;
  127.   using Factory = EvictionPolicyFactory<Entry>;

  128. public:
  129.   Cache(const std::size_t capacity, const EvictionPolicyType type)
  130.       : capacity_(capacity),
  131.         eviction_policy_(Factory::Instance().createEvictionPolicy(type)) {
  132.   }

  133.   const Value* get(const Key &key) {
  134.     const auto it = storage_.find(key);
  135.     if (it == storage_.end()) {
  136.       return nullptr;
  137.     }

  138.     eviction_policy_->entryReferenced(&(*it));
  139.     return &it->second;
  140.   }

  141.   void put(const Key &key, const Value &value) {
  142.     const auto it = storage_.find(key);
  143.     if (it == storage_.end()) {
  144.       if (storage_.size() >= capacity_) {
  145.         const auto ev = eviction_policy_->chooseEntryToEvict();
  146.         eviction_policy_->entryEvicted(ev);
  147.         storage_.erase(ev->first);
  148.       }
  149.       const auto ret = storage_.emplace(key, value);
  150.       eviction_policy_->entryCreated(&(*ret.first));
  151.     } else {
  152.       it->second = value;
  153.       eviction_policy_->entryReferenced(&(*it));
  154.     }
  155.   }

  156. private:
  157.   const std::size_t capacity_;
  158.   Storage storage_;
  159.   std::unique_ptr<EvictionPolicy<Entry>> eviction_policy_;
  160. };

  161. /*******************************************************************************
  162. * 可以用于提交LeetCode 146的代码

  163. class LRUCache {
  164. public:
  165.   LRUCache(int capacity)
  166.       : cache_(capacity, EvictionPolicyType::kLRU) {
  167.   }

  168.   int get(int key) {
  169.     const int *value = cache_.get(key);
  170.     return value == nullptr ? -1 : *value;
  171.   }

  172.   void put(int key, int value) {
  173.     cache_.put(key, value);
  174.   }

  175. private:
  176.   Cache<int, int> cache_;
  177. };

  178. *******************************************************************************/

  179. template <typename C, typename T>
  180. void Print(C &cache, const T &key) {
  181.   const auto *value = cache.get(key);
  182.   if (value == nullptr) {
  183.     std::cout << "[" << key << "] NULL\n";
  184.   } else {
  185.     std::cout << "[" << key << "] " << *value << "\n";
  186.   }
  187. }

  188. int main(int argc, char *argv[]) {
  189.   std::vector<std::pair<EvictionPolicyType, std::string>> types = {
  190.       { EvictionPolicyType::kLRU, "LRU" },
  191.       { EvictionPolicyType::kLFU, "LFU" }
  192.   };

  193.   // 测试 LRU/LFU
  194.   for (const auto type : types) {
  195.     Cache<std::string, std::string> cache(2, type.first);

  196.     std::cout << "Testing " << type.second << ":\n";
  197.     std::cout << "--------\n";

  198.     cache.put("1", "A");
  199.     cache.put("2", "B");

  200.     Print(cache, "1");
  201.     Print(cache, "2");
  202.     std::cout << "##\n";

  203.     cache.put("1", "C");
  204.     cache.put("3", "D");
  205.     cache.put("4", "E");
  206.     Print(cache, "1");
  207.     Print(cache, "2");
  208.     Print(cache, "3");
  209.     Print(cache, "4");

  210.     std::cout << "\n";
  211.   }
  212. }
复制代码



PS 可以在线测试一下代码:http://cpp.sh/5ifbw

输出是这样的
  1. Testing LRU:
  2. --------
  3. [1] A
  4. [2] B
  5. ##
  6. [1] NULL
  7. [2] NULL
  8. [3] D
  9. [4] E

  10. Testing LFU:
  11. --------
  12. [1] A
  13. [2] B
  14. ##
  15. [1] C
  16. [2] NULL
  17. [3] NULL
  18. [4] E
复制代码

回复

使用道具 举报

全局:
flykite083 发表于 2018-1-29 23:50
我也想过strategy, 但是问题是cache自带一些数据结构,比如说dictionary, linkedlist, capacity等。然后 ...

我刚搜了圈,发现 apache ignite 有类似的抽象的例子
https://ignite.apache.org/releas ... EvictionPolicy.html,不过好像就是把evictionPloicy抽象成abstract class了,然后数据结构也是在子类中定义的。就如同你说的,不一样的policy的数据结构是不一样的,强行去abstract common 数据结构会有点得不偿失,所以用个工厂模式可能会比较好懂,代码也比较直观。
回复

使用道具 举报

推荐
 楼主| flykite083 2018-1-29 23:50:59 | 只看该作者
全局:
kuanghaochina 发表于 2018-1-29 22:23
我觉得可能strategy看起来更好些,考虑到是个behavioral pattern,不同algorithm 重写同一个方法, 但是需要 ...

我也想过strategy, 但是问题是cache自带一些数据结构,比如说dictionary, linkedlist, capacity等。然后用strategy的话,就是LRUstrategy和LFUstrategy class各自都有他们的get和put. 这时get和put就得传一堆东西了,比如node, dictionary等。这样好么?
回复

使用道具 举报

🔗
oneexy 2018-1-28 07:18:08 | 只看该作者
全局:
这个问题,伟大的linux帮你准备了N多套方案,简单来说,就是在cache hit和latency之间找平衡,lru最好啦,要速度就lfu 或者clock, 都要就lru+lfu。
回复

使用道具 举报

🔗
 楼主| flykite083 2018-1-28 09:36:10 | 只看该作者
全局:
oneexy 发表于 2018-1-28 07:18
这个问题,伟大的linux帮你准备了N多套方案,简单来说,就是在cache hit和latency之间找平衡,lru最好啦, ...

多谢解答!不过面试的那个问题是要你design这个,是个OOD的问题。
回复

使用道具 举报

🔗
oneexy 2018-1-28 09:42:16 | 只看该作者
全局:
flykite083 发表于 2018-1-28 09:36
多谢解答!不过面试的那个问题是要你design这个,是个OOD的问题。

那样的话就变成一个算法题了呗,就是落实到代码呗,最难得就是lru这个会了就没啥问题了吧。真遇到了这个问题,我肯定选一个我会写代码的发昂发去做。
回复

使用道具 举报

🔗
 楼主| flykite083 2018-1-28 10:08:04 | 只看该作者
全局:
oneexy 发表于 2018-1-28 09:42
那样的话就变成一个算法题了呗,就是落实到代码呗,最难得就是lru这个会了就没啥问题了吧。真遇到了这个 ...

应该是这样的:你可以选择eviction的policy, LRU, LFU, 或者其他x策略,y策略,z策略。用什么design pattern?
回复

使用道具 举报

🔗
oneexy 2018-1-28 10:15:37 | 只看该作者
全局:
flykite083 发表于 2018-1-28 10:08
应该是这样的:你可以选择eviction的policy, LRU, LFU, 或者其他x策略,y策略,z策略。用什么design patt ...

工厂模式么
回复

使用道具 举报

🔗
 楼主| flykite083 2018-1-28 10:18:08 | 只看该作者
全局:

嗯,我也这么觉得。想到过strategy模式,后来写了写觉得不对劲。
回复

使用道具 举报

🔗
AAS1 2018-1-28 10:28:59 | 只看该作者
全局:
请问这是哪家面试问的,有点狠。
回复

使用道具 举报

🔗
 楼主| flykite083 2018-1-28 10:46:25 | 只看该作者
全局:
AAS1 发表于 2018-1-28 10:28
请问这是哪家面试问的,有点狠。

听说是Houzz
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表