笔记,《转》

C语言中,static的字面意思很容易把我们导入歧途,其实它的作用有三条。

(1)先来介绍它的第一条也是最重要的一条:隐藏。

当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是a.c,另一个是main.c。

下面是a.c的内容

char a = 'A'; // global variable
void msg() 
{
    printf("Hello\n"); 
}

下面是main.c的内容

int main(void)
{    
    extern char a;    // extern variable must be declared before use
    printf("%c ", a);
    (void)msg();
    return 0;
}

程序的运行结果是:

A Hello

你可能会问:为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。

如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。Static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏,而对于变量,static还有下面两个作用。

(2)static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见,但我还是举一个例子。

#include <stdio.h>

int fun(void){
    static int count = 10;    // 事实上此赋值语句从来没有执行过
    return count--;
}

int count = 1;

int main(void)
{    
    printf("global\t\tlocal static\n");
    for(; count <= 10; ++count)
        printf("%d\t\t%d\n", count, fun());    
    
    return 0;
}

程序的运行结果是:

global          local static

1               10

2               9

3               8

4               7

5               6

6               5

7               4

8               3

9               2

10              1

(3)static的第三个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加’\0’太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是’\0’。不妨做个小实验验证一下。

#include <stdio.h>

int a;

int main(void)
{
    int i;
    static char str[10];

    printf("integer: %d;  string: (begin)%s(end)", a, str);

    return 0;
}

程序的运行结果如下

integer: 0; string: (begin)(end)

最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0。

posted @ 2012-05-16 09:08 笑笑小白 阅读(5) 评论(0) 编辑

数组指针 和指针数组的区别

数组指针是指向数组的,
如:   int   a[3][3],(*p)[3];
        p=a;
这里(*p)[3]用于告诉编译系统,它是一个指针,指向一个长度为3的整型数组。这样在用指针访问其所指向的内存单元的时候就可以用*(*(p+i)+j)来表示a[i][j];
若:int   a[3][3],*p;
        p=a;
就需用:*(p+3*i+j)来表示a[i][j].
指针数组是这样一种特殊的数组:它的每一个数组元素都是一个指针。
如:int   *p[3];
*p[0],*p[1],*p[2]都是一个指针。

int   (*p)[10];
定义了一个数组指针,这个指针与一般的指针没有什么区别,仅仅是这个指针指向一个数组。这里我们把数组作为了基本的元素处理。也就是说,将整个数组作为一种类型,而数组名就是这个类型的一个具体变量。例如:
int   a[10];
一个数组类型,形状就是这样:int   [10];a就是新定义的一个变量。
int   b[3];
一个数组类型,形状就是这样:int   [3];b就是新定义的一个变量。
因为这两个类型形状不一样,因此是两个不同的类型,因此a,b就是不同类型的变量。这就好比int   a和double   b   :a和b不一样。不知道大家是否已经对数组类型有了基本的印象?
那么把数组名作为该数组类型的一个具体变量,我们就可以定义指向这个变量的指针,即数组指针。
对于数组类型:int   [10],我们可以定义一个指针,int   (*p)   [10].注意这里一定要加上小括弧。否则就会变成了指针数组。定义了指针之后,我们可以对该指针赋值,如p=&a;如果定义了一个二维数组,int   c[3][10]。我们可以认为定义了一个一维的数组,这个数组有三个int[10]的元素。因此和一般的数组一样,我们可以将该数组名赋给指针,其实也就是第一个元素的地址付给指针。即:   p=c;或者p=&c[0]

posted @ 2012-05-16 08:55 笑笑小白 阅读(9) 评论(0) 编辑

2012年5月9日

new的三种形态

C++语言一直被认为是复杂编程语言中的杰出代表之一,不仅仅是因为其繁缛的语法规则,还因为其晦涩的术语。下面要讲的就是你的老熟人—new:

它是一个内存管理的操作符,能够从堆中划分一块区域,自动调用构造函数,动态地创建某种特定类型的数据,最后返回该区域的指针。该数据使用完后,应调用delete运算符,释放动态申请的这块内存。

如果这就是你对new的所有认识,那么我不得不说,你依旧被new的和善外表所蒙蔽着。看似简单的new其实有着三种不同的外衣。

是的,你没有看错,也不用感到惊奇,一个简单的new确实有三种不同的形态,它扮演着三种不同的角色,如下所示:

  1. new operator  
  2. operator new  
  3. placement new 

下面的代码片段展示的是我们印象中熟悉的那个new:

  1. string *pStr = new string("Memory Management");  
  2. int *pInt = new int(2011); 

这里所使用的new是它的第一种形态new operator。它与sizeof有几分类似,它是语言内建的,不能重载,也不能改变其行为,无论何时何地它所做的有且只有以下三件事,如图3-2所示。

 
图3-2 new operator所完成的三件事


所以当写出“string *pStr = new string("Memory Management");”代码时,它其实做的就是以下几件事:

  1. //为string对象分配raw内存  
  2. void *memory = operator new( sizeof(string) );  
  3. //调用构造函数,初始化内存中的对象  
  4. call string::string()on memory;  
  5. //获得对象指针  
  6. string *pStr = static_cast<string*>(memory);  
  7. 当然,对于内置类型,第二步是被忽略的,即:  
  8. //为int分配raw内存  
  9. void *memory = operator new( sizeof(int) );  
  10. //获得对象指针  
  11. int *pInt = static_cast<int*>(memory); 

其实new operator背后还藏着一个秘密,即它在执行过程中,与其余的两种形态都发生了密切的关系:第一步的内存申请是通过operator new完成的;而在第二步中,关于调用什么构造函数,则由new的另外一种形态placement new来决定的。

对于new的第二种形态—内存申请中所调用的operator new,它只是一个长着“明星脸”的普通运算符,具有和加减乘除操作符一样的地位,因此它也是可以重载的。

operator new在默认情况下首先会调用分配内存的代码,尝试从堆上得到一段空间,同时它对事情的结果做了最充分的准备:如果成功则直接返回;否则,就转而去调用一个new_hander,然后继续重复前面过程,直到异常抛出为止。所以如果operator new要返回,必须满足以下条件之一:

内存成功分配。

抛出bad_alloc异常。

通常,operator new函数通过以下方式进行声明:

  1. void* operator new(size_t size); 

注意,这个函数的返回值类型是void*,因为这个函数返回的是一个未经处理的指针,是一块未初始化的内存,它像极了C库中的malloc函数。如果你对这个过程不满意,那么可以通过重载operator new来进行必要的干预。例如:

  1. class A  
  2. {  
  3. public:  
  4.      A(int a);  
  5.      ~A();  
  6.      void* operator new(size_t size);  
  7.  ...  
  8. };  
  9. void* A::operator new(size_t size)  
  10. {  
  11.  cout<<"Our operator new...");  
  12.  return ::operator new(size);  

这里的operator new调用了全局的new来进行内存分配(::operator new(size))。当然这里的全局new也是可以重载的,但是在全局空间中重载void * operator new(size_t size)函数将会改变所有默认的operator new的行为方式,所以必须十二分的注意。还有一点需要注意的是,正像new与delete一一对应一样,operator new和operator delete也是一一对应的;如果重载了operator new,那么也得重载对应的operator delete。

最后,要介绍的是new的第三种形态—placement new。正如前面所说的那样,placement new是用来实现定位构造的,可以通过它来选择合适的构造函数。虽然通常情况下,构造函数是由编译器自动调用的,但是不排除你有时确实想直接手动调用,比如对未初始化的内存进行处理,获取想要的对象,此时就得求助于一个叫做placement new的特殊的operator new了:

  1. #include <new> 
  2. #include "ClassA.h"  
  3. int main()  
  4. {  
  5.      void *s = operator new(sizeof(A));  
  6.  A* p = (A*)s;  
  7.  new(p) A(2011);  //p->A::A(2011);  
  8.  ... // processing code  
  9.      return 0;  

placement new是标准C++库的一部分,被声明在了头文件中,所以只有包含了这个文件,我们才能使用它。它在文件中的函数定义很简单,如下所示:

  1. #ifndef __PLACEMENT_NEW_INLINE  
  2. #define __PLACEMENT_NEW_INLINE  
  3. inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()  
  4. {   // construct array with placement at _Where  
  5.     return (_Where);  
  6. }  
  7.  
  8. inline void __CRTDECL operator delete(void *, void *) _THROW0()  
  9. {   // delete if placement new fails  
  10. }  
  11. #endif /* __PLACEMENT_NEW_INLINE */ 

这就是placement new需要完成的事。细心的你可能会发现,placement new的定义与operator new声明之间的区别:placement new的定义多一个void*参数。使用它有一个前提,就是已经获得了指向内存的指针,因为只有这样我们才知道该把placement new初始化完成的对象放在哪里。

在使用placement new的过程中,我们看到的却是"new(p) A(2011)"这样奇怪的调用形式,它在特定的内存地址上用特定的构造函数实现了构造一个对象的功能,A(2011)就是对构造函数A(int a)的显式调用。当然,如果显式地调用placement new,那么也得本着负责任的态度显式地调用与之对应的placement delete:p->~A();。这部分工作本来可以由编译器独自完成的:在使用new operator的时候,编译器会自动生成调用placement new的代码,相应的,在调用delete operator时同样会生成调用析构函数的代码。所以,除非特别必要,不要直接使用placement new。但是要清楚,它是new operator的一个不可或缺的步骤。当默认的new operator对内存的管理不能满足我们的需要,希望自己手动管理内存时,placement new就变得有用了。就像STL中的allocator一样,它借助placement new来实现更灵活有效的内存管理。

最后,总结一下:

如果是在堆上建立对象,那么应该使用 new operator,它会为你提供最为周全的服务。

如果仅仅是分配内存,那么应该调用operator new,但初始化不在它的工作职责之内。如果你对默认的内存分配过程不满意,想单独定制,重载operator new 是不二选择。

如果想在一块已经获得的内存里建立一个对象,那就应该用placement new。但是通常情况下不建议使用,除非是在某些对时间要求非常高的应用中,因为相对于其他两个步骤,选择合适的构造函数完成对象初始化是一个时间相对较长的过程。

请记住:

不要自信地认为自己对new很熟悉,要正确区分new所具有的三种不同形态,并能在合适的情形下选择合适的形态,以满足特定需求。

posted @ 2012-05-09 16:49 笑笑小白 阅读(13) 评论(0) 编辑

转C++内存池实现

#pragma once
#ifndef _MEMORY_POOL_ 
#define _MEMORY_POOL_
#include <list>
#include <Windows.h>
using std::list;

template <typename Type>
class MemoryPool
{
private:
int m_nMaxCount;
int m_nFreeCount;
list <Type*> m_pMemList;
Type * m_pType;
CRITICAL_SECTION m_csMemLock;
public:
MemoryPool(int nMax);
~MemoryPool();
Type* New();
void Delete(Type* p);
int GetFreeCount();
bool IsFull();
};
template <typename Type>
MemoryPool<Type>::MemoryPool(int nMax)
{
m_pType=NULL;
m_pType=new (std::nothrow) Type[nMax];
if (m_pType!=NULL)
{
for (int i = 0; i < nMax; ++i)
{
m_pMemList.push_back(&m_pType[i]);
}
m_nMaxCount=nMax;
m_nFreeCount=m_nMaxCount;
}
else
{
m_nMaxCount=0;
m_nFreeCount=0;
}
InitializeCriticalSection(&m_csMemLock);
}

template <typename Type>
inline MemoryPool <Type>::~MemoryPool()
{
delete[] m_pType;
DeleteCriticalSection(&m_csMemLock);
}

template <typename Type>
Type* MemoryPool <Type>::New()
{

Type* pNew=NULL;
if (m_pType != NULL && m_nFreeCount > 0)
{
EnterCriticalSection(&m_csMemLock);
pNew=m_pMemList.front();
m_pMemList.pop_front();
LeaveCriticalSection(&m_csMemLock);
--m_nFreeCount;
}
return pNew;
}

template <typename Type>
void MemoryPool <Type>::Delete(Type* p)
{
bool bIsValaidPointer = false;
for (int i = 0;i < m_nMaxCount;++i)
{
if (&m_pType[i] == p)
{
//判断p是否是内存池中的内存指针,防止传入其他外部自己new的内存在内部释放
bIsValaidPointer=true;
}
}
list<Type*>::iterator iter;
for (iter = m_pMemList.begin();iter != m_pMemList.end();++iter)
{
if (*iter==p)
{
//判断p是否已经被释放过了,防止对同一指针多次释放
bIsValaidPointer = false;
}
}
if (p != NULL && m_pType != NULL && m_nFreeCount < m_nMaxCount && bIsValaidPointer)
{
EnterCriticalSection(&m_csMemLock);
m_pMemList.push_front(p);
LeaveCriticalSection(&m_csMemLock);
++m_nFreeCount;
}
}
template <typename Type>
inline int MemoryPool<Type>::GetFreeCount() //获取剩余容量
{
return m_nFreeCount;
}
template <typename Type>
inline bool MemoryPool <Type>::IsFull()
{
return m_nFreeCount == 0?true::false;
}

#endif

posted @ 2012-05-09 14:52 笑笑小白 阅读(19) 评论(0) 编辑

转:自定义内存池的使用

6.1 自定义内存池性能优化的原理

图书信息   书名:《C++应用程序性能优化》 
作者:冯宏华、徐莹、程远、汪磊 等编著 
出版社:电子工业出版社 
出版日期:2007 年 03 月

如前所述,读者已经了解到"堆"和"栈"的区别。而在编程实践中,不可避免地要大量用到堆上的内存。例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从内存堆上分配或者释放一定的内存;在维护一个动态数组时,如果动态数组的大小不能满足程序需要时,也要在内存堆上分配新的内存空间。

6.1.1 默认内存管理函数的不足

利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。

系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。

默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。

可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。

默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。

6.1.2 内存池的定义和分类

自定义内存池的思想通过这个"池"字表露无疑,应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的分配和释放则可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控之中。

应用程序自定义的内存池根据不同的适用场景又有不同的类型。

从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。

从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。

6.1.3 内存池工作原理示例

下面以固定内存池为例说明内存池的工作原理,如图6-1所示。


图6-1 固定内存池
图6-1  固定内存池 
 

固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元。

如图6-1所示,该内存池一共包含4个内存块。在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存的不断需求,内存池判断需要动态扩大时,才再次向系统申请新的内存块,并把所有这些内存块通过指针链接起来。对于操作系统来说,它已经为该应用程序分配了4个等大小的内存块。由于是大小固定的,所以分配的速度比较快;而对于应用程序来说,其内存池开辟了一定大小,内存池内部却还有剩余的空间。

例如放大来看第4个内存块,其中包含一部分内存池块头信息和3个大小相等的内存池单元。单元1和单元3是空闲的,单元2已经分配。当应用程序需要通过该内存池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位到还有空闲单元的那个内存池块。然后根据该块的块头信息直接定位到第1个空闲的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元为空闲单元即可。

可见与系统管理内存相比,内存池的操作非常迅速,它在性能优化方面的优点主要如下。

(1)针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空闲表的额外开销,从而获得较高的性能。

(2)由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性,提升了程序性能。

(3)比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。

6.2 一个内存池的实现实例

本节分析在某个大型应用程序实际应用到的一个内存池实现,并详细讲解其使用方法与工作原理。这是一个应用于单线程环境且分配单元大小固定的内存池,一般用来为执行时会动态频繁地创建且可能会被多次创建的类对象或者结构体分配内存。

本节首先讲解该内存池的数据结构声明及图示,接着描述其原理及行为特征。然后逐一讲解实现细节,最后介绍如何在实际程序中应用此内存池,并与使用普通内存函数申请内存的程序性能作比较。

6.2.1 内部构造

内存池类MemoryPool的声明如下:

class MemoryPool
{
private:
    MemoryBlock*   pBlock;
    USHORT          nUnitSize;
    USHORT          nInitSize;
    USHORT          nGrowSize;

public:
                     MemoryPool( USHORT nUnitSize,
                                  USHORT nInitSize = 1024,
                                  USHORT nGrowSize = 256 );
                    ~MemoryPool();

    void*           Alloc();
    void            Free( void* p );
};


 

MemoryBlock为内存池中附着在真正用来为内存请求分配内存的内存块头部的结构体,它描述了与之联系的内存块的使用信息:

struct MemoryBlock
{
    USHORT          nSize;
    USHORT          nFree;
    USHORT          nFirst;
    USHORT          nDummyAlign1;
    MemoryBlock*  pNext;
    char            aData[1];

	static void* operator new(size_t, USHORT nTypes, USHORT nUnitSize)
	{
		return ::operator new(sizeof(MemoryBlock) + nTypes * nUnitSize);
	}
	static void  operator delete(void *p, size_t)
	{
		::operator delete (p);
	}

	MemoryBlock (USHORT nTypes = 1, USHORT nUnitSize = 0);
	~MemoryBlock() {}
};


 

此内存池的数据结构如图6-2所示。


图6-2 内存池的数据结构
图6-2  内存池的数据结构 
 

6.2.2 总体机制

此内存池的总体机制如下。

(1)在运行过程中,MemoryPool内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个MemoryBlock结构体和多个可供分配的内存单元组成,所有内存块组成了一个内存块链表,MemoryPool的pBlock是这个链表的头。对每个内存块,都可以通过其头部的MemoryBlock结构体的pNext成员访问紧跟在其后面的那个内存块。

(2)每个内存块由两部分组成,即一个MemoryBlock结构体和多个内存分配单元。这些内存分配单元大小固定(由MemoryPool的nUnitSize表示),MemoryBlock结构体并不维护那些已经分配的单元的信息;相反,它只维护没有分配的自由分配单元的信息。它有两个成员比较重要:nFree和nFirst。nFree记录这个内存块中还有多少个自由分配单元,而nFirst则记录下一个可供分配的单元的编号。每一个自由分配单元的头两个字节(即一个USHORT型值)记录了紧跟它之后的下一个自由分配单元的编号,这样,通过利用每个自由分配单元的头两个字节,一个MemoryBlock中的所有自由分配单元被链接起来。

(3)当有新的内存请求到来时,MemoryPool会通过pBlock遍历MemoryBlock链表,直到找到某个MemoryBlock所在的内存块,其中还有自由分配单元(通过检测MemoryBlock结构体的nFree成员是否大于0)。如果找到这样的内存块,取得其MemoryBlock的nFirst值(此为该内存块中第1个可供分配的自由单元的编号)。然后根据这个编号定位到该自由分配单元的起始位置(因为所有分配单元大小固定,因此每个分配单元的起始位置都可以通过编号分配单元大小来偏移定位),这个位置就是用来满足此次内存申请请求的内存的起始地址。但在返回这个地址前,需要首先将该位置开始的头两个字节的值(这两个字节值记录其之后的下一个自由分配单元的编号)赋给本内存块的MemoryBlock的nFirst成员。这样下一次的请求就会用这个编号对应的内存单元来满足,同时将此内存块的MemoryBlock的nFree递减1,然后才将刚才定位到的内存单元的起始位置作为此次内存请求的返回地址返回给调用者。

(4)如果从现有的内存块中找不到一个自由的内存分配单元(当第1次请求内存,以及现有的所有内存块中的所有内存分配单元都已经被分配时会发生这种情形),MemoryPool就会从进程堆中申请一个内存块(这个内存块包括一个MemoryBlock结构体,及紧邻其后的多个内存分配单元,假设内存分配单元的个数为n,n可以取值MemoryPool中的nInitSize或者nGrowSize),申请完后,并不会立刻将其中的一个分配单元分配出去,而是需要首先初始化这个内存块。初始化的操作包括设置MemoryBlock的nSize为所有内存分配单元的大小(注意,并不包括MemoryBlock结构体的大小)、nFree为n-1(注意,这里是n-1而不是n,因为此次新内存块就是为了满足一次新的内存请求而申请的,马上就会分配一块自由存储单元出去,如果设为n-1,分配一个自由存储单元后无须再将n递减1),nFirst为1(已经知道nFirst为下一个可以分配的自由存储单元的编号。为1的原因与nFree为n-1相同,即立即会将编号为0的自由分配单元分配出去。现在设为1,其后不用修改nFirst的值),MemoryBlock的构造需要做更重要的事情,即将编号为0的分配单元之后的所有自由分配单元链接起来。如前所述,每个自由分配单元的头两个字节用来存储下一个自由分配单元的编号。另外,因为每个分配单元大小固定,所以可以通过其编号和单元大小(MemoryPool的nUnitSize成员)的乘积作为偏移值进行定位。现在唯一的问题是定位从哪个地址开始?答案是MemoryBlock的aData[1]成员开始。因为aData[1]实际上是属于MemoryBlock结构体的(MemoryBlock结构体的最后一个字节),所以实质上,MemoryBlock结构体的最后一个字节也用做被分配出去的分配单元的一部分。因为整个内存块由MemoryBlock结构体和整数个分配单元组成,这意味着内存块的最后一个字节会被浪费,这个字节在图6-2中用位于两个内存的最后部分的浓黑背景的小块标识。确定了分配单元的起始位置后,将自由分配单元链接起来的工作就很容易了。即从aData位置开始,每隔nUnitSize大小取其头两个字节,记录其之后的自由分配单元的编号。因为刚开始所有分配单元都是自由的,所以这个编号就是自身编号加1,即位置上紧跟其后的单元的编号。初始化后,将此内存块的第1个分配单元的起始地址返回,已经知道这个地址就是aData。

(5)当某个被分配的单元因为delete需要回收时,该单元并不会返回给进程堆,而是返回给MemoryPool。返回时,MemoryPool能够知道该单元的起始地址。这时,MemoryPool开始遍历其所维护的内存块链表,判断该单元的起始地址是否落在某个内存块的地址范围内。如果不在所有内存地址范围内,则这个被回收的单元不属于这个MemoryPool;如果在某个内存块的地址范围内,那么它会将这个刚刚回收的分配单元加到这个内存块的MemoryBlock所维护的自由分配单元链表的头部,同时将其nFree值递增1。回收后,考虑到资源的有效利用及后续操作的性能,内存池的操作会继续判断:如果此内存块的所有分配单元都是自由的,那么这个内存块就会从MemoryPool中被移出并作为一个整体返回给进程堆;如果该内存块中还有非自由分配单元,这时不能将此内存块返回给进程堆。但是因为刚刚有一个分配单元返回给了这个内存块,即这个内存块有自由分配单元可供下次分配,因此它会被移到MemoryPool维护的内存块的头部。这样下次的内存请求到来,MemoryPool遍历其内存块链表以寻找自由分配单元时,第1次寻找就会找到这个内存块。因为这个内存块确实有自由分配单元,这样可以减少MemoryPool的遍历次数。

综上所述,每个内存池(MemoryPool)维护一个内存块链表(单链表),每个内存块由一个维护该内存块信息的块头结构(MemoryBlock)和多个分配单元组成,块头结构MemoryBlock则进一步维护一个该内存块的所有自由分配单元组成的"链表"。这个链表不是通过"指向下一个自由分配单元的指针"链接起来的,而是通过"下一个自由分配单元的编号"链接起来,这个编号值存储在该自由分配单元的头两个字节中。另外,第1个自由分配单元的起始位置并不是MemoryBlock结构体"后面的"第1个地址位置,而是MemoryBlock结构体"内部"的最后一个字节aData(也可能不是最后一个,因为考虑到字节对齐的问题),即分配单元实际上往前面错了一位。又因为MemoryBlock结构体后面的空间刚好是分配单元的整数倍,这样依次错位下去,内存块的最后一个字节实际没有被利用。这么做的一个原因也是考虑到不同平台的移植问题,因为不同平台的对齐方式可能不尽相同。即当申请MemoryBlock大小内存时,可能会返回比其所有成员大小总和还要大一些的内存。最后的几个字节是为了"补齐",而使得aData成为第1个分配单元的起始位置,这样在对齐方式不同的各种平台上都可以工作。

6.2.3 细节剖析

有了上述的总体印象后,本节来仔细剖析其实现细节。

(1)MemoryPool的构造如下:

MemoryPool::MemoryPool( USHORT _nUnitSize,
                            USHORT _nInitSize, USHORT _nGrowSize )
{
    pBlock      = NULL;	            ①
    nInitSize   = _nInitSize;       ②
    nGrowSize   = _nGrowSize;       ③

    if ( _nUnitSize > 4 )
        nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT-1)) & ~(MEMPOOL_ALIGNMENT-1); ④
    else if ( _nUnitSize <= 2 )
        nUnitSize = 2;              ⑤
    else
        nUnitSize = 4;
}


 

从①处可以看出,MemoryPool创建时,并没有立刻创建真正用来满足内存申请的内存块,即内存块链表刚开始时为空。

②处和③处分别设置"第1次创建的内存块所包含的分配单元的个数",及"随后创建的内存块所包含的分配单元的个数",这两个值在MemoryPool创建时通过参数指定,其后在该MemoryPool对象生命周期中一直不变。

后面的代码用来设置nUnitSize,这个值参考传入的_nUnitSize参数。但是还需要考虑两个因素。如前所述,每个分配单元在自由状态时,其头两个字节用来存放"其下一个自由分配单元的编号"。即每个分配单元"最少"有"两个字节",这就是⑤处赋值的原因。④处是将大于4个字节的大小_nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍数(前提是MEMPOOL_ALIGNMENT为2的倍数)。如_nUnitSize为11时,MEMPOOL_ALIGNMENT为8,nUnitSize为16;MEMPOOL_ALIGNMENT为4,nUnitSize为12;MEMPOOL_ALIGNMENT为2,nUnitSize为12,依次类推。

(2)当向MemoryPool提出内存请求时:

void* MemoryPool::Alloc()
{
    if ( !pBlock )           ①
    {
			……							
    }

    MemoryBlock* pMyBlock = pBlock;
    while (pMyBlock && !pMyBlock->nFree )②
        pMyBlock = pMyBlock->pNext;

    if ( pMyBlock )	         ③
    {
        char* pFree = pMyBlock->aData+(pMyBlock->nFirst*nUnitSize);			
        pMyBlock->nFirst = *((USHORT*)pFree);
							
        pMyBlock->nFree--;	
        return (void*)pFree;
    }
    else                    ④
    {
        if ( !nGrowSize )
            return NULL;

		pMyBlock = new(nGrowSize, nUnitSize) FixedMemBlock(nGrowSize, nUnitSize);
        if ( !pMyBlock )
            return NULL;

        pMyBlock->pNext = pBlock;
        pBlock = pMyBlock;

        return (void*)(pMyBlock->aData);
    }

}


 

MemoryPool满足内存请求的步骤主要由四步组成。

①处首先判断内存池当前内存块链表是否为空,如果为空,则意味着这是第1次内存申请请求。这时,从进程堆中申请一个分配单元个数为nInitSize的内存块,并初始化该内存块(主要初始化MemoryBlock结构体成员,以及创建初始的自由分配单元链表,下面会详细分析其代码)。如果该内存块申请成功,并初始化完毕,返回第1个分配单元给调用函数。第1个分配单元以MemoryBlock结构体内的最后一个字节为起始地址。

②处的作用是当内存池中已有内存块(即内存块链表不为空)时遍历该内存块链表,寻找还有"自由分配单元"的内存块。

③处检查如果找到还有自由分配单元的内存块,则"定位"到该内存块现在可以用的自由分配单元处。"定位"以MemoryBlock结构体内的最后一个字节位置aData为起始位置,以MemoryPool的nUnitSize为步长来进行。找到后,需要修改MemoryBlock的nFree信息(剩下来的自由分配单元比原来减少了一个),以及修改此内存块的自由存储单元链表的信息。在找到的内存块中,pMyBlock->nFirst为该内存块中自由存储单元链表的表头,其下一个自由存储单元的编号存放在pMyBlock->nFirst指示的自由存储单元(亦即刚才定位到的自由存储单元)的头两个字节。通过刚才定位到的位置,取其头两个字节的值,赋给pMyBlock->nFirst,这就是此内存块的自由存储单元链表的新的表头,即下一次分配出去的自由分配单元的编号(如果nFree大于零的话)。修改维护信息后,就可以将刚才定位到的自由分配单元的地址返回给此次申请的调用函数。注意,因为这个分配单元已经被分配,而内存块无须维护已分配的分配单元,因此该分配单元的头两个字节的信息已经没有用处。换个角度看,这个自由分配单元返回给调用函数后,调用函数如何处置这块内存,内存池无从知晓,也无须知晓。此分配单元在返回给调用函数时,其内容对于调用函数来说是无意义的。因此几乎可以肯定调用函数在用这个单元的内存时会覆盖其原来的内容,即头两个字节的内容也会被抹去。因此每个存储单元并没有因为需要链接而引入多余的维护信息,而是直接利用单元内的头两个字节,当其分配后,头两个字节也可以被调用函数利用。而在自由状态时,则用来存放维护信息,即下一个自由分配单元的编号,这是一个有效利用内存的好例子。

④处表示在②处遍历时,没有找到还有自由分配单元的内存块,这时,需要重新向进程堆申请一个内存块。因为不是第一次申请内存块,所以申请的内存块包含的分配单元个数为nGrowSize,而不再是nInitSize。与①处相同,先做这个新申请内存块的初始化工作,然后将此内存块插入MemoryPool的内存块链表的头部,再将此内存块的第1个分配单元返回给调用函数。将此新内存块插入内存块链表的头部的原因是该内存块还有很多可供分配的自由分配单元(除非nGrowSize等于1,这应该不太可能。因为内存池的含义就是一次性地从进程堆中申请一大块内存,以供后续的多次申请),放在头部可以使得在下次收到内存申请时,减少②处对内存块的遍历时间。

可以用图6-2的MemoryPool来展示MemoryPool::Alloc的过程。图6-3是某个时刻MemoryPool的内部状态。


图6-3 某个时刻MemoryPool的内部状态
图6-3  某个时刻MemoryPool的内部状态 
 

因为MemoryPool的内存块链表不为空,因此会遍历其内存块链表。又因为第1个内存块里有自由的分配单元,所以会从第1个内存块中分配。检查nFirst,其值为m,这时pBlock->aData+(pBlock->nFirst*nUnitSize)定位到编号为m的自由分配单元的起始位置(用pFree表示)。在返回pFree之前,需要修改此内存块的维护信息。首先将nFree递减1,然后取得pFree处开始的头两个字节的值(需要说明的是,这里aData处值为k。其实不是这一个字节。而是以aData和紧跟其后的另外一个字节合在一起构成的一个USHORT的值,不可误会)。发现为k,这时修改pBlock的nFirst为k。然后,返回pFree。此时MemoryPool的结构如图6-4所示。


图6-4 MemoryPool的结构
图6-4  MemoryPool的结构 
 

可以看到,原来的第1个可供分配的单元(m编号处)已经显示为被分配的状态。而pBlock的nFirst已经指向原来m单元下一个自由分配单元的编号,即k。

(3)MemoryPool回收内存时:

void MemoryPool::Free( void* pFree )
{
    ……

    MemoryBlock* pMyBlock = pBlock;

    while ( ((ULONG)pMyBlock->aData > (ULONG)pFree) ||
         ((ULONG)pFree >= ((ULONG)pMyBlock->aData + pMyBlock->nSize)) )①
    {
         ……
    }

    pMyBlock->nFree++;                     ②
    *((USHORT*)pFree) = pMyBlock->nFirst;  ③
    pMyBlock->nFirst = (USHORT)(((ULONG)pFree-(ULONG)(pBlock->aData)) / nUnitSize);④

    if (pMyBlock->nFree*nUnitSize == pMyBlock->nSize )⑤
    {
        ……
    }
    else
    {
        ……
    }
}


 

如前所述,回收分配单元时,可能会将整个内存块返回给进程堆,也可能将被回收分配单元所属的内存块移至内存池的内存块链表的头部。这两个操作都需要修改链表结构。这时需要知道该内存块在链表中前一个位置的内存块。

①处遍历内存池的内存块链表,确定该待回收分配单元(pFree)落在哪一个内存块的指针范围内,通过比较指针值来确定。

运行到②处,pMyBlock即找到的包含pFree所指向的待回收分配单元的内存块(当然,这时应该还需要检查pMyBlock为NULL时的情形,即pFree不属于此内存池的范围,因此不能返回给此内存池,读者可以自行加上)。这时将pMyBlock的nFree递增1,表示此内存块的自由分配单元多了一个。

③处用来修改该内存块的自由分配单元链表的信息,它将这个待回收分配单元的头两个字节的值指向该内存块原来的第一个可分配的自由分配单元的编号。

④处将pMyBlock的nFirst值改变为指向这个待回收分配单元的编号,其编号通过计算此单元的起始位置相对pMyBlock的aData位置的差值,然后除以步长(nUnitSize)得到。

实质上,③和④两步的作用就是将此待回收分配单元"真正回收"。值得注意的是,这两步实际上是使得此回收单元成为此内存块的下一个可分配的自由分配单元,即将它放在了自由分配单元链表的头部。注意,其内存地址并没有发生改变。实际上,一个分配单元的内存地址无论是在分配后,还是处于自由状态时,一直都不会变化。变化的只是其状态(已分配/自由),以及当其处于自由状态时在自由分配单元链表中的位置。

⑤处检查当回收完毕后,包含此回收单元的内存块的所有单元是否都处于自由状态,且此内存是否处于内存块链表的头部。如果是,将此内存块整个的返回给进程堆,同时修改内存块链表结构。

注意,这里在判断一个内存块的所有单元是否都处于自由状态时,并没有遍历其所有单元,而是判断nFree乘以nUnitSize是否等于nSize。nSize是内存块中所有分配单元的大小,而不包括头部MemoryBlock结构体的大小。这里可以看到其用意,即用来快速检查某个内存块中所有分配单元是否全部处于自由状态。因为只需结合nFree和nUnitSize来计算得出结论,而无须遍历和计算所有自由状态的分配单元的个数。

另外还需注意的是,这里并不能比较nFree与nInitSize或nGrowSize的大小来判断某个内存块中所有分配单元都为自由状态,这是因为第1次分配的内存块(分配单元个数为nInitSize)可能被移到链表的后面,甚至可能在移到链表后面后,因为某个时间其所有单元都处于自由状态而被整个返回给进程堆。即在回收分配单元时,无法判定某个内存块中的分配单元个数到底是nInitSize还是nGrowSize,也就无法通过比较nFree与nInitSize或nGrowSize的大小来判断一个内存块的所有分配单元是否都为自由状态。

以上面分配后的内存池状态作为例子,假设这时第2个内存块中的最后一个单元需要回收(已被分配,假设其编号为m,pFree指针指向它),如图6-5所示。

不难发现,这时nFirst的值由原来的0变为m。即此内存块下一个被分配的单元是m编号的单元,而不是0编号的单元(最先分配的是最新回收的单元,从这一点看,这个过程与栈的原理类似,即先进后出。只不过这里的"进"意味着"回收",而"出"则意味着"分配")。相应地,m的"下一个自由单元"标记为0,即内存块原来的"下一个将被分配出去的单元",这也表明最近回收的分配单元被插到了内存块的"自由分配单元链表"的头部。当然,nFree递增1。


图6-5 分配后的内存池状态
图6-5  分配后的内存池状态 
 

处理至⑥处之前,其状态如图6-6所示。


图6-6 处理至⑥处之前的内存池状态
图6-6  处理至⑥处之前的内存池状态 
 

这里需要注意的是,虽然pFree被"回收",但是pFree仍然指向m编号的单元,这个单元在回收过程中,其头两个字节被覆写,但其他部分的内容并没有改变。而且从整个进程的内存使用角度来看,这个m编号的单元的状态仍然是"有效的"。因为这里的"回收"只是回收给了内存池,而并没有回收给进程堆,因此程序仍然可以通过pFree访问此单元。但是这是一个很危险的操作,因为首先该单元在回收过程中头两个字节已被覆写,并且该单元可能很快就会被内存池重新分配。因此回收后通过pFree指针对这个单元的访问都是错误的,读操作会读到错误的数据,写操作则可能会破坏程序中其他地方的数据,因此需要格外小心。

接着,需要判断该内存块的内部使用情况,及其在内存块链表中的位置。如果该内存块中省略号"……"所表示的其他部分中还有被分配的单元,即nFree乘以nUnitSize不等于nSize。因为此内存块不在链表头,因此还需要将其移到链表头部,如图6-7所示。


图6-7 因回收引起的MemoryBlock移动
图6-7  因回收引起的MemoryBlock移动 
 

如果该内存块中省略号"……"表示的其他部分中全部都是自由分配单元,即nFree乘以nUnitSize等于nSize。因为此内存块不在链表头,所以此时需要将此内存块整个回收给进程堆,回收后内存池的结构如图6-8所示。


图6-8 回收后内存池的结构
图6-8  回收后内存池的结构 
 

一个内存块在申请后会初始化,主要是为了建立最初的自由分配单元链表,下面是其详细代码:

MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize)
	: nSize  (nTypes * nUnitSize),
	  nFree  (nTypes - 1),                     ④
	  nFirst (1),                              ⑤
	  pNext  (0)
{
		char * pData = aData;                  ①
		for (USHORT i = 1; i < nTypes; i++) ②
		{
			*reinterpret_cast<USHORT*>(pData) = i; ③
			pData += nUnitSize;
		}
}


 

这里可以看到,①处pData的初值是aData,即0编号单元。但是②处的循环中i却是从1开始,然后在循环内部的③处将pData的头两个字节值置为i。即0号单元的头两个字节值为1,1号单元的头两个字节值为2,一直到(nTypes-2)号单元的头两个字节值为(nTypes-1)。这意味着内存块初始时,其自由分配单元链表是从0号开始。依次串联,一直到倒数第2个单元指向最后一个单元。

还需要注意的是,在其初始化列表中,nFree初始化为nTypes-1(而不是nTypes),nFirst初始化为1(而不是0)。这是因为第1个单元,即0编号单元构造完毕后,立刻会被分配。另外注意到最后一个单元初始并没有设置头两个字节的值,因为该单元初始在本内存块中并没有下一个自由分配单元。但是从上面例子中可以看到,当最后一个单元被分配并回收后,其头两个字节会被设置。

图6-9所示为一个内存块初始化后的状态。


图6-9 一个内存块初始化后的状态
图6-9  一个内存块初始化后的状态 
 

当内存池析构时,需要将内存池的所有内存块返回给进程堆:

MemoryPool::~MemoryPool()
{
    MemoryBlock* pMyBlock = pBlock;
    while ( pMyBlock )
    {
        ……
    }
}


 

6.2.4 使用方法

分析内存池的内部原理后,本节说明如何使用它。从上面的分析可以看到,该内存池主要有两个对外接口函数,即Alloc和Free。Alloc返回所申请的分配单元(固定大小内存),Free则回收传入的指针代表的分配单元的内存给内存池。分配的信息则通过MemoryPool的构造函数指定,包括分配单元大小、内存池第1次申请的内存块中所含分配单元的个数,以及内存池后续申请的内存块所含分配单元的个数等。

综上所述,当需要提高某些关键类对象的申请/回收效率时,可以考虑将该类所有生成对象所需的空间都从某个这样的内存池中开辟。在销毁对象时,只需要返回给该内存池。"一个类的所有对象都分配在同一个内存池对象中"这一需求很自然的设计方法就是为这样的类声明一个静态内存池对象,同时为了让其所有对象都从这个内存池中开辟内存,而不是缺省的从进程堆中获得,需要为该类重载一个new运算符。因为相应地,回收也是面向内存池,而不是进程的缺省堆,还需要重载一个delete运算符。在new运算符中用内存池的Alloc函数满足所有该类对象的内存请求,而销毁某对象则可以通过在delete运算符中调用内存池的Free完成。

6.2.5 性能比较

为了测试利用内存池后的效果,通过一个很小的测试程序可以发现采用内存池机制后耗时为297 ms。而没有采用内存池机制则耗时625 ms,速度提高了52.48%。速度提高的原因可以归结为几点,其一,除了偶尔的内存申请和销毁会导致从进程堆中分配和销毁内存块外,绝大多数的内存申请和销毁都由内存池在已经申请到的内存块中进行,而没有直接与进程堆打交道,而直接与进程堆打交道是很耗时的操作;其二,这是单线程环境的内存池,可以看到内存池的Alloc和Free操作中并没有加线程保护措施。因此如果类A用到该内存池,则所有类A对象的创建和销毁都必须发生在同一个线程中。但如果类A用到内存池,类B也用到内存池,那么类A的使用线程可以不必与类B的使用线程是同一个线程。

另外,在第1章中已经讨论过,因为内存池技术使得同类型的对象分布在相邻的内存区域,而程序会经常对同一类型的对象进行遍历操作。因此在程序运行过程中发生的缺页应该会相应少一些,但这个一般只能在真实的复杂应用环境中进行验证。

6.3 本章小结

内存的申请和释放对一个应用程序的整体性能影响极大,甚至在很多时候成为某个应用程序的瓶颈。消除内存申请和释放引起的瓶颈的方法往往是针对内存使用的实际情况提供一个合适的内存池。内存池之所以能够提高性能,主要是因为它能够利用应用程序的实际内存使用场景中的某些"特性"。比如某些内存申请与释放肯定发生在一个线程中,某种类型的对象生成和销毁与应用程序中的其他类型对象要频繁得多,等等。针对这些特性,可以为这些特殊的内存使用场景提供量身定做的内存池。这样能够消除系统提供的缺省内存机制中,对于该实际应用场景中的不必要的操作,从而提升应用程序的整体性能。

posted @ 2012-05-09 14:46 笑笑小白 阅读(23) 评论(0) 编辑

转:C++内存池

#ifndef  _MEMPOOL_H_
#define  _MEMPOOL_H_

/*
本类封装了一个内存池,采用模板类,
模板参数就是内存池中分配的对象类型
本类主要用链表来实现,适用于固定大小的内存块分配
*/

#include <vector>
using std::vector;

template<typename T>
class CMemPool
{
    struct _MemNode
    {
        _MemNode *pPrev;
        char data[sizeof(T) - sizeof(_MemNode*)];
    };
    
    struct _MemBlock
    {
        _MemBlock *pPrev;
        _MemNode  *pNode;
    };

    _MemBlock *m_pBlockHeader;        //大内存块链表头指针
    _MemNode  *m_FreeNodeHeader;      //空闲的小块头指针
    int m_BlockSize;                  //当前小内存块数量

private:
    void AllocBlocks();        //分配内存块
    void ReallocBlocks();      //扩大内存池大小

 
public:
    //构造函数,nInitSize为初始内存块数
    CMemPool(int nInitSize = 128) : m_BlockSize(nInitSize), m_pBlockHeader(NULL),m_FreeNodeHeader(NULL)
    {
        AllocBlocks();
    }

    //析构函数,释放所有底层内存块
    ~CMemPool()
    {
        while(m_pBlockHeader != NULL)
        {
            _MemBlock *tmp = m_pBlockHeader;
            m_pBlockHeader = tmp->pPrev;
            free(tmp->pNode);
            free(tmp);
        }
    }

    / * 下面两个函数是主要的对外接口 */
    void *AllocBlock(size_t size);                     //从内存池请求内存,size为分配的内存块大小
    void  FreeBlock(void *pobj , size_t size );        //把内存归还给内存池,pobj是内存块指针,size为内存块大小

//打印信息,用于调试程序
/*  void PrintTestInfo()
    {
        printf("Handle pool: size = %d, free_index = %d, capacity = %d ", m_BlockSize,m_FreeIndex, m_Blocks.capacity());
    }
*/

};

template<typename T>
void *CMemPool<T>::AllocBlock(size_t size)
{
    if (size != sizeof(T)) //假如size不等于T类型大小,则使用全局分配符分配内存
    {
        return malloc(size);
    }

    if (m_FreeNodeHeader == NULL) //当前没有空闲的内存块,则扩大内存
    {
        ReallocBlocks();
    }

    void *p = m_FreeNodeHeader;    //从空闲块中分配一块内存
    m_FreeNodeHeader = m_FreeNodeHeader->pPrev;

    return p;
}

template<typename T>
void CMemPool<T>::FreeBlock(void *pobj, size_t size)
{
    if( pobj == NULL )        //pobj不能为NULL
        return ;
    if( size != sizeof(T) )   //假如size不等于T类型大小,则使用全局释放内存操作符释放内存      
    {
        free(pobj);
    }
    else  //将内存归还给内存池
    {
        _MemNode *p = (_MemNode*)pobj;
        p->pPrev = m_FreeNodeHeader;
        m_FreeNodeHeader = p;
    }
}

template<typename T>
void CMemPool<T>::AllocBlocks()
{
    //分配m_BlockSize大小个内存块,放入内存池
    _MemBlock *newBlock = (_MemBlock*)malloc(sizeof(_MemBlock));

    //分配大块内存
    newBlock->pNode = (_MemNode*)malloc(sizeof(_MemNode) *m_BlockSize);

    //将分配的大块内存分成小块,串接在空闲块链表上
    newBlock->pNode->pPrev = NULL;
    for(int i = 1; i < m_BlockSize; ++i)
    {
        newBlock->pNode[i].pPrev = &(newBlock->pNode[i - 1]);
    }
    m_FreeNodeHeader = &newBlock->pNode[m_BlockSize - 1];
 
    newBlock->pPrev = m_pBlockHeader;
    m_pBlockHeader = newBlock;
}

template<typename T>
void CMemPool<T>::ReallocBlocks()
{
    //将内存池扩大2倍
    m_BlockSize *= 2;
    AllocBlocks();
}

#endif

//--------------------------------------------------------------------------------
本内存池只适合固定大小的内存块分配,避免频繁分配小块内存造成的内存碎片,同时也提高了分配内存的速度。特别适合服务器程序使用。下面有一个测试程序:

#include "MF_mempool.h"
#include "windows.h"

class Test
{
    int    ss;
    char   *p;
    static CMemPool<Test> mp;
public:
    static void *operator new(size_t size);
    static void operator delete(void *pobj, size_t size);
};

CMemPool<Test> Test::mp(512);

void *Test::operator new(size_t size)
{
    return mp.AllocBlock(size);
}

void Test::operator delete(void *pobj, size_t size)
{
    mp.FreeBlock(pobj, size);
}

#define Max_Blocks  1000000
int _tmain(int argc, _TCHAR *argv[])
{
    Test* *ptrarry= new Test *[Max_Blocks];
    DWORD ss = GetTickCount();
 
    for(int i = 0; i < Max_Blocks; ++i)
    {
        ptrarry[i] = new Test;
    }
 
    for(int j = Max_Blocks - 1; j >= Max_Blocks; --j)
    {
        delete ptrarry[j];
    }

    DWORD end = GetTickCount();

    printf("take out time is %d\n", end - ss);
    return 0;
}

posted @ 2012-05-09 14:30 笑笑小白 阅读(30) 评论(0) 编辑

2012年5月8日

在linux查看内存的大小

用free -m查看的结果:
# free -m
         total    used    free     shared buffers     cached
Mem:           504        471       32       0       19        269
-/+ buffers/cache:        183        321
Swap:       996       0        996


查看/proc/kcore文件的大小:
# ll -h /proc/kcore
-r-------- 1 root root 512M 10月 26 20:40 /proc/kcore

在Linux下查看内存我们一般用free命令:
[root@scs-2 tmp]# free
             total       used       free     shared    buffers     cached
Mem:       3266180    3250004      16176          0     110652    2668236
-/+ buffers/cache:     471116    2795064
Swap:      2048276      80160    1968116

下面是对这些数值的解释:
total:总计物理内存的大小。
used:已使用多大。
free:可用有多少。
Shared:多个进程共享的内存总额。
Buffers/cached:磁盘缓存的大小。
第三行(-/+ buffers/cached):
used:已使用多大。
free:可用有多少。
第四行就不多解释了。
区别:第二行(mem)的used/free与第三行(-/+ buffers/cache) used/free的区别。 这两个的区别在于使用的角度来看,第一行是从OS的角度来看,因为对于OS,buffers/cached 都是属于被使用,所以他的可用内存是16176KB,已用内存是3250004KB,其中包括,内核(OS)使用+Application(X, oracle,etc)使用的+buffers+cached.
第三行所指的是从应用程序角度来看,对于应用程序来说,buffers/cached 是等于可用的,因为buffer/cached是为了提高文件读取的性能,当应用程序需在用到内存的时候,buffer/cached会很快地被回收。
所以从应用程序的角度来说,可用内存=系统free memory+buffers+cached。
如上例:
2795064=16176+110652+2668236

接下来解释什么时候内存会被交换,以及按什么方交换。 当可用内存少于额定值的时候,就会开会进行交换。
如何看额定值:
cat /proc/meminfo

[root@scs-2 tmp]# cat /proc/meminfo
MemTotal:      3266180 kB
MemFree:         17456 kB
Buffers:        111328 kB
Cached:        2664024 kB
SwapCached:          0 kB
Active:         467236 kB
Inactive:      2644928 kB
HighTotal:           0 kB
HighFree:            0 kB
LowTotal:      3266180 kB
LowFree:         17456 kB
SwapTotal:     2048276 kB
SwapFree:      1968116 kB
Dirty:               8 kB
Writeback:           0 kB
Mapped:         345360 kB
Slab:           112344 kB
Committed_AS:   535292 kB
PageTables:       2340 kB
VmallocTotal: 536870911 kB
VmallocUsed:    272696 kB
VmallocChunk: 536598175 kB
HugePages_Total:     0
HugePages_Free:      0
Hugepagesize:     2048 kB

用free -m查看的结果:
[root@scs-2 tmp]# free -m 
             total       used       free     shared    buffers     cached
Mem:          3189       3173         16          0        107       2605
-/+ buffers/cache:        460       2729
Swap:         2000         78       1921


查看/proc/kcore文件的大小(内存镜像):
[root@scs-2 tmp]# ll -h /proc/kcore 
-r-------- 1 root root 4.1G Jun 12 12:04 /proc/kcore

备注:

占用内存的测量

测量一个进程占用了多少内存,linux为我们提供了一个很方便的方法,/proc目录为我们提供了所有的信息,实际上top等工具也通过这里来获取相应的信息。

/proc/meminfo 机器的内存使用信息

/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。

/proc/pid/statm 进程所占用的内存

[root@localhost ~]# cat /proc/self/statm

654 57 44 0 0 334 0

输出解释

CPU 以及CPU0。。。的每行的每个参数意思(以第一行为例)为:

参数 解释 /proc//status

Size (pages) 任务虚拟地址空间的大小 VmSize/4

Resident(pages) 应用程序正在使用的物理内存的大小 VmRSS/4

Shared(pages) 共享页数 0

Trs(pages) 程序所拥有的可执行虚拟内存的大小 VmExe/4

Lrs(pages) 被映像到任务的虚拟内存空间的库的大小 VmLib/4

Drs(pages) 程序数据段和用户态的栈的大小 (VmData+ VmStk )4

dt(pages) 04

查看机器可用内存

/proc/28248/>free

total used free shared buffers cached

Mem: 1023788 926400 97388 0 134668 503688

-/+ buffers/cache: 288044 735744

Swap: 1959920 89608 1870312

我们通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在linux中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。

所以 空闲内存=free+buffers+cached=total-used

posted @ 2012-05-08 18:11 笑笑小白 阅读(16) 评论(0) 编辑

2012年5月7日

linux下的log4cplus的安装和使用

log4cplus是一款优秀的基于C/C++的开源日志库。log4cplus具有线程安全,不用但心在多线程状态下写日志问题;使用灵活,可通过配置文件设置日志级别下输出位置,还可以在程序运行时动态设置日志输出级别,随心所欲掌控日志的输出;以及多粒度控制的特点。通过将信息划分优先级使其可以面向程序调试、运行、测试、和维护等全生命周期; 可以选择将信息输出到屏幕、文件、NT event log、甚至是远程服务器;通过指定策略对日志进行定期备份。可以满足大部分开发者对日志系统需求,功能全面。
下载地址:http://sourceforge.net/projects/log4cplus/files/log4cplus-stable/
tar -xjf log4cplus-1.0.4.tar.bz2(解压缩),切换到解压缩路径下
安装:./configure;make;make install
这里已经安装成功,默认的路径lib库路径是/usr/local/lib/,头文件的位置:/usr/local/include/log4cplus
将/usr/local/lib下的和log4cplus相关的库都拷贝到/usr/lib,将头文件加到/etc/profile下的CPLUS_INCLUDE_PATH

这里要强调是使用log4cplus时,要 加上-llog4cplus -lrt -lpthread -lrt(或者 -llog4cplus -lpthread ),才能顺利的编译通过和使用;

 

确保你的Makefile中包含 /usr/local/lib/liblog4cplus.a(静态库)或-llog4cplus(动态库)即可。

头文件在/usr/local/include/log4cplus目录下。

对于动态库,要想正常使用,还得将库安装路径加入到 LD_LIBRARY_PATH 中,以管理员身份登录,在/etc/ld.so.conf中加入安装路径,这里是/usr/local/lib,然后执行 ldconfig使设置生效即可。

 

在其log4cplus官网http://log4cplus.sourceforge.net/codeexamples.html 里面自带的三个例程。

 

 

 

2、  安装与配置
安装
#tar –jxvf  log4cplus-1.0.4-rc4.tar.bz2*.tar.bz2
#cd log4cplus-1.0.4-rc4
#configure --prefix=/usr/local/
#make
#make install
配置
#ewindowsxport LD_LIBRARY_PATH=/usr/local/log4cplus/lib/
3、  使用案例
log4cplus库编写了5个等级的信息:DEBUG、INFO、WARNING、ERROR与FATAL。
Ø         日志内容形式为:DEBUG   Enter the main function
Ø         并且对日志文件名没有进行规定。
本文的案例对log4提供的日志函数进行了封装,最终增加的功能如下:
Ø         日志文件名形式:2009-10-10.txt
Ø         日志内容形式:DEBUG  [2009-10-10 10:12:34]  Enter the main function
Ø         自定义写日志接口函数形式:writelog("TRACE",” Enter the main function”);
 
#include <log4cplus/logger.h>
#include <log4cplus/configurator.h>
#include <iomanip>
#include <log4cplus/logger.h>
#include <log4cplus/fileappender.h>
#include <log4cplus/consoleappender.h>
#include <log4cplus/layout.h>
#include <time.h>
using namespace std;
using namespace log4cplus;
Logger pTestLogger;
void writelog(char* leval,char* info)
{
struct tm *p;
time_t lt=time(NULL);
p=localtime(&lt);
char* timetemp=ctime(&lt);
*(timetemp+strlen(timetemp)-1)='\0';
char temp[10000];
sprintf(temp,"[%s] %s",timetemp,info);
        printf("temp==%s",temp);
printf("leval====%s ",leval);
if(memcmp(leval,"TRACE",5)==0)
printf("%d===%d",memcmp("TRACE","TRAC1E",5),memcmp(leval,"TRACE",5));
LOG4CPLUS_TRACE(pTestLogger,temp);
if(memcmp(leval,"DEBUG",5)==0)
LOG4CPLUS_DEBUG(pTestLogger,temp);
if(memcmp(leval,"INFO",4)==0)
LOG4CPLUS_INFO(pTestLogger,temp);
if(memcmp(leval,"WARN",4)==0)
LOG4CPLUS_WARN(pTestLogger,temp);
if(memcmp(leval,"ERROR",5)==0)
LOG4CPLUS_ERROR(pTestLogger,temp);
if(memcmp(leval,"FATAL",5)==0)
LOG4CPLUS_FATAL(pTestLogger,temp);
}
int main()
{
char* info="you have a iuns";
char filename[50];
struct tm *p;
time_t lt=time(NULL);
p=localtime(&lt);
sprintf(filename,"%d-%d-%d.txt",(1900+p->tm_year), (1+p->tm_mon),p->tm_mday);
FILE* stream=fopen(filename,"wb");
SharedAppenderPtr pFileAppender(new FileAppender((filename)));
pTestLogger = Logger::getInstance(("LoggerName"));
pTestLogger.addAppender(pFileAppender);
writelog("TRACE",info);
writelog("DEBUG",info);
writelog("ERROR",info);
        return 0;
}
 
编译命令:
 
#g++ filetime.cpp -I /usr/local/log4cplus/include/ -L /usr/local/log4cplus/lib -llog4cplus -o filetime

 

posted @ 2012-05-07 15:08 笑笑小白 阅读(124) 评论(0) 编辑

2012年5月4日

日志信息的编写与调用

StdLogger.h

#ifndef STDLOGGER_H
#define STDLOGGER_H
#include <log4cplus/logger.h>
#include <log4cplus/configurator.h>
#include <log4cplus/helpers/loglog.h>
#include <log4cplus/consoleappender.h>
#include <log4cplus/fileappender.h>


class StdLogger
{
public:
StdLogger();
virtual ~StdLogger();

/// 启动日志系统
/// @param[in] properties_filename 日志系统配置文件文件名
void StartSystem();

/// 关闭日志系统
void StopSystem();

public:
void Debug(const char* pFormat, ...);

void Error(const char* pFormat, ...);

void Fatal(const char* pFormat, ...);

void Info(const char* pFormat, ...);

void Warn(const char* pFormat, ...);

void Trace(const char* pFormat, ...);

public:
static inline StdLogger* getSingletonPtr()
{
return &getSingleton();
}
static inline StdLogger& getSingleton()
{
static StdLogger _instance;
return _instance;
}
};
#define g_Logger StdLogger::getSingleton()
#define g_pLogger StdLogger::getSingleton()


//////////////////////////////////////////////////////////////////////////
// 断言日志
//////////////////////////////////////////////////////////////////////////
#define ASSERT_LOG(expr)\
if ( (expr) ) {;} else g_Logger.Error(__FILE__, __LINE__, #expr);


//////////////////////////////////////////////////////////////////////////
// 以下的宏只有VS2005以及之上的版本可以使用!因为VS2005之下的版本不支持可变参数宏
//////////////////////////////////////////////////////////////////////////
#if defined(_MSC_VER) && _MSC_VER > 1400
#define LOG_DEBUG(...) g_Logger.Debug(__FILE__, __LINE__, __VA_ARGS__);
#define LOG_ERROR(...) g_Logger.Error(__FILE__, __LINE__, __VA_ARGS__);
#define LOG_FATAL(...) g_Logger.Fatal(__FILE__, __LINE__, __VA_ARGS__);
#define LOG_INFO(...) g_Logger.Info(__FILE__, __LINE__, __VA_ARGS__);
#define LOG_WARN(...) g_Logger.Warn(__FILE__, __LINE__, __VA_ARGS__);
#define LOG_TRACE(...) g_Logger.Trace(__FILE__, __LINE__, __VA_ARGS__);
#endif


#endif

实现:


#include "StdLogger.h"
#include <cstdlib>
#include <iostream>
#include <log4cplus/config.hxx>
#include <log4cplus/logger.h>
#include <log4cplus/configurator.h>
#include <log4cplus/helpers/loglog.h>
#include <log4cplus/helpers/stringhelper.h>
#include <log4cplus/helpers/socket.h>
#include <log4cplus/helpers/threads.h>
#include <log4cplus/spi/loggerimpl.h>
#include <log4cplus/spi/loggingevent.h>
#include <log4cplus/fileappender.h>


using namespace log4cplus;
using namespace log4cplus::helpers;

StdLogger::StdLogger()
{

}

StdLogger::~StdLogger()
{
log4cplus::Logger _logger = log4cplus::Logger::getRoot();
LOG4CPLUS_INFO(_logger, "Logger System Stop Finish.");
}

#define DO_LOGGER(logLevel,pFormat, bufSize)\
log4cplus::Logger _logger = log4cplus::Logger::getRoot();\
\
if(_logger.isEnabledFor(logLevel))\
{ \
va_list args; \
va_start(args, pFormat); \
char buf[bufSize] = {0}; \
_vsnprintf(buf, sizeof(buf), pFormat, args); \
va_end(args); \
_logger.forcedLog(logLevel, buf); \
}

void StdLogger::Debug( const char* pFormat, ...)
{
DO_LOGGER(log4cplus::DEBUG_LOG_LEVEL,pFormat, 1024);
}

void StdLogger::Error( const char* pFormat, ...)
{
DO_LOGGER(log4cplus::ERROR_LOG_LEVEL,pFormat, 256);
}

void StdLogger::Fatal( const char* pFormat, ...)
{
DO_LOGGER(log4cplus::FATAL_LOG_LEVEL,pFormat, 256);
}

void StdLogger::Info(const char* pFormat,...)
{
DO_LOGGER(log4cplus::INFO_LOG_LEVEL,pFormat, 512);
}

void StdLogger::Warn(const char* pFormat, ...)
{
DO_LOGGER(log4cplus::WARN_LOG_LEVEL,pFormat, 256);
}

void StdLogger::Trace(const char* pFormat, ...)
{
DO_LOGGER(log4cplus::TRACE_LOG_LEVEL, pFormat, 1024);
}

void StdLogger::StartSystem( )
{
SharedAppenderPtr _append (new ConsoleAppender());

_append->setName("screen_test");

std::string pattern = "%d{%m/%d/%y %H:%M:%S} - %m %n";
std::string pattern2 = "%d{%m/%d/%y %H:%M:%S} - %m [%l]%n";
std::auto_ptr<Layout> _layout(new PatternLayout(pattern));
std::auto_ptr<Layout> layout1(new PatternLayout(pattern2));
std::auto_ptr<Layout> layout2(new PatternLayout(pattern));
_append->setLayout( _layout );


SharedAppenderPtr append1(new FileAppender("web_all.log"));
append1->setName("all log test");

SharedAppenderPtr append2(new FileAppender("web_info.log"));
append2->setName("info log test");

append1->setLayout( layout1 );
append2->setLayout( layout2 );

Logger logger1=Logger::getRoot();
Logger logger2 =Logger::getInstance("web");
Logger logger3 = Logger::getInstance("web.subtest");


logger1.addAppender(append1);
//logger1.setLogLevel(INFO_LOG_LEVEL);

logger2.addAppender(append2);
logger1.setLogLevel(ALL_LOG_LEVEL);

logger3.addAppender(_append);
logger3.setLogLevel(INFO_LOG_LEVEL);
}

void StdLogger::StopSystem()
{

}

///调用

g_Logger.StartSystem();
log4cplus::Logger root_logger = Logger::getRoot();
log4cplus::Logger info_logger= Logger::getInstance("web.subtest");

LOG4CPLUS_INFO(root_logger, "all log Logger Start .");
LOG4CPLUS_INFO(info_logger, "web info Logger Start .");

posted @ 2012-05-04 18:20 笑笑小白 阅读(23) 评论(0) 编辑

log4cplus 配置文件的编写

log4cplus 的configure.h 中注释如下:

/**
* Read configuration from a file. <b>The existing configuration is
* not cleared nor reset.</b> If you require a different behavior,
* then call {@link BasicConfigurator#resetConfiguration
* resetConfiguration} method before calling
* <code>doConfigure</code>.
*
* <p>The configuration file consists of statements in the format
* <code>key=value</code>. The syntax of different configuration
* elements are discussed below.
*
* <h3>Appender configuration</h3>
*
* <p>Appender configuration syntax is:
* <pre>
* # For appender named <i>appenderName</i>, set its class.
* # Note: The appender name can contain dots.
* log4cplus.appender.appenderName=fully.qualified.name.of.appender.class
*
* # Set appender specific options.
* log4cplus.appender.appenderName.option1=value1
* ...
* log4cplus.appender.appenderName.optionN=valueN
* </pre>
*
* For each named appender you can configure its {@link Layout}. The
* syntax for configuring an appender's layout is:
* <pre>
* log4cplus.appender.appenderName.layout=fully.qualified.name.of.layout.class
* log4cplus.appender.appenderName.layout.option1=value1
* ....
* log4cplus.appender.appenderName.layout.optionN=valueN
* </pre>
*
* <h3>Configuring loggers</h3>
*
* <p>The syntax for configuring the root logger is:
* <pre>
* log4cplus.rootLogger=[LogLevel], appenderName, appenderName, ...
* </pre>
*
* <p>This syntax means that an optional <em>LogLevel value</em> can
* be supplied followed by appender names separated by commas.
*
* <p>The LogLevel value can consist of the string values FATAL,
* ERROR, WARN, INFO, DEBUG or a <em>custom LogLevel</em> value.
*
* <p>If a LogLevel value is specified, then the root LogLevel is set
* to the corresponding LogLevel. If no LogLevel value is specified,
* then the root LogLevel remains untouched.
*
* <p>The root logger can be assigned multiple appenders.
*
* <p>Each <i>appenderName</i> (separated by commas) will be added to
* the root logger. The named appender is defined using the
* appender syntax defined above.
*
* <p>For non-root loggers the syntax is almost the same:
* <pre>
* log4cplus.logger.logger_name=[LogLevel|INHERITED], appenderName, appenderName, ...
* </pre>
*
* <p>The meaning of the optional LogLevel value is discussed above
* in relation to the root logger. In addition however, the value
* INHERITED can be specified meaning that the named logger should
* inherit its LogLevel from the logger hierarchy.
*
* <p>By default loggers inherit their LogLevel from the
* hierarchy. However, if you set the LogLevel of a logger and
* later decide that that logger should inherit its LogLevel, then
* you should specify INHERITED as the value for the LogLevel value.
*
* <p>Similar to the root logger syntax, each <i>appenderName</i>
* (separated by commas) will be attached to the named logger.
*
* <p>See the <a href="http://www.cnblogs.com/http://www.cnblogs.com/manual.html#additivity">appender
* additivity rule</a> in the user manual for the meaning of the
* <code>additivity</code> flag.
*
* <p>The user can override any of the {@link
* Hierarchy#disable} family of methods by setting the a key
* "log4cplus.disableOverride" to <code>true</code> or any value other
* than false. As in <pre>log4cplus.disableOverride=true </pre>
*
* <h3>Example</h3>
*
* <p>An example configuration is given below.
*
* <pre>
*
* # Set options for appender named "A1".
* # Appender "A1" will be a SyslogAppender
* log4cplus.appender.A1=log4cplus::SyslogAppender
*
* # The syslog daemon resides on www.abc.net
* log4cplus.appender.A1.SyslogHost=www.abc.net
*
* # A1's layout is a PatternLayout, using the conversion pattern
* # <b>%r %-5p %c{2} %M.%L %x - %m\n</b>. Thus, the log output will
* # include # the relative time since the start of the application in
* # milliseconds, followed by the LogLevel of the log request,
* # followed by the two rightmost components of the logger name,
* # followed by the callers method name, followed by the line number,
* # the nested disgnostic context and finally the message itself.
* # Refer to the documentation of {@link PatternLayout} for further information
* # on the syntax of the ConversionPattern key.
* log4cplus.appender.A1.layout=log4cplus::PatternLayout
* log4cplus.appender.A1.layout.ConversionPattern=%-4r %-5p %c{2} %M.%L %x - %m\n
*
* # Set options for appender named "A2"
* # A2 should be a RollingFileAppender, with maximum file size of 10 MB
* # using at most one backup file. A2's layout is TTCC, using the
* # ISO8061 date format with context printing enabled.
* log4cplus.appender.A2=log4cplus::RollingFileAppender
* log4cplus.appender.A2.MaxFileSize=10MB
* log4cplus.appender.A2.MaxBackupIndex=1
* log4cplus.appender.A2.layout=log4cplus::TTCCLayout
* log4cplus.appender.A2.layout.ContextPrinting=enabled
* log4cplus.appender.A2.layout.DateFormat=ISO8601
*
* # Root logger set to DEBUG using the A2 appender defined above.
* log4cplus.rootLogger=DEBUG, A2
*
* # Logger definitions:
* # The SECURITY logger inherits is LogLevel from root. However, it's output
* # will go to A1 appender defined above. It's additivity is non-cumulative.
* log4cplus.logger.SECURITY=INHERIT, A1
* log4cplus.additivity.SECURITY=false
*
* # Only warnings or above will be logged for the logger "SECURITY.access".
* # Output will go to A1.
* log4cplus.logger.SECURITY.access=WARN
*
*
* # The logger "class.of.the.day" inherits its LogLevel from the
* # logger hierarchy. Output will go to the appender's of the root
* # logger, A2 in this case.
* log4cplus.logger.class.of.the.day=INHERIT
* </pre>
*
* <p>Refer to the <b>setOption</b> method in each Appender and
* Layout for class specific options.
*
* <p>Use the <code>#</code> character at the beginning of a line for comments.
*/

原文地址:https://www.cnblogs.com/shenlian/p/2655089.html