TensorFlow 源码分析之内存管理BFC算法

2016-10-07 DeepReve

目录


       TensorFlow的内存分配器实现了一个叫做best-fitwith coalescingBFC)的算法,它本质上是Doug Lea's mallocdlmalloc)的一个非常简单的版本。

1 结构

1.1 Chunk

       整个内存空间由一个按基址升序排列的Chunk双向链表来表示,它们的直接前趋和后继必须在地址连续的内存空间。Chunk结构体里含有实际大小、请求大小、是否被占用、基址、直接前趋、直接后继、Bin索引等信息,如图1所示。 


1

1.2 Bin

       Bin是一个拥有相似大小的空闲Chunk的集合。集合里的Chunksize >= Bin size,并且Chunksize和基址升序排列。

       所有Bin由一个Bins集合管理,Bins的结构如图2所示。


2

1.3 AllocationRegion

       AllocationRegion给一个连续的内存区域做指针到ChunkHandle的映射。

1.4 RegionManager

       RegionManager聚集了一个或多个AllocationRegion,并提供一个从指针到基础ChunkHandle的间接层,这个间接层可在多个不连续的内存区域进行分配。

2 分配内存

2.1 调整大小

       将每次分配的内存大小调整为kMinAllocationSizeN倍,这样所有内存地址都是很好地按字节对齐了。


// file: ./core/common_runtime/bfc_allocator.h

228   size_t BFCAllocator::RoundedBytes(size_t bytes) {

229   size_t rounded_bytes =

230       (kMinAllocationSize *

231        ((bytes + kMinAllocationSize - 1) /kMinAllocationSize));

232   DCHECK_EQ(size_t{0}, rounded_bytes % kMinAllocationSize);

233   return rounded_bytes;

234 }


2.2 查找

2.2.1 查找Bin

       Bin size2562^N倍。372行先将分配内存的字节数右移8位,然后把结果用在373行,计算出的二进制有效位数即为BinBins中的索引。

Bin size2562^N倍。372行先将分配内存的字节数右移8位,然后把结果用在373行,计算出的二进制有效位数即为BinBins中的索引。


// file: ./core/common_runtime/bfc_allocator.cc

249   BinNum bin_num = BinNumForSize(rounded_bytes);

 

351   inlineint Log2FloorNonZero(uint64 n) {

352 #ifdefined(__GNUC__)

353     return63 ^ __builtin_clzll(n);

354 #else

355     int r = 0;

356     while (n > 0) {

357       r++;

358       n >>= 1;

359     }                                  

360     return r;                          

361 #endif

362   }

 

368   size_t BinNumToSize(BinNum index) {

369     returnstatic_cast<size_t>(256) << index;

370   } 

371   BinNum BinNumForSize(size_t bytes) {

372     uint64 v = std::max<size_t>(bytes, 256) >> kMinAllocationBits;

373     int b = std::min(kNumBins - 1, Log2FloorNonZero(v));

374     return b;

375   }


2.2.2 查找Chunk

1)加锁:


251   mutex_lock l(lock_);


2)从之前得到的Bin索引开始,查找合适的空闲Chunk


287       const BFCAllocator::ChunkHandle h = (*citer);

288       BFCAllocator::Chunk* chunk = ChunkFromHandle(h);


3)将找到的ChunkBin中移除:


293         RemoveFreeChunkIterFromBin(&b->free_chunks, citer);

 

439 void BFCAllocator::RemoveFreeChunkIterFromBin(

440     BFCAllocator::Bin::FreeChunkSet*free_chunks,

441     const BFCAllocator::Bin::FreeChunkSet::iterator& citer) {

442   ChunkHandle h = *citer;

443   Chunk* c = ChunkFromHandle(h);

444   CHECK(!c->in_use() &&(c->bin_num != kInvalidBinNum));

445   free_chunks->erase(citer);

446   c->bin_num = kInvalidBinNum;

447 }


4)拆分Chunk

       如果Chunk的大小大于等于申请内存大小的2倍,那么将该Chunk拆分成2个:第一个Chunk的大小等于申请内存大小,第二个Chunk作为它的直接后继。


300         if (chunk->size >= rounded_bytes * 2) {

301           SplitChunk(h, rounded_bytes);

302           chunk = ChunkFromHandle(h); 

303         }

 

332 void BFCAllocator::SplitChunk(BFCAllocator::ChunkHandle h, size_t num_bytes) {

      ...

340   BFCAllocator::Chunk* new_chunk = ChunkFromHandle(h_new_chunk);

341   new_chunk->ptr = static_cast<void*>(static_cast<char*>(c->ptr) + num_bytes);

      /* 保存新Chunk的基址到ChunkHandle的映射 */

342   region_manager_.set_handle(new_chunk->ptr, h_new_chunk);

      ...

351   //Maintain the pointers.

352   // c<-> c_neighbor becomes

353   // c<-> new_chunk <-> c_neighbor

354   BFCAllocator::ChunkHandle h_neighbor = c->next;

355   new_chunk->prev = h;

356   new_chunk->next = h_neighbor;

357   c->next = h_new_chunk;

358   if (h_neighbor != kInvalidChunkHandle) {

359     Chunk* c_neighbor = ChunkFromHandle(h_neighbor);

360     c_neighbor->prev = h_new_chunk;

361   }

364   InsertFreeChunkIntoBin(h_new_chunk);

365 }


5)修改Chunk的请求大小、分配ID(标记Chunk被占用):


307         chunk->requested_size = num_bytes;

310         chunk->allocation_id =next_allocation_id_++;


6)更新统计;

7)成功时返回找到的Chunk指针,失败时返回空指针。

2.2.3 扩展内存

       如果查找失败,那么扩展内存,然后再查找合适的空闲Chunk

1)已占用的加上申请的内存大小,超过最大内存限制时,返回失败;

2)循环将当前区域可分配的内存扩充1倍,直到大于等于申请的内存大小: 


 97   bool increased_allocation = false;

 98   while (rounded_bytes> curr_region_allocation_bytes_) {

 99     curr_region_allocation_bytes_ *= 2;

100     increased_allocation = true;

101   }


3)从内存池里分配内存,如果失败,尝试分配申请内存大小的90%。一直重复,直到分配成功或可用内存不足:


110     staticconstexprfloat kBackpedalFactor = 0.9;

112     // Tryallocating less memory.

113     bytes = RoundedBytes(bytes *kBackpedalFactor);

114     while (mem_addr == nullptr && bytes > rounded_bytes) {

115       mem_addr = suballocator_->Alloc(32, bytes);

116       bytes = RoundedBytes(bytes *kBackpedalFactor);

117     }

118   }


(4)给分配的内存添加AllocationRegion,然后创建一个空闲Chunk,插入Bin中:


138   region_manager_.AddAllocationRegion(mem_addr,bytes);

150   region_manager_.set_handle(c->ptr, h);

157   InsertFreeChunkIntoBin(h);


(5)再次查找Chunk

2.3 分配失败

    可用内存不足导致分配失败时,10秒后重试一次,然后返回结果。

3 回收内存

3.1 获取ChunkHandle

       先加锁,然后从RegionManager的指针到ChunkHandle的映射关系中得到ChunkHandle


377   mutex_lock l(lock_);

380   BFCAllocator::ChunkHandle h = region_manager_.get_handle(ptr);

381   CHECK(h != kInvalidChunkHandle);


3.2 更新状态

       Chunk标记为空闲,然后把总占用的内存量减去Chunk的大小。

3.3 合并Chunk

3.3.1 合并直接后继

       如果直接后继Chunk空闲,那么合并它们。

1)将直接后继从Bin中移除;

2)把它的直接后继作为当前Chunk的新直接后继;

3)修改它的直接后继的直接前趋;

4)将当前Chunk加上它的内存大小;

5)删除它。


405   // c1<-> c2 <-> c3 should become

406   // c1 <-> c3

481       RemoveFreeChunkFromBin(c->next);

482       Merge(h, ChunkFromHandle(h)->next);


3.3.2 合并直接前趋

       如果直接前趋Chunk空闲,那么合并它们。

1)将直接前趋从Bin中移除;

2)把当前Chunk的直接后继作为它的新直接后继;

3)修它的新直接后继的直接前趋;

4)将它加上当前Chunk的内存大小;

5)删除当前Chunk


497       RemoveFreeChunkFromBin(c->prev);

498       Merge(ChunkFromHandle(h)->prev, h);

499       c= ChunkFromHandle(h);


3.3.3 插入Bin

      将合并后的空闲Chunk插入Bin


503   InsertFreeChunkIntoBin(chunk_to_reassign);



关注 DeepReve & OpenAI 微信公众号,获取最新内容



推荐阅读

2017-08-21 DeepReve CS:自动瞄准与射击系统


© 2017 DeepReve. All rights reserved.