levelDB之内存碎片和Arena

什么是内存碎片?

在内存单元100的起始地址到内存单元200之间,一共申请了100块1字节的区域,但是释放的时候,先释放了内存地址为基数的单元,如释放101、103…而偶数单元不释放,释放50次后,虽然还有50字节的内存是空余的,但是如果下次要申请2字节的内存单元,是无法在100到200之间申请到的,因为这个区域没有连续的2字节空间,这就是内存碎片。

内存碎片分为两种:内部碎片和外部碎片

(1)内部碎片

内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

(2)外部碎片

外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。

(3)如何避免内存碎片

1、少用动态内存分配的函数(尽量使用栈空间)

2、分配内存和释放的内存尽量在同一个函数中

3、尽量一次性申请较大的内存2的指数次幂大小的内存空间,而不要反复申请小内存(少进行内存的分割)

4、使用内存池来减少使用堆内存引起的内存碎片

 

为了提升效率和不浪费资源,LevelDB中自然而然使用到了内存池技术,具体实现是类Arena。

Arena:

类的属性和方法如下:

 

实际上总体来说思路如下:

1、先判断当前分配内存块中剩余的内存是否有足够的内存去分配新的内存;如果足以容纳,则直接从当前内存块中提取内存作为所需内存使用。若是不足以容纳,见2。

2、先判断需要申请的内存是否大于1K,若是大于1K,直接使用系统的new操作符向系统申请足够的内存以供使用。若是小于1K,见3。

3、先直接使用系统new操作符向系统申请4k的内存作为新的内存块(此时旧的内存块中即使还有内存未使用,也不会再拿来使用,因为alloc_ptr_会指向新申请的内存块)。然后再向新申请的内存块提取内存以供使用。

这里Level对于小于1K的内存申请才向内存池提取内存,主要是连续多次申请小的内存会容易导致内存碎片,影响系统的性能。并且多次的new和delete比较耗时(不断的构造和析构),会付出额外的空间和时间。

 

整体的代码实现实际上很简单,唯一需要注意的函数是内存对齐函数AllocateAligned,其实现用了一定的技巧,受益匪浅。

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 (4 & 3 = 0) (8 & 7 = 0) (6 & 5 != 0) 
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);  //相当于A % B 前提条件是B为2的幂次。
  size_t slop = (current_mod == 0 ? 0 : align - current_mod); //判断还差多少字节才能align字节对齐
  size_t needed = bytes + slop;  //位移slop字节对齐后还需要分配bytes字节,所以总共需要bytes + slop字节的空间
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop; //此时返回的result由于位移了slop,达到了字节对齐的目的
    alloc_ptr_ += needed; //alloc_ptr_由于分配出去了needed字节的空间,需要更新到未分配的首地址
    alloc_bytes_remaining_ -= needed;
  } else {
    // AllocateFallback always returned aligned memory 和系统的new特性有关?其分配的内存一定是内存对齐的
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);
  return result;
}

那么为什么需要内存对齐?

我们知道计算机中内存是以字节为单位划分的,CPU通过地址总线来访问内存,CPU一个时钟周期内能处理多少字节的数据,就命令地址总线读取几个字节的数据。举个例子:32位的CPU,一次能处理32bit的数据,也就是4字节的数据,那么CPU就命令地址总线一次性读取4字节的数据,即每次的步长都为4字节,只对地址是4的整倍数的地址进行寻址,比如:0,4,8,100等进行寻址。对于程序来说,一个变量的地址最好刚在一个寻址步长内,这样一次寻址就可以读取到该变量的值,如果变量跨步长存储,就需要寻址两次甚至多次然后再进行拼接才能获取到变量的值,效率明显就低了,所以编译器会进行内存对齐,以保证寻址效率。

32位CPU为例,寻址步长为4,程序中如果一个int变量的地址为8,那么一次寻址就可以拿到该变量的值,如果int变量的地址为10,那么需要先寻址地址为8的地址拿到数据的一部分,再寻址12的地址拿到另一部分,然后再进行拼接

同时也因为平台原因,不是所有的硬件平台都能访问任意地址上的数据,不对齐可能会抛出硬件异常。

总结起来如下两点:

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

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.
  size_t MemoryUsage() const {
    return memory_usage_.load(std::memory_order_relaxed);
  }

 private:
 	//z 如果剩余空间不足于容纳,那么根据bytes大小决定是否重新分配一块标准大小的内存(4096),要是bytes大于1024,直接  
    //z 分配其大小的内存,原标准块内存仍旧有用。  
    //z 如果小于 1024 ,说明原标准块剩余内存不足以容纳1024,新分配一块标准块内存,用此来为bytes分配对应内存。
  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_;

  // Total memory usage of the arena.
  //
  // TODO(costan): This member is accessed via atomics, but the others are
  //               accessed without any locking. Is this OK?
  std::atomic<size_t> memory_usage_;

  // No copying allowed
  Arena(const Arena&);
  void operator=(const Arena&);
};

源文件:

static const int kBlockSize = 4096;

Arena::Arena() : memory_usage_(0) {
  alloc_ptr_ = nullptr;  // First allocation will allocate a block
  alloc_bytes_remaining_ = 0;
}

Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }
}

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;
}

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 (4 & 3 = 0) (8 & 7 = 0) (6 & 5 != 0) 
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);  //相当于A % B 前提条件是B为2的幂次。
  size_t slop = (current_mod == 0 ? 0 : align - current_mod); //判断还差多少字节才能align字节对齐
  size_t needed = bytes + slop;  //位移slop字节对齐后还需要分配bytes字节,所以总共需要bytes + slop字节的空间
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop; //此时返回的result由于位移了slop,达到了字节对齐的目的
    alloc_ptr_ += needed; //alloc_ptr_由于分配出去了needed字节的空间,需要更新到未分配的首地址
    alloc_bytes_remaining_ -= needed;
  } else {
    // AllocateFallback always returned aligned memory 和系统的new特性有关?其分配的内存一定是内存对齐的
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);
  return result;
}

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;
}

 

帮助博客:

https://www.cnblogs.com/418ks/p/10801954.html

https://blog.youkuaiyun.com/qq_43461641/article/details/102807052

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值