zoukankan      html  css  js  c++  java
  • 【LevelDB源码阅读】Cache

    是什么

    leveldb内部实现的缓存

    为什么要用

    • 性能高于内嵌哈希表

    学到什么

    • 与(&)操作进行模运算
    • 减少cache锁操作,可以分为多组cache

    源码分析

    LRUHandle

    缓存中代表键值对的数据结构

    // An entry is a variable length heap-allocated structure.  Entries
    // are kept in a circular doubly linked list ordered by access time.
    struct LRUHandle {
      void *value;  // 存储键值对的值,可以为任意类型
      void (*deleter)(const Slice &, void *value);  // 函数指针当前结构体的清除函数
      LRUHandle *next_hash;                         // 用户hash冲突使用
      LRUHandle *next;                              // 当前节点下一个节点
      LRUHandle *prev;                              // 当前节点上一个节点
      size_t charge;                                // 当前节点占用内存
      size_t key_length;                            // 当前节点键的长度
      bool in_cache;     // Whether entry is in the cache.
      uint32_t refs;     // References, including cache reference, if present.
      uint32_t hash;     // Hash of key(); used for fast sharding and comparisons
      char key_data[1];  // Beginning of key
    
      Slice key() const {
        // next_ is only equal to this if the LRU handle is the list head of an
        // empty list. List heads never have meaningful keys.
        assert(next != this);
    
        return Slice(key_data, key_length);
      }
    };
    

    HandleTable

    自定义的哈希表,首先看数据成员

      // The table consists of an array of buckets where each bucket is
      // a linked list of cache entries that hash into the bucket.
      uint32_t length_;  // hash表桶数组长度
      uint32_t elems_;   // 哈希表存储元素的数量
      LRUHandle **list_;  // 哈希数组指针,其中每个数组元素存储指针类型
    

    构造函数和析构函数

      HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }
      ~HandleTable() { delete[] list_; }
    

    主要接口函数

    • 查询
      LRUHandle* Lookup(const Slice &key, uint32_t hash) {
        return *FindPointer(key, hash);
      }
    

    其中FindPointer方法实现如下,首先确定在哈希数组的哪个哈希桶中,然后沿着哈希链表查找。
    如果某个LRUHandle* 存在相同hash和key值,则返回LRUHandle*的二级指针,即上一个LRUHandle*的next_hash的二级指针。
    如果不存在,则返回bucket中最后一个 LRUHandle*的next_hash,即为nullptr。

      // Return a pointer to slot that points to a cache entry that
      // matches key/hash.  If there is no such cache entry, return a
      // pointer to the trailing slot in the corresponding linked list.
      LRUHandle** FindPointer(const Slice &key, uint32_t hash) {
        LRUHandle **ptr = &list_[hash & (length_ - 1)];  // 下标结果是0到length-1
        while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
          ptr = &(*ptr)->next_hash;  // 迭代哈希链找到键值相等节点,ptr为上一节点next_hash值
        }
        return ptr;
      }
    
      LRUHandle* Insert(LRUHandle *h) {
        LRUHandle **ptr = FindPointer(h->key(), h->hash);
        LRUHandle *old = *ptr;
        h->next_hash = (old == nullptr ? nullptr : old->next_hash);
        *ptr = h;  //将ptr值也就是对应上一个LRUHandle的next_hash值赋值为h来跳过old
        if (old == nullptr) {  // h是新节点
          ++elems_;
          // Since each cache entry is fairly large, we aim for a small
          // average linked list length (<= 1).
          if (elems_ > length_) {
            Resize();
          }
        }
        return old;
      }
    

    其中Resize方法实现如下:

    1. 初始大小为4,每次以原有大小的2倍扩容
    2. 以新的长度创建哈希桶大小,并设置每个哈希桶为空
    3. 遍历原有每个哈希桶的每个元素,重新确定在新哈希桶的位置,采用头插法插入到新哈希桶的每个链表中
    4. delete原有哈希链表,并赋值为新的哈希链表
      void Resize() {
        uint32_t new_length = 4;
        while (new_length < elems_) {
          new_length *= 2;  // 每次2倍扩容
        }
        LRUHandle **new_list = new LRUHandle *[new_length];
        memset(new_list, 0, sizeof(new_list[0]) * new_length);  //一级指针置空
        uint32_t count = 0;
        for (uint32_t i = 0; i < length_; ++i) {
          LRUHandle *h = list_[i];
          while (h != nullptr) {
            LRUHandle *next = h->next_hash;  // 一级指针上二级指针通过next_hash连接
            uint32_t hash = h->hash;
            LRUHandle **ptr = &new_list[hash & (new_length - 1)];  // 确定在新桶中位置
            h->next_hash = *ptr;  // 第一次*ptr为nullptr,其它为上个循环h的地址
            *ptr = h;
            h = next;
            ++count;
          }
        }
        assert(elems_ == count);
        delete[] list_;
        list_ = new_list;
        length_ = new_length;
      }
    
    • 删除
      LRUHandle* Remove(const Slice &key, uint32_t hash) {
        LRUHandle **ptr = FindPointer(key, hash);
        LRUHandle *result = *ptr;
        if (result != nullptr) {  // 找到节点
          *ptr = result->next_hash;
          --elems_;
        }
        return result;
      }
    

    LRUCache

    数据成员如下包括:

    • cache总大小
    • cache已经使用大小
    • 冷链表(lru_),即在cache中,但没有被外部引用,cache容量满时会被淘汰
    • 热链表(in_use_),即在cache中,同时又被外部引用,cache容量满时也不会被淘汰
    • 哈希表(table_),用于快速查找(链表结构则用于快速插入和删除)
      // Initialized before use.
      size_t capacity_;  // cache总容量
    
      // mutex_ protects the following state.
      mutable port::Mutex mutex_;
      size_t usage_ GUARDED_BY(mutex_);  // 已经使用的cache空间
    
      // Dummy head of LRU list.
      // lru.prev is newest entry, lru.next is oldest entry.
      // Entries have refs==1 and in_cache==true.
      LRUHandle lru_ GUARDED_BY(mutex_);  // 双向循环链表
    
      // Dummy head of in-use list.
      // Entries are in use by clients, and have refs >= 2 and in_cache==true.
      LRUHandle in_use_ GUARDED_BY(mutex_);
    
      HandleTable table_ GUARDED_BY(mutex_);  // 二级指针数组,用于缓存数据
    

    构造函数和析构函数
    构造函数,初始化所有数据成员,其中哈希执行默认初始化。

    LRUCache::LRUCache() : capacity_(0), usage_(0) {
      // Make empty circular linked lists.
      lru_.next = &lru_;
      lru_.prev = &lru_;
      in_use_.next = &in_use_;
      in_use_.prev = &in_use_;
    }
    

    执行析构函数时,热链表中不应该有元素,函数体主要删除冷链表中元素。

    LRUCache::~LRUCache() {
      assert(in_use_.next == &in_use_);  // Error if caller has an unreleased handle
      for (LRUHandle *e = lru_.next; e != &lru_;) {
        LRUHandle *next = e->next;
        assert(e->in_cache);
        e->in_cache = false;
        assert(e->refs == 1);  // Invariant of lru_ list.
        Unref(e);
        e = next;
      }
    }
    

    主要对外接口

    • 查找

    根据key和hash值在哈希表中进行查找,查找到后更新热链表。

    Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {
      MutexLock l(&mutex_);
      LRUHandle* e = table_.Lookup(key, hash);
      if (e != nullptr) {
        Ref(e);
      }
      return reinterpret_cast<Cache::Handle*>(e);
    }
    
    • 插入

    首先申请LRUHandle内存并且初始化,LRUHandle对象指针会返回给调用方,所以refs此时为1,然后添加到in_use_链表尾部,refs此时为2,同时更新in_cache=true和cache使用量。如果
    插入到哈希表table_中已经存在,则会将原有节点从cache中删除。最后如果容量超限,从lru_进行淘汰并从哈希表中删除。

    Cache::Handle* LRUCache::Insert(const Slice& key, uint32_t hash, void* value,
                                    size_t charge,
                                    void (*deleter)(const Slice& key,
                                                    void* value)) {
      MutexLock l(&mutex_);
    
      LRUHandle* e =
          reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
      e->value = value;
      e->deleter = deleter;
      e->charge = charge;
      e->key_length = key.size();
      e->hash = hash;
      e->in_cache = false;
      e->refs = 1;  // for the returned handle.
      std::memcpy(e->key_data, key.data(), key.size());
    
      if (capacity_ > 0) {
        e->refs++;  // for the cache's reference.
        e->in_cache = true;
        LRU_Append(&in_use_, e);
        usage_ += charge;
        FinishErase(table_.Insert(e));  // 插入到哈希表中同时清理旧节点
      } else {  // don't cache. (capacity_==0 is supported and turns off caching.)
        // next is read by key() in an assert, so it must be initialized
        e->next = nullptr;
      }
      while (usage_ > capacity_ && lru_.next != &lru_) {
        LRUHandle* old = lru_.next;
        assert(old->refs == 1);
        bool erased = FinishErase(table_.Remove(old->key(), old->hash));
        if (!erased) {  // to avoid unused variable when compiled NDEBUG
          assert(erased);
        }
      }
    
      return reinterpret_cast<Cache::Handle*>(e);
    }
    
    • 删除

    从哈希表中删除,然后从cache链表删除。

    void LRUCache::Erase(const Slice& key, uint32_t hash) {
      MutexLock l(&mutex_);
      FinishErase(table_.Remove(key, hash));
    }
    
    • 其它辅助方法

    Ref方法表示要使用该cache,如果元素位于冷链表中,需要移到热链表。

    void LRUCache::Ref(LRUHandle* e) {
      if (e->refs == 1 && e->in_cache) {  // If on lru_ list, move to in_use_ list.
        LRU_Remove(e);
        LRU_Append(&in_use_, e);
      }
      e->refs++;
    }
    

    Unref正好相反,表示用户不再使用该元素,需要将引用计数--,如果彻底没有用户使用,即引用计数为0,则删除这个元素,如果引用计数为1,则从热链表移到冷链表中。

    void LRUCache::Unref(LRUHandle* e) {
      assert(e->refs > 0);
      e->refs--;
      if (e->refs == 0) {  // Deallocate.
        assert(!e->in_cache);
        (*e->deleter)(e->key(), e->value);
        free(e);
      } else if (e->in_cache && e->refs == 1) {
        // No longer in use; move to lru_ list.
        LRU_Remove(e);
        LRU_Append(&lru_, e);
      }
    }
    

    其中从链表删除元素和将元素插入指定链表如下:

    void LRUCache::LRU_Remove(LRUHandle* e) {
      e->next->prev = e->prev;
      e->prev->next = e->next;
    }
    
    void LRUCache::LRU_Append(LRUHandle* list, LRUHandle* e) {
      // Make "e" newest entry by inserting just before *list
      e->next = list;
      e->prev = list->prev;
      e->prev->next = e;
      e->next->prev = e;
    }
    

    SharedLRUCache

    为什么有SharedLRUCache?
    LRUCache的接口都会加锁,为了更少的锁持有时间以及更高的缓存命中率,可以定义多个LRUCache,分别处理不同hash取模后的缓存处理。SharedLRUCache是LRUCache上的一层封装,管理16个cache,对外提供服务,外部调用接口都委托内部某个LRUCache。

    数据成员

      LRUCache shard_[kNumShards];  // 16个LRUCache
    

    SharedLRUCache的主要作用就是计算Hash值,选择LRUCache,代码如下:

      static inline uint32_t HashSlice(const Slice &s) {
        return Hash(s.data(), s.size(), 0);
      }
    
      static uint32_t Shard(uint32_t hash) {  // 得到shard_数组下标
        return hash >> (32 - kNumShardBits);  // 取hash高4位得到shard_数组下标
      }
    

    主要对外接口
    这些接口都是先计算hash值,然后根据hash值选择LRUCache,最后通过调用LRUCache来实现。

    • 插入
      Handle* Insert(const Slice &key, void *value, size_t charge,
                     void (*deleter)(const Slice &key, void *value)) override {
        const uint32_t hash = HashSlice(key);
        return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
      }
    
    • 查找
      Handle* Lookup(const Slice &key) override {
        const uint32_t hash = HashSlice(key);
        return shard_[Shard(hash)].Lookup(key, hash);
      }
    
    • 删除
      void Erase(const Slice &key) override {
        const uint32_t hash = HashSlice(key);
        shard_[Shard(hash)].Erase(key, hash);
      }
    
    • 查看缓存使用大小
      size_t TotalCharge() const override {
        size_t total = 0;
        for (int s = 0; s < kNumShards; s++) {
          total += shard_[s].TotalCharge();
        }
        return total;
      }
    
  • 相关阅读:
    一文让你彻底了解卷积神经网络
    处理 3d 视频的简单理论基础
    ffmpeg 学习:000-概述和库的组成
    Linux 补丁 的简单使用: 制作补丁,打补丁,撤销补丁
    “反向传播算法”过程及公式推导
    机器学习:梯度下降算法原理讲解
    神经网络浅讲:从神经元到深度学习
    Git详解之安装
    Git详解之Git起步
    Git详解之协议/速度/安全
  • 原文地址:https://www.cnblogs.com/galaxy-hao/p/13123673.html
Copyright © 2011-2022 走看看