对小内存块快速有效的内存分配器

对小内存块快速有效的内存分配器

作者 znrobinson   翻译 郭世龙

 

介绍

     动态内存分配是件有趣的事。大多数人在调用malloc/free不会考虑发生的与之相关的代价。提到基于堆的内存分配,为了能够重新申请和重新使用这些内存,内存块的管理必须进行大量的薄记,而这中薄记是要花费CPU周期的。我们尝试写高效的代码时候,首要的规则是:尽量避免触及分配器。但是存在malloc/free的一个原因是,它像其他函数一样也是一个工具,只是它需要被适当的使用。但是您能低成本地使用它吗?

      很多人和我自己一样接手这个小小的挑战,以展示大量荷尔蒙驱动下的一个成熟男人所做的工作。目标是写一个快速的,最有效率的小内存块分配器并且有机会获得在被人面前滔滔不绝吹牛的权利。那么,好...这不是大男子气概的姿态,根本原因是我们工作的UI工具箱以惊人的速度分配了惊人的内存数量,其中包括非常多的小于1024byte的小内存请求。对于谁更牛一些,我们有着很绅士的不同意见。嗯...我的意思是谁能够写出跟好的分配器。既然竞争,那么基本的规则是:

       具有如下声明的两个函数:void* alloc(long size); void free(void *p);

       必须支持>1024的分配(速度和效率测试是基于<=1024的分配);

       必须是“自然的”对齐到下一个2的幂,最大8byte对齐;

       NULL释放时不能崩溃;

       必须支持0分配(返回一个有效的指针);

       必须调用blockalloc()/blockfree()(实质上是malloc和free)来构建池分配。

 

得分的主要点是速度和效率,通过在评估过程中你有多少的内存浪费来衡量效率的缺乏。

 

      效率=你的分配器请求的内存量/你从blockalloc请求的内存量

 

给定效率,得分基本计算如下:

       分数=时间(单位毫秒)/(效率×效率×效率)

 

得分最低者胜。从这里可以看出,不尽可能的高效则会有很大的惩罚。在我们的效果和效率测试中,我的分配器能够击败Visual C的malloc/free,速度上提高25倍效率提高13%。

 

 尽管这个实现相当的快速,但是不主张将这个建立为最快的可能的小块分配器。我有很多想法关于怎样使它更快并且我确信这之外还有实现能击败它。但是,有趣的是它击败微软的箱外(out-of-the-box)实现是多么的容易啊。并且对与有兴趣进一步开发它的你们,这是一个好的起点。

    一句很好的信条是使事情尽可能的简单,只在需要的时候才引入复杂。我的前两个分配器都是复杂的怪兽,最大的原因是我将焦点集中在了错误的问题上了(例如,最小化块头尺寸等等)。最后,我的分配器变成了相当简单本质上是一个固定块分配器,它管理者129个分离的堆,每个堆管理一个具体的固定分配大小从4byte开始然后是8byte然后以8byte增长到1024byte。技术上,这是一个子分配器;他使用malloc/free来分配和释放更大的内存块。这个内存块管理这些内存块然后用他们分配更小的内存块。在我们的测试中,这个分配器通过比一般目的的malloc/free更有效的管理这个更小的分配而获胜。

     固定块的分配器,就像它听起来的那样,是一个仅能分配一个固定或给定大小块的的分配器。由于只需要处理给定大小的块,代码量和需要管理内存的数据结构的复杂性就被最小化了;这直接映射到效果。

 

分配内存

 看一下rtAllocator::alloc 方法:

                               void* rtAllocator::alloc(long ls);

 

 这里首要的事情是你需要找出129个独立的堆中哪一个适合用来满足请求。首先检查分配的大小(ls)看看是否>1024。如果请求大于1024,那么这个请求就简单地扔个通用目的的分配器(malloc)。因为我不关心这样大小的内存分配。如果大小<=1024,你需要决定129个固定大小的堆中哪一个用来满足请求。为了快速高效的完成这个,需要用一个1024个元素的查找表。这个查找表用一个数来初始化,这个数指明了129个固定大小的堆中哪一个用来满足请求。看一下这个代码:

 

    void* rtAllocator::alloc(long ls)
{
   if (ls == 0) ls = 1; int bdIndex = -1;
   if (ls <= 1024) bdIndex = mBDIndexLookup[ls];

 

 第一行处理试图分配0byte的特例;在这种情况中,你处理它为分配1byte,通过改变大小为1来。在下一行,初始化索引bdIndex值为-1,并且如果分配在你的目标范围之内(<=1024),那么查找表用大小作为表的偏移量决定使用129个堆中的哪个。

 

  如果分配请求大于1024,index和bdIndex将被设为-1,并且这个请求被简单的传递给通用目的分配器,代码如下:

 

 if (bdIndex < 0)
{
   // Not handling blocks of this size throw to blockalloc
   INCALLOCCOUNTER(bdCount);
   return ALLOCJR_ALLOC(ls);
}
            注意:宏 ALLOCJR_ALLOC 用来包装malloc,这样你可以记录分配统计值。ALLOCJR_FREE用做同样的目的包装调用free函数。

 

 在代码中这一点上,你知道你正处理的分配大小(<=1024)并且你知道129个固定大小的堆中的哪个将被用来满足请求。因此,接下来的要做的事情是检查看看你是否已经有了一块必要的空间的内存块来满足请求。每一个堆都维护着一个空闲块的双向链表(内满足至少一次分配的块)。如果没有空闲块,你要分配一块(用malloc)并且把它链入到你的空闲块链表中,用如下代码:

  

if (!mFreeBlocks[bdIndex])
{
   INCBLOCKCOUNTER();
   block* b = (block*)ALLOCJR_ALLOC(
      block::getAllocSize(bd[bdIndex].fixedAllocSize,
      bd[bdIndex].chunks));
   if (b)
   {
      b->init(bd[bdIndex].fixedAllocSize, bdIndex, bd[bdIndex].chunks);

      addBlockToArray(b);
      mFreeBlocks[bdIndex] = b;
   }
}

在这一点上,应该至少有一个快可以提供被用来满足分配请求。这个分配请求被推送到block::alloc函数在可提供的空闲块中分配内存。每一个块都有很多大块,每一个大块都足够大到满足一次分配请求。在block数据结构中,在block中维护着一个空闲大块(chunck)的单链表。

   为了避免初始化个新分配的块是建立链表的成本,需要维护一个篱笆指针(fence pointer)并指向第一个未初始化的块。当分配内存时候,首先查看这个链表是否链表包含任何空闲大块。如果没有,你要通过检查篱笆指针(chence pointer)看看是否在块中——这个包含在空闲大块(chunk)链表中——有大块(chunk)。如果有空间,增加mInitCursor到下一个大块(chunk)并且使用以前被mInitCursor指向的大块。
 
inline void* alloc()
{
   void* result;

   if (mFreeChunk)
   {
      result = mFreeChunk;
      mFreeChunk = (void**)*mFreeChunk;
   }
   else
   {
      result = mInitCursor;
      mInitCursor += mFixedAllocSize;
   }

   mAllocCount++;
   return result;
}
从block::alloc返回后,你需要看看是否这个块是否完全满了,这个通过调用block::idFull来完成。如果满了,就从双向空闲链表中移除这个块,这样在以后寻找空闲空间满足请求时就不用考虑这个块了。当这个块从空闲链表移除时,一个哨兵只被赋值给block的mNextFreeBlock指针,这样你可以容易判断这个块满了。查看下面代码:
 
block *b = mFreeBlocks[bdIndex];

if (b->mNextFreeBlock != ALLOCJR_FULLBLOCK && b->isFull())
{
   // Unlink from freelist
   if (b->mNextFreeBlock)
   {
      b->mNextFreeBlock->mPrevFreeBlock = b->mPrevFreeBlock;
   }
   if (b->mPrevFreeBlock)
   {
      b->mPrevFreeBlock->mNextFreeBlock = b->mNextFreeBlock;
   }
   mFreeBlocks[bdIndex] = b->mNextFreeBlock;
   // special value means removed from free list
   b->mNextFreeBlock = ALLOCJR_FULLBLOCK;
   b->mPrevFreeBlock = ALLOCJR_FULLBLOCK;
}

在这一点,你已经成功的分配了一个请求大小的块。既然你已经走完了内存分配过程,现在我将带你看看内存释放过程。
 
释放内存
 
内存释放的过程开始于调用rtAllocator::free
 
     void rtAllocator::free( void* p)
 
这个函数首先要做的是检查是否你传递了一个空指针。如果这样,想如下一样返回:
 
    if (!p) return;
 
如果这个指针非空,需要做一个检查看看内存是否由你管理或者已经从传递到malloc返回。因此你要对你维护的块指针数组做二分查找来看看是否他是你不是你的指针。这通过调用如下函数实现:
 
block* b = findBlockInArray(p);
 
如果这个块是你的,b将是非空的;否则,你知道你正释放的指针不是你的并且你可以直接传递给free函数。如果它是你的,调用block::free函数来释放内存块。在block::free函数中,你只是将大块(chunk)返回给在快内的空闲链表这样大块(chunk)可以再从新利用。
 
inline void free(void* p)
{
   void **pp = (void**)p;
   *pp = mFreeChunk;
   mFreeChunk = (void**)p;
   mAllocCount--;
}
这个函数的第一行中,你将指针强制转换成void**;这将允许你很容易的将当前链表头指针写到大块(chunk)的前面。那么,这实际上是在第二行完成的。在第三行,通过设置头指针指向新插入的大块(chunk)来完成将大块(chuck)插入到空闲链表。这个函数中你要做的最后一件事情是减少记录当前块中被分配的大块(chunk)的计数器。一从block::free函数返回,你仍不得不做至少两个检查。首先,你要检查看看最后对block::free的调用是否置空了块。如下:
 
if (b->isEmpty())
 
如果这个现在块完全空了,那么你要从空闲块的双向链表移除这个快并且通过调用free函数将它归还给系统。如下:
 
if (b->isEmpty())
{
   // Unlink from freelist and return to the system
   if (b->mNextFreeBlock)
   {
      b->mNextFreeBlock->mPrevFreeBlock = b->mPrevFreeBlock;
   }
   if (b->mPrevFreeBlock)
   {
      b->mPrevFreeBlock->mNextFreeBlock = b->mNextFreeBlock;
   }
   if (mFreeBlocks[b->mBDIndex] == b)
      mFreeBlocks[b->mBDIndex] = b->mNextFreeBlock;

   removeBlockFromArray(b);

   DECBLOCKCOUNTER();
   ALLOCJR_FREE(b);
}
如果这个块不空,那么你要确信这个块被包含在双向空闲链表中,因为你现在知道至少在块中(block)至少有一个空闲大块(chunk)可用。这个检查通过比较mNextFreeBlock成员和一个无效指针常量ALLOCJR_FULLBLOCK来完成,无论什么时候块满了这个指针(ALLOCJR_FULLBLOCK)都会被复制给mNextFreeBlock。如果检查成功,你要把它在链接到链表中如下:
 
// need to see if block is not in free list; if not, add it back
if (b->mNextFreeBlock == ALLOCJR_FULLBLOCK)
{
   b->mPrevFreeBlock = NULL;
   b->mNextFreeBlock = mFreeBlocks[b->mBDIndex];
   if (mFreeBlocks[b->mBDIndex])
   mFreeBlocks[b->mBDIndex]->mPrevFreeBlock = b;
   mFreeBlocks[b->mBDIndex] = b;
}
到这,内存已经被回收能被再次利用了。
 
 
结论
 
总结我只想说在打造这个分配器的时候有很多乐趣并且我希望你能通过我的对它的描述享受的阅读。看着写代码多么有意思啊!
 
作者的 博客 有更多的编程tips、代码片段和软件。
 
 
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值