最近开始学习内存池技术,《高质量c++/c编程指南》在内存管理的第一句话就是:欢迎进入内存这片雷区,由此可见掌握内存管理对于c++/c程序员的重要性。使用内存池的优点有:降低动态申请内存的次数,提升系统性能,减少内存碎片,增加内存空间使用率。
内存池的分类:
一、不定长内存池:优点:不需要为不同的数据创建不同的内存池,缺点是分配出去的内存池不能回收到池中(?)。代表有apr_pool,obstack。
二、定长内存池:优点:使用完立即把内存归还池中。代表有Loki, Boost。
本次以sgi stl中实现的内存池作为学习对象。由于要实现的是一个C语言的内存池,所以这里用C的描述方式。喜欢C++的朋友可以直接看源文件或者《STL源码剖析》的讲解。sgi设计了二级配置机制,第一级配置器直接使用malloc()和free()。当配置区块超过128 bytes时,则采用第一级配置器;否则采用memory pool方式。
memory pool的整体思想是维护128/8 = 16个自由链表,这里的8是小型区块的上调边界。每个自由链表串接的区块大小如下:
| 序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 串接区块 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 |
| 范围 | 1-8 | 9-16 | 17-24 | 25-32 | 33-40 | 41-48 | 49-56 | 57-64 | 65-72 | 73-80 | 81-88 | 89-96 | 97-104 | 105-112 | 113-120 | 121-128 |
几个过程的思路:
一、申请过程:
if (用户申请的内存不大于128 bytes) 查找对应的链表 if (对应链表拥有一块以上的区块) 调整链表 返回第一个区块地址 else 准备重新填充链表 向内存池申请内存(指定数量的区块) if (内存池申请到一个区块) 返回第一个区块地址 else 调整链表,将区块串接起来 返回第一个区块地址 else 直接用malloc()申请内存
二、释放过程:
if (用户释放的内存块大于128 bytes) 直接用free()释放 else 查找对应的链表 回收内存
三、向内存池申请内存过程:
if (内存池空间完全满足需求量) 调整内存池起始位置 返回空间地址 else if (内存池空间不能完全满足需求量,但能提供一个以上的区块) 计算能够提供的最大内存 调整内存池起始位置 返回空间地址 else 从内存池中压缩内存 收集比size大的空间 递归调用,修正nobjs 再次申请内存,可能抛出异常
这一节学习基础知识:所用到的数据结构。首先看内存池的接口:
#ifndef _MEM_POOL_H #define _MEM_POOL_H static size_t __freelist_index(size_t bytes); static size_t __round_up(size_t bytes); static void *mem_malloc(size_t n); static void mem_free(void *p, size_t n); static void *mem_realloc(void* ptr, size_t new_sz, size_t old_sz) static void *refill(size_t n); static char *chunk_alloc(size_t size, int *nobjs); #endif
用到的常量与变量:
#define __MAX_BYTES 128 /* 小型区块的上限 */ #define __ALIGN 8 /* 小型区块的上调边界 */ #define __NFREELISTS __MAX_BYTES / __ALIGN /* 链表个数 */ obj *free_list[__NFREELISTS]; /* 自由链表 */ static char *start_free; /* 内存池起始位置 */ static char *end_free; /* 内存池结束位置 */ static size_t heap_size; /* 内存池大小 */
这里值得一提的是共用体obj,定义如下:
typedef union obj { union obj *free_list_link; char client_data[1]; }obj;
一般情况下我们构建单链表时需要创建如下的一个结构体:
struct obj {
obj *next; /* 指向下一个这样的结构体 */
char *p; /* 指向真正可用空间 */
int size; /* 记录空间的大小 */
};
用户申请12字节的空间时使用时如下方式:
obj *pj = (obj *)malloc(12 + sizeof(struct obj));
pj->next = NULL;
pj->p = (char*)p + sizeof(struct obj);
pj->size = 12;
但是采用这种方式有个缺点就是我们需要花费额外的开销(记录指向下一个结点的指针和大小),我们可以通过直接定位链表在free_list数组中的位置来减掉 size 的开销,因为 free_list[0] 指向的是8 bytes的区块,free_list[1] 指向的是16 bytes的区块……。但仍需承担 next 和 p 指针的开销。
当我们采用
union obj {
union obj *free_list_link;
char client_data[1];
};
时,sizeof(obj)的大小为4,当然我们更不需负担这4个字节的开销。因为我们可以充分利用union的特性——同一时刻只存在一个变量。当我们构建空闲链表时,我们通过free_list_link指向下一个obj,当把这个obj分配出去的时候,我们直接返回client_data的地址。这样我们就不会在用户申请的空间上添加任何东西,达到了一物二用的结果。
接下来看几个简单的函数:
static size_t __freelist_index(size_t bytes) { return (bytes + __ALIGN - 1) / __ALIGN - 1; } static size_t __round_up(size_t bytes) { return (bytes + __ALIGN - 1) & ~(__ALIGN - 1); }
__freelist_index的作用是函数根据区块的大小,决定使用第n号free-list。n从0算起。__round_up用于将bytes上调至 __ALIGN 的倍数。
接下来要进入几个主要的函数学习了。
今天进入主要函数的学习。首先看用户申请内存函数mem_malloc:
static void *mem_malloc(size_t n) { obj * volatile *my_free_list; obj *result; /* 如果申请的内存超过我们设置的最大内存,直接使用crt */ if (n > (size_t)__MAX_BYTES) { void *p = (void *)malloc(n); return p; } /* 寻找16个free list链表中适当的一个 */ my_free_list = free_list + __freelist_index(n); result = *my_free_list; if (result == 0) { /* 当没有可用的free list时,准备重新填充free list */ void *ret = refill(__round_up(n)); return ret; } /* 调整链表 */ *my_free_list = result->free_list_link; return result; }
这里需要说明的是在开始的时候,为了提高效率,内存分配及填充free list会推迟到用户申请内存的时候,其他没什么说的。接着看释放内存函数mem_free:
static void mem_free(void *p, size_t n) { obj *q = (obj*)p; obj * volatile *my_free_list; /* 大于__MAX_BYTES直接释放 */ if (n > (size_t) __MAX_BYTES) { free(p); return; } /* 找到适当的链表,回收区块并调整链表 */ my_free_list = free_list + __freelist_index(n); q->free_list_link = *my_free_list; *my_free_list = q; }
释放过程也没什么可说的,接着看填充链表函数refill:
static void *refill(size_t n) { int nobjs = 20; char *chunk = chunk_alloc(n, &nobjs); obj * volatile *my_free_list; obj *result; obj *current_obj, *next_obj; int i; if (nobjs == 1) return chunk; my_free_list = free_list + __freelist_index(n); result = (obj *)chunk;/* 这个区块返回给用户 */ /* free list指向新配置的空间(取自内存池)*/ *my_free_list = next_obj = (obj *)(chunk + n); /* 以下将各个节点串接起来 */ for (i = 1; ; i++) {/* 从1开始,因为第0块返回给用户 */ current_obj = next_obj; next_obj = (obj *)((char *)next_obj + n); if (nobjs - 1 == i) {/* 最后一个节点*/ current_obj->free_list_link = NULL; break; } else { current_obj->free_list_link = next_obj; } } return result; }
当用户调用申请mem_malloc发现找到的对应链表指针指向的NULL时,将会调用refill,refill会调用chunk_alloc从内存池‘要’一定数量的用户所申请区块大小的内存。如果此时内存池返回1个区块,那么直接返回给用户,完成申请过程,否者将除去返给用户的区块外,剩下内存串接起来以备下次使用。接下来看内存池函数chunk_alloc,她的工作是从内存池中取空间给free list用:
static char *chunk_alloc(size_t size, int *nobjs) { char *result; size_t total_bytes = size * (*nobjs); /* 要申请的内存总量 */ size_t byte_left = end_free - start_free; /* 内存池剩余空间 */ if (byte_left >= total_bytes) { /* 内存池中的内存完全满足需要 */ result = start_free; start_free += total_bytes; return result; } else if (byte_left >= size) { /* 内存池不能完全满足需求,但能提供一块以上的区块 */ *nobjs = byte_left / size; total_bytes = size * (*nobjs); result = start_free; start_free += total_bytes; return result; } else { /* 内存池剩余空间连一个区块的大小都无法提供 */ size_t bytes_to_get = + __round_up(heap_size >> 4); /* 将内存池的零头配给适当的free list */ if (byte_left > 0) { obj * volatile * my_free_list = free_list + __freelist_index(byte_left); ((obj*)start_free)->free_list_link = *my_free_list; *my_free_list = (obj *)start_free; } /* 配置heap空间,用来补充内存池 */ start_free = (char *)malloc(bytes_to_get); if (0 == start_free) { /* heap 空间不足,malloc()失败 */ size_t i; obj * volatile *my_free_list; obj *p; /* 搜寻适当的(比当前size大)free list */ for (i = size; i <= __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + __freelist_index(i); p = *my_free_list; if (0 != p) { /* free list 内尚有未用区块 */ /* 调整free list以释放出未用区块 */ *my_free_list = p->free_list_link; start_free = (char *)p; end_free = start_free + i; return chunk_alloc(size, nobjs); /* 递归调用,为了修正nobjs */ /* ps:任何残余零头最终会编入适当free list中备用 */ } } end_free = 0; start_free = (char*)malloc(bytes_to_get); } /* 记录当前内存容量 */ heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return chunk_alloc(size,nobjs); } }
chunk_alloc的作用是从内存池中取内存给free list,所以会竭尽全力满足用户需要。如果有足够的空间,会返回指定数量的区块,否者返回实际能提供数量的区块,或者无法满足用户需要,那么会将内存池剩下的零头编入适当的链表中,然后向系统索要空间来补充内存池。如果系统也无内存可用,则寻找free list中是否有比size大的区块,将其用来补充空间。
如果到处都已无内存可用了,会再次配置空间,可能抛出异常。否则记录内存池大小。接下来我们看内存池的使用。
补上mem_realloc函数:
static void *mem_realloc(void *ptr, size_t new_sz, size_t old_sz) { void *result; size_t copy_sz; /* 如果old_sz 和 new_sz均大于我们设置的最大内存,那么直接用 crt */ if (old_sz > (size_t)__MAX_BYTES && new_sz > (size_t)__MAX_BYTES) return realloc(ptr, new_sz); if (__round_up(old_sz) == __round_up(new_sz)) return ptr; result = mem_malloc(new_sz); copy_sz = new_sz > old_sz ? old_sz : new_sz; memcpy(result, ptr, copy_sz); mem_free(ptr, old_sz); return result; }
mem_realloc的作用是替换是替换realloc函数,这里要结合上下文来理解,如果old_sz 和 new_sz均大于我们设置的最大内存,那么直接用 crt。那么假设old_sz < __MAX_BYTES = 128呢?这里分两种情况考虑:
1、new_sz > __MAX_BYTES:显然内存池不能满足需要,可为什么程序还要执行这句:result = mem_malloc(new_sz)呢,这里别忘了mem_malloc对超过__MAX_BYTES的内存会使用crt
2、new_sz <= __MAX_BYTES:
a、__round_up(old_sz) == __round_up(new_sz):直接返回ptr。这里可能存在的一点疑惑的是,假设old_sz = 1, new_sz = 5,那么直接返回的话不是没有满足用户需要吗?其实不然,原因是我们使用mem_malloc(old_sz)时分配的内存是8啊!
b、__round_up(old_sz) != __round_up(new_sz):这种情况直接执行后续操作——分配内存,将原有内容复制到新内存,释放旧内存。
如果 old_sz > __MAX_BYTES 且 new_sz < __MAX_BYTES呢?很简单,如2.b一样,直接执行后续操作。
能说的就这么一点了。
浙公网安备 33010602011771号