浮萍晓生的开发日志

记录学习的旅程,把握可预见的未来

导航

STL内存池学习

Posted on 2014-03-20 13:21  浮萍晓生  阅读(495)  评论(0)    收藏  举报

最近开始学习内存池技术,《高质量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一样,直接执行后续操作。
      能说的就这么一点了。