动态内存分配DSA
DSA:Dynamic Storage Allocation,用于动态管理程序运行时所需的内存。动态内存分配涉及在程序运行时根据需要分配和释放内存,以存储数据结构和数据。
- 内存管理方式:动态内存分配与静态内存分配相对应,静态内存分配是在程序编译时为变量分配固定大小的空间,而动态内存分配是在程序运行时根据需要动态调整内存空间。
- 内存分配函数:重新语言通常提供内置的内存分配函数,如C/C++中的malloc、calloc和C++中new,用于在堆中分配内存。这些函数返回一个指向分配内存首地址的指针。
- 内存释放:动态分配的内存必须在使用完后进行释放,以防止内存泄漏。释放内存的函数是C/C++中的free和C++中的delete。不释放已分配的内存会导致程序内存占用增加,最终可能导致系统性能下降。
- 内存泄漏:如果分配的内存在使用完后没有被释放,就会发生内存泄漏。这会导致内存逐渐耗尽,最终可能导致程序崩溃或系统不稳定。
- 碎片问题:频繁的动态内存分配和释放可能导致外碎片增加。
- 动态数据结构:DSA允许在运行时创建动态数据结构,如链表、树和图、这些数据结构的大小和形状可以根据程序的需求动态变化。
- 性能开销:与静态内存分配相比,动态内存分配需要更多的系统开销,包括内存分配表的维护、碎片整理等。因此,在某些情况下,需要权衡是否使用动态内存分配。
- 错误处理:动态内存分配可能因为内存不足或其它原因失败。程序应该对分配失败进行适当的错误处理,避免崩溃或不可预料的行为。
DSA的衡量指标
- 快速响应时间:当申请分配size大小的内存时,算法耗费多少时间查找到合适内存块,越短越好。
- 有界响应时间:内存分配最坏的响应时间,比如某空闲list算法,在一些情况下直到遍历到最后一个节点,才能发现合适的内存块,比正常找到慢很多。这种边界性超长响应对一些实时要求高的系统或软件并不友好。
- 高效内存使用:设计内存碎片化的处理。比如内存池总共有1000个字节内存,运行到一段时间后,500字节占用,500字节空闲,但占用与空闲的内存刚好单字节一一交错。这时虽然内存里还有500字节,但一个连续的2字节的内存都无法分配。
常见的分配策略
- sequential fit:最基础的算法,空闲链表法,所有内存块都放到一个单向/双向list中,查找时会有边界响应问题。
- segregated fit:对sequentical fit进行改进,对所有内存块按其大小区间放到不同list中,这些list首地址组成array,查找速度更快,dlmalloc算法使用该策略。
- Buddy system:对segregated fit算法的改进,更好的切割和合并,分配时效不错但内部碎片化问题比较严重。典型算法策略Binary Buddies,Fibonacci Buddies, Weighted Buddies,Double Buddies。
- Indexed Fit:基于使用高级结构索引空闲内存块。典型算法策略:基于平衡树的“Best Fit”,基于笛卡尔树存储的Stephenson’s Fast-Fit等。在一些情况下,比Segregated系列效果更好。
- Bitmap Fit:位图法,算是Indexed Fit算法的改进,使用小段内存表示的位图来确认内存块的占用或空闲,其通过降低缓存未命中的概率来提高响应时间。
TLSF算法
TLSF(Two-Level Segregated Fit),两级隔离Fit内存分配器,是一款通用的动态内存分配,专门用于实时要求。
其有以下特别:
- 算法复杂度为O(1)
- 每次分配的开销极低(4字节)
- 低碎片化
- 主要采用两级位图(Two-Level Bitmap)与分级空闲块链表(Segregated Free List)的数据结构管理动态内存池(memory pool)以及其中的空闲块(free blocks),用Good-Fit的策略进行分配。
分级空闲块链表(Segregated Free List)
分级空闲块链表(Segregated Free List)的设计思想是将空闲块按照大小分级,形成了不同块大小范围的分级,组成空闲块用链表链接起来。
每次分配时先按分级大小范围查找到相应链表,再从相应链表挨个检索合适的空闲块,如果找不到,就在大小范围更大的一级查找,直到找到合适的块分配出去。
两级位图(Two-Level Bitmap)
使用位图的优势:
- 节省存储空闲,用1-bt表示某个区间范围大小的内存块是否存在。
- 位操作速度快,部分体系结构有加速特殊位操作的指令(如clz,ffs,fls)
TLSF采用了两级位图(Two-Level Bitmap)来管理不同大小范围的空闲块链(free blocks lists)。上图中包含三个虚线矩形框分别是:
- 第一级位图(First-Level Bitmap),表示内存块的粗粒度范围,一般是2的幂次粒度(例如2n~2n+1)。
- 第二季位图(Second-Level Bitmap)是一个数组,一级位图中的每一位,对应这个数组的一项,表示内存块的细粒度范围(例如(2n + 02 n-2~ 2n+12n-2))。
- 第三个框是内存中真正的空闲内存块(free blocks)
Best-fit(内部碎片最优化)
常规思想是:找到能满足内存请求大小的最小空闲块,就会有下面的流程(以搜索大小为69字节的空闲块为例)
- 基于位运算找到请求大小所在的第一级位图对应的粗粒度范围(64~128)。
- 在粗粒度范围内,根据二级位图索引检索第二级位图得到细粒度范围(68~70).
- 如上图所示,沿着右下角空闲块链表可以检索到69字节的那一块是Best-fit。
Best-fit策略最主要的问题还在于第三步,仍然需要检索对应范围的那一条空闲块链表,存在潜在的时间复杂度。
Good-fit
Good-fit思路与Best-fit不同之处在于,Good-fit并不保证找到满足需求的最小空闲块,而是尽可能接近要分配的大小。
还以上述搜索大小为69字节的空闲块为例,Good-fit并不是找到[6870]这一范围,而是比这个范围稍微大一点儿的范围,例如[7173],这样设计的好处就是对应的空闲块链中每一块都能满足需求,不需要检索空闲块链表找到最小的,而是直接取空闲块链中第一块即可。整体上还不会造成太多碎片。