现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

Arena内存池简介

2020-01-09 06:42 工业·编程 ⁄ 共 3254字 ⁄ 字号 暂无评论

一. 什么是内存池?

内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。

通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

二. 内存池的优势

1. 直接使用系统调用的弊端

调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销。

频繁使用时会产生大量内存碎片,从而降低程序运行效率。

容易造成内存泄漏。

2. 内存池的优点

(1) 减少内存分配次数,集中一次释放。

(2) 比malloc/free进行内存申请/释放的方式快(向内存池请求,而不是直接向操作系统请求)

(3) 不会产生或很少产生堆碎片

(4) 可避免内存泄漏(直接通过内存池操作对象,无需关心释放单个对象,整块内存池一次释放)

三. Arena内存池

内存池很多地方都有用到,像linux内核也有个内存池。在leveldb中也有内存池解决方案,就是Arena内存池。下面主要讲解Arena内存池的原理。

Arena类采用vector来存储每次分配内存的指针,每一次分配的内存,我们称为一个块block。block默认大小为4096kb。

1. Arena类的成员变量

// Array of new[] allocated memory blocks

std::vector<char*> blocks_;

// Allocation state

char* alloc_ptr_;

size_t alloc_bytes_remaining_;

// Total memory usage of the arena.

port::AtomicPointer memory_usage_;

Arena类的成员变量还是相对比较简单的,blocks_是一个vector用来存储每一次向系统请求的分配的内存指针。alloc_ptr_表示当前内存块(block)偏移量指针,也就是未使用内存的首地址。 alloc_bytes_remaining_表示当前块所未使用的空间大小。 memory_usage_则是用来记录Arena类内存使用情况的。

2. Arena类的成员函数

a. 构造函数

Arena::Arena() : memory_usage_(0) {

  alloc_ptr_ = NULL;  // First allocation will allocate a block

  alloc_bytes_remaining_ = 0;

}

非常简单的一个构造函数,负责初始化一些变量,注意刚开始是没有立刻分配内存的

b. 析构函数

Arena::~Arena() {

  for (size_t i = 0; i < blocks_.size(); i++) {

    delete[] blocks_[i];

  }

}

析构函数负责将从系统中申请的内存返还给系统。

c. Allocate() && AllocateFallback() && AllocateNewBlock()

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;

}

char* Arena::AllocateNewBlock(size_t block_bytes) {

  char* result = new char[block_bytes];

  blocks_.push_back(result);

  memory_usage_.NoBarrier_Store(

      reinterpret_cast<void*>(MemoryUsage() + block_bytes + sizeof(char*)));

  return result;

}

Allocate是Arena向外界提供的接口,该函数会调用AllocateFallback() && AllocateNewBlock() 这两个私有函数。

如果需求的内存小于剩余的内存,那么直接从内存池中获取。

如果需求的内存大于剩余的内存,而且大于1k(4096/4),则给这个需求单独分配一块需求内存大小的内存。

如果需求的内存大于剩余的内存,而且小于1k,则重新分配一个内存块,默认大小4096,用于存储数据。原有的剩余空间浪费掉。

d. AllocateAligned()

Arena还提供了字节对齐内存分配,一般情况是8个字节对齐分配。对齐内存的好处简单的说就是加速内存访问。

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。

比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。

CPU一次访问时,要么读0x01~0x04,要么读0x05~0x08…硬件不支持一次访问就读到0x02~0x05

例:如果0x02~0x05存了一个int,读取这个int就需要先读0x01~0x04,留下0x02~0x04的内容,再读0x05~0x08,留下0x05的内容,两部分拼接起来才能得到那个int的值,这样读一个int就要两次内存访问,效率就低了。

如上图所示,第一种排列方式,如果short存储位置是在奇数位置,那么读取这个变量就要访问两次内存了。

e. MemoryUsage()

size_t MemoryUsage() const {

  return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load());

}

Arena最后一个对外接口是返回这个内存池分配总的内存大小。

给我留言

留言无头像?