leveldb源码解析1——内存管理类Arena

由于这是第一篇就介绍下levedb,这里引用下面这个链接的文章对其进行的介绍:

http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html

“说起LevelDb也许您不清楚,但是如果作为IT工程师,不知道下面两位大神级别的工程师,那您的领导估计会Hold不住了:Jeff Dean和Sanjay Ghemawat。这两位是Google公司重量级的工程师,为数甚少的Google Fellow之二。

  Jeff Dean其人:http://research.google.com/people/jeff/index.html,Google大规模分布式平台Bigtable和MapReduce主要设计和实现者。

  Sanjay Ghemawat其人:http://research.google.com/people/sanjay/index.html,Google大规模分布式平台GFS,Bigtable和MapReduce主要设计和实现工程师。

  LevelDb就是这两位大神级别的工程师发起的开源项目,简而言之,LevelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库。正像上面介绍的,这二位是Bigtable的设计和实现者,如果了解Bigtable的话,应该知道在这个影响深远的分布式存储系统中有两个核心的部分:Master Server和Tablet Server。其中Master Server做一些管理数据的存储以及分布式调度工作,实际的分布式数据存储以及读写操作是由Tablet Server完成的,而LevelDb则可以理解为一个简化版的Tablet Server。

  LevelDb有如下一些特点:

    首先,LevelDb是一个持久化存储的KV系统,和Redis这种内存型的KV系统不同,LevelDb不会像Redis一样狂吃内存,而是将大部分数据存储到磁盘上。

    其次,LevleDb在存储数据时,是根据记录的key值有序存储的,就是说相邻的key值在存储文件中是依次顺序存储的,而应用可以自定义key大小比较函数,LevleDb会按照用户定义的比较函数依序存储这些记录。

    再次,像大多数KV系统一样,LevelDb的操作接口很简单,基本操作包括写记录,读记录以及删除记录。也支持针对多条操作的原子批量操作。

    另外,LevelDb支持数据快照(snapshot)功能,使得读取操作不受写操作影响,可以在读操作过程中始终看到一致的数据。

  除此外,LevelDb还支持数据压缩等操作,这对于减小存储空间以及增快IO效率都有直接的帮助。

  LevelDb性能非常突出,官方网站报道其随机写性能达到40万条记录每秒,而随机读性能达到6万条记录每秒。总体来说,LevelDb的写操作要大大快于读操作,而顺序读写操作则大大快于随机读写操作。至于为何是这样,看了我们后续推出的LevelDb日知录,估计您会了解其内在原因。”

----------- 引用自http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html

膜拜完了大神之后,就可以学习大神,学习leveldb的源码可以了解这种K-V存储的实习思想,可以学习G公司大神的C++编码,其中还有很多大神的编程技巧和经验,另外还有很多非常实用的数据结构与算法(Skiplist、Bloomfilter、LRUCache......)。

我决定从最基本的模块开始看起比如Arena、Status、Slice、Hash、Cache、port_posix等,因为他们经常被其他类用到,打好基础之后看后面的比较trick的模块就是效率高多了。

好吧,从Arena开始,这是一个内存管理类,它用经典的预先分配的方式,来减少new的次数,使用2的指数对齐方便操作,虽然很简单但是蕴藏了一些不错的编程技巧,边看源码边讲吧:

class Arena {
 public:
  Arena();
  ~Arena();

  // Return a pointer to a newly allocated memory block of "bytes" bytes.
  char* Allocate(size_t bytes);

  // Allocate memory with the normal alignment guarantees provided by malloc
  char* AllocateAligned(size_t bytes);

  // Returns an estimate of the total memory usage of data allocated
  // by the arena (including space allocated but not yet used for user
  // allocations).
  size_t MemoryUsage() const {
    return blocks_memory_ + blocks_.capacity() * sizeof(char*);
  }
... ...

从这一段代码可以看出,Arena提供了3个公共接口,Allocate提供了普通的内存分配,AllocateAligned提供了类似于malloc的2的指数对齐的内存分配,MemoryUsage返回占用内存大小,包括管理实际block的指针,这些指针都是存储vector<char*> blocks_这个私有成员变量中,注意使用了capacity方法而不是size。

private:
  char* AllocateFallback(size_t bytes);
  char* AllocateNewBlock(size_t block_bytes);

  // Allocation state
  char* alloc_ptr_;
  size_t alloc_bytes_remaining_;

  // Array of new[] allocated memory blocks
  std::vector<char*> blocks_;

  // Bytes of memory in blocks allocated so far
  size_t blocks_memory_;

  // No copying allowed
  Arena(const Arena&);
  void operator=(const Arena&);
};
复制和赋值构造函数作为私有方法使得该类无法复制和赋值,可以用这篇 博客里面的一个宏 DISABLE_COPY_AND_ASSIGN(Arena),AllocateFallback被Allocate和AllocateAligned调用,而AllocateNewBlock被AllocatedFallback调用,它是最终调用new分配内存的函数,下面先介绍Allocate为例描述Allocate->AllocateFallback->AllocateNewBlock这条调用链,然后介绍AllocateAligned的对齐机制,AllocateAligned就是比Allocate多一部分对齐处理,其它一样。

inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don't need
  // them for our internal use).
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}

char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    // Object is more than a quarter of our block size.  Allocate it separately
    // to avoid wasting too much space in leftover bytes.
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // We waste the remaining space in the current block.
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}
alloc_ptr_是指向Arena中空闲缓存未被占用的位置,如果空闲缓存不够则调用AllocatedFallback。在AllocateFallback首先会判断需要的缓存大小是否超过kBlockSize的1/4,KBlockSize是个const static大小为4K,为什么是4K,要和内核的默认页面大小一致?关于为什么内核页面大小要4K这里 一大坨Linus的邮件还有这篇博客也介绍了;

如果大小超过1/4 kBLockSize则安装实际需要大小分配,否则分配kBlockSize分配,代码注释上说是为了避免浪费更多的剩余字节(为什么呢?);最后调用AllocateNewBlock,这个函数就是调用new分配内存,然后把指针push到block_中blablabla。

char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
  assert((align & (align-1)) == 0);   // Pointer size should be a power of 2

  //当align为power of 2时alloc_ptr_除以align的余数实际就是alloc_ptr_ & (align-1)
  //或者这样理解,alloc_ptr_最多只需要移动align-1个字节来到达align对齐,所以只需要移动alloc_ptr_ & (align-1)
  //个字节来保持align对齐(基于align是power of 2的特点),比如align=4时,align=0x100,(align-1)=0x011,
  //这样alloc_ptr_的3及大于3的bit为的数字可以不care(因为肯定是align=4的倍数),只有0-2位是我们关心的,通过
  //alloc_ptr_ & (align-1)则可以达到这个目的,其结果current_mod就是alloc_ptr_在align对齐时所处的相对位置,
  //最后通过(current_mod == 0 ? 0 : align - current_mod)就可以计算出align对齐需要移动的字节数。
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);

  size_t needed = bytes + slop;
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop;
    alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
    // AllocateFallback always returned aligned memory
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);
  return result;
}
AllocateAligned使用sizeof(void*)或者8字节对齐,它们都是2的指数,二进制表示是只有一个1(0x100、0x1000、0x100..000之类),你会发现有个规律,当align为2的指数倍时,任何整数比如num & align实际就是num % align,而且使用位操作显然比余除操作效率甩了几条街(除操作在cpu不知道占了多少级流水节点,即使考虑乱序或者forwarding也是开销大);代码中我加了详细的注释,slop就是还需要额外移动的字节数来保证对齐。

好吧最后一个图


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值