首页 > 系统相关 >LevelDB源码剖析(1) Arena内存管理

LevelDB源码剖析(1) Arena内存管理

时间:2022-11-27 01:56:31浏览次数:41  
标签:Arena LevelDB bytes char 源码 内存 分配 size

1. 背景

对于数据库来说,内存的分配非常重要,当我们使用C++默认的内存分配方式 malloc/free或者new/delete的时候,如果遇到很小的键值对时,每次调用的平均开销就会比较大,同时会产生很多内存碎片。
由于在MemTable中经常会遇到需要为较小键值对分配内存的原因,LevelDB在MemTable使用了自己的内存管理,其为每个MemTable都绑定了一个Arena来管理内存。

  • 在MemTable以外的地方都使用了malloc/free, 因为他们申请较大的内存或者并不会频繁的申请内存

2. 原理

2.1 内存分配

内存分配中有一种常见的思想,是使用new预分配一块比较大的内存,需要使用小内存的时候,就从这块大内存里继续分配,这种分配只需要移动指针并且更新变量即可,相比于调用malloc会更加高效。
Arena就是基于这种思想进行内存管理,如下图所示,Arena将内存分为多个块,其中块分为4KB的基本块和超过1KB的大块,当需要分配内存时,会首先使用当前预申请的基本块中的内存分配,当需要分配的内存大于当前基本块中的数据时,就会申请新的空间,此时Arena会进行判断,如果需要分配的内存大于1KB,则为这块内存单独申请空间。如果小于1KB,则申请一个新的基本块,并从新的基本块为其分配空间,此时旧的基本块不再使用,多出来的空间就会会被浪费。

2.2 内存销毁

Arena不支持单独释放某个块,只能销毁整个Arena。这个和他的使用方式有关,对于MemTable来说,对于内存只有插入键值对的操作,没有删除的操作,所以不需要释放某一块内存,而是当MemTable的数据都Dump到SSTable中的时候,才会对整个Arena进行释放。

3. 源码解析

3.1 Arena.h

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

  //加上=delete意味禁止编译器自动生成,Arena负责内存分配,每次使用都应该独立初始化,应当禁止发生拷贝
  Arena(const Arena&) = delete; 
  Arena& operator=(const Arena&) = delete;
  
  char* Allocate(size_t bytes);  // 请求分配bytes个字节的内存,返回分配到的内存的指针
  char* AllocateAligned(size_t bytes); //按照字节对齐来分配内存

  // 返回内存的使用量
  size_t MemoryUsage() const {
    return memory_usage_.load(std::memory_order_relaxed); //memory_order_relaxed是atomic的一种memory ordering
  }

 private:
  char* AllocateFallback(size_t bytes); //分配一块超出当前余量的内存
  char* AllocateNewBlock(size_t block_bytes); //申请一个新的块并分配内存

  char* alloc_ptr_; //指向当前块的第一个free字节
  size_t alloc_bytes_remaining_; //当前块的余量
  std::vector<char*> blocks_; //指向内存块的数组

  std::atomic<size_t> memory_usage_; //当前Arena的内存用量,其中atomic代表这是一个原子类型的变量
};

头文件通过注释可以获知每个函数以及成员变量的作用,特别的,其中memory_usage是一个原子类型的变量,而其获取这个变量的值的时候使用了memory_usage_.load(std::memory_order_relaxed), 是在获取的时候使用了一种ordering,让编译器优化生成的代码,从而提高性能。

3.2 Arena.cc

Allocate

Allocate是Arena暴露对外的内存分配入口,当需要分配内存时,首先检查申请量是否小于alloc_bytes_remaining_(即当前内存块剩余的内存),如果小于则直接分配,否则触发AllocateFallback

inline char* Arena::Allocate(size_t bytes) {
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}

AllocateFallback

AllocateFallback 是当前内存块不足时Arena执行的内存分配逻辑。

char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
	//当申请的内存超过块大小的1/4时,会为其单独分配一块内存,这是为了防止申请浪费过多的申请内存。毕竟如果直接申请新的内存块,原来内存块的剩余空间就浪费了,这保证了4KB的基本内存块最多只会浪费1KB的空间。
    char* result = AllocateNewBlock(bytes);
    return result;
  }
  //当申请的内存块小于1/4时,会重新申请一块新的4KB基本内存块并将其放入。
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize; //kBlokSize是一个全局静态常量,大小为4096,表示一个基本块的大小

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}

AllocateNewBlock

AllocateNewBlock负责通过new向内存申请一块新的空间,作为新的基本内存块或大块,其逻辑就是申请内存、指针加入blocks、增加统计指标、返回指针

char* Arena::AllocateNewBlock(size_t block_bytes) {
  char* result = new char[block_bytes];
  blocks_.push_back(result);
  memory_usage_.fetch_add(block_bytes + sizeof(char*),
                          std::memory_order_relaxed);
  return result;
}

AllocateAligned

AllocateAligned负责申请一块对齐的内存。

char* Arena::AllocateAligned(size_t bytes) {
  //align表示需要对齐的字节数,使用机器的void*的大小来对齐,最多8字节
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
  
  //判断对齐字节数是否是2的次幂,只有2的幂次的数据x & (x - 1)为0
  static_assert((align & (align - 1)) == 0,
                "Pointer size should be a power of 2");
  //这里用到了一个公式:x & (y - 1) = x % y
  //所以current_mod实际上是在计算alloc_ptr_与对齐字节的偏差量
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
  
  //在知道了偏差之后,slop指的是需要向后多申请多少个字节,needed指的是将多申请的字节与需要分配的字节加起来,就是实际需要分配的字节
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);
  size_t needed = bytes + slop;
  
  //最终只要按照needed来分配内存,并将返回的指针变为alloc_ptr_向后移动slop个字节的位置,就可以得到一块字节对齐的内存
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop;
    alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
    //因为AllocateFallback是需要申请一块新的内存(无论是基本块还是大块)来分配的,所以新申请的内存总是字节对齐的,就不用再使用slop和needed来做偏移了
    result = AllocateFallback(bytes);
  }
  
  //校验最终结果是否内存对齐
  assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
  return result;
}

标签:Arena,LevelDB,bytes,char,源码,内存,分配,size
From: https://www.cnblogs.com/Hugh-Locke/p/16928851.html

相关文章

  • LevelDB源码剖析(2) 编码与字符串
    1.背景编码指的是内存里的整数和字符串放到磁盘上的方式,其主要目的有两个对不定长整数以及字符串能够在读取的时候感知到已经读取完了整个值最大程度的节省在磁盘上占......
  • LevelDb基础原理(1) SSTable
    1.介绍1.1描述SSTable(SortedStringTable)是一个通常放在磁盘上的,排序的字符串表,用来高效存储大量的键值对数据,同时搭配上优化实现IO操作的高吞吐量.1.2背景......
  • easylogging++的那些事(四)源码分析(二)日志记录宏(一)CLOG宏(五)其他相关类
    目录el::base::Writer类el::base::NullWriter类el::LogMessage类el::Logger类isValidId接口flush接口initUnflushedCount接口el::base::MessageBuilder类成员变量成员函数......
  • springboot事务源码分析
    1、本次使用springboot框架分析事务源码2、打开spring-boot-autoconfigure查看spring.factories发现关于事务的自动配置包含:DataSourceTransactionManagerAutoConfigurati......
  • k8s源码分析6-kubectl功能和对象总结
    kubectl的职责主要的工作是处理用户提交的东西(包括,命令行参数,yaml文件等)然后其会把用户提交的这些东西组织成一个数据结构体然后把其发送给APIServerkubectl的代......
  • 实验三·bdev原理和源码分析
    任务配置bdev运行环境运行hello_bdev程序并分析源码通过bdev接口写入数据并读取Bdev是在物理设备上的进一步抽象,物理层有NVM、NVMeOF、CEPHRBD等,通过bdev向......
  • 实验四·blobstore原理和源码分析
    实验任务学习Blob基本原理完成hello_blob程序运行修改底层bdev为nvmeBlob构建在bdev之上,是对数据存储的进一步抽象,类似于文件,但是不具备文件POSIX接口,可近似按文件形......
  • easylogging++的那些事(四)源码分析(二)日志记录宏(一)CLOG宏(四)日志信息保存
    目录writer类的输出运算符writer类的流操控符el::base::MessageBuilder类CLOG宏接口调用流程图在上一篇中我们分析完了CLOG宏日志输出的流程,在结尾的时候我们提......
  • 在CentOS编译Git源码
    Git是一个免费的开源分布式版本控制系统,旨在处理从小到小到的所有内容具有速度和效率的超大型项目。Git易于学习,占用空间很小,性能快如闪电。它超越了Subversion,CVS,Per......
  • CentOS7源码安装Nginx1.22
    CentOS7源码安装Nginx1.22一、安装下载源码包:wgethttp://nginx.org/download/nginx-1.22.1.tar.gz安装依赖:yum-yinstallgccmakepcrepcre-developensslope......