用未公开的MFC类加强动态内存分配

 
用未公开的MFC 类加强动态内存分配
 
 
         下面是示例程序使用及未使用这个未公开MFC类的对比情况截图,是不是有点心动了呢,接着往下看。
 
 
 
         简介
         如果你经常浏览MFC的源代码,说不定就会有意想不到的惊喜发现,这不,很快就发现了一个,它是一个小工具类的集合,允许为特定类的对象定义怎样为其动态分配内存,这个类就是CFixecAlloc,通常它在大家熟悉的CString类的内部,用来分配字符串缓冲区。而我们今天要说的,就是如何在现有代码中,应用CFixecAlloc类,只花最少的修改成本来获取可观的性能提升,本文中探讨CRT动态分配的开销、针对CRT不足之处的备选方法、对CFixecAlloc类的剖析、及如何在程序中使用CFixecAlloc,最后,将在一个示例程序中证明这一切。
 
 
         为什么不用malloc/free 、new/delete 呢?
         众所周知,动态内存分配开销非常大,它会导致程序执行时间上的性能损耗,而且占用空间也更大,如果还不相信,可以亲自查看一下CRT源代码中提供的malloc.c文件,怎么说呢,malloc()函数有点复杂,执行起来可能要花点时间;至于空间上的开销,说起来就稍有点繁琐了,基本上编译程序时不同版本的VC++、程序运行时不同版本的Windows,都会用到不同版本的堆,但这已超出了本文的范围。如果真的对CRT如何选择堆的版本很感兴趣,可以看一下heapinit.c文件中的__heap_select()函数。在CRT的winheap.h头文件中,定义了三种不同版本堆:
 
 
__SYSTEM_HEAP
__V5_HEAP
__V6_HEAP
 
 
         V5及V6版本映射的堆实现,对CRT库来说是私有的,而系统堆(system heap)直接映射到Win32堆服务(如HeapCreate()、HeapAlloc()等等)。对于V5及V6版本,查看CRT源代码之后就可完全知道其空间开销,例如,对V6堆而言,就可找到下面这行语句:
 
 
//添加8个字节的开销并求整到下一个参数大小
sizeEntry = (intSize + 2 * (int)sizeof(int) +
            (BYTES_PER_PARA - 1)) & ~(BYTES_PER_PARA - 1);
 
 
         BYTES_PER_PARA等同于16字节,这是一个非常大的开销,如果我们只需要12字节呢,而CRT将会为此保留32字节,这已经比所需的两倍还多了。但V5及V6堆现今已很少使用了,且也没有源代码,所以还不能确切了解HeapAlloc()的开销是多少,然而,存在一定程度上的开销总是不争的事实,微软对此在HeapCreate()文档中的声明是:
 
         系统使用私有堆中的内存,用于存储支撑堆的结构,因此,并不是所有规定的堆大小对进程来说都是可用的。例如,如果HeapAlloc() 函数从一个最大64K 的堆中请求64k (千字节)内存,那么会因为存在系统开销而调用失败。
 
 
         其他方法的局限性
         当然,也有其他人编写了内存池类用于解决CRT堆开销过多的问题,然而,大多数这类方法需要大量修改现有程序的代码,以便让所有对new、delete的调用,都转向对缓冲池类成员的调用;另外,这类方法还有一个潜在的局限性,它们缺乏线程安全,而CFixedAlloc就是为了解决这些情况而生的。
 
 
CFixedAlloc的类声明:
 
class CFixedAlloc
{
//构造函数
public:
    CFixedAlloc(UINT nAllocSize, UINT nBlockSize = 64);
 
//属性
    UINT GetAllocSize() { return m_nAllocSize; }
 
//具体操作
public:
    void* Alloc();      //返回一块nAllocSize大小的内存
    void Free(void* p); //释放由Alloc分配的一块内存
    void FreeAll();     //释放所有此类分配的内存
 
//实现
public:
    ~CFixedAlloc();
 
protected:
    struct CNode
    {
        CNode* pNext;
    };
 
    UINT m_nAllocSize; //Alloc中每个块的大小
    UINT m_nBlockSize; //每次获取的块数
    CPlex* m_pBlocks;   //块链表(nBlocks*nAllocSize)
    CNode* m_pNodeFree; //第一个要释放的节点,如果没有则为NULL
    CRITICAL_SECTION m_protect;
};
 
 
         可以看到,这是一个非常简单的类,而且,你也许也留意到了,这个类由临界区提供了线程安全。m_AllocSize中包含了类的对象大小,m_blockSize指定了每个固定内存块能包含的对象数,这两个成员都在构造函数中设置。唯一剩下的就是CPlex指针了,在这不打算详细讲解这个类,只需了解它才是真正进行CRT动态内存分配的类就行了,它包含了一个指针,指向另一个CPlex对象以创建一个CPlex链表,所以它的大小只能为4字节,当进行内存分配时,就需要m_allocSize*m_blockSize+sizeof(CPlex)。
         现在,我们可以对比CRT与CFixedAlloc之间的差异了:使用CRT,对于每个对象都会存在CRT开销;而使用CFixedAlloc,内存开销则为CRT开销加上CPlex大小(4字节),再除以已分配对象数,当分配数目非常大时,此开销接近于零。下面,再来仔细看一下两个更重要的CFixedAlloc函数:
 
 
void* CFixedAlloc::Alloc()
{
    EnterCriticalSection(&m_protect);
    if (m_pNodeFree == NULL)
    {
        CPlex* pNewBlock = NULL;
        TRY
        {
            //添加另一个块
            pNewBlock = CPlex::Create(m_pBlocks, m_nBlockSize, m_nAllocSize);
        }
        CATCH_ALL(e)
        {
            LeaveCriticalSection(&m_protect);
            THROW_LAST();
        }
        END_CATCH_ALL
 
        //把它们放进要释放的列表中
        CNode* pNode = (CNode*)pNewBlock->data();
        //逆向进行释放以便于调试
        (BYTE*&)pNode +=
            (m_nAllocSize * m_nBlockSize) - m_nAllocSize;
        for (int i = m_nBlockSize-1; i >= 0; i--,
                     (BYTE*&)pNode -= m_nAllocSize)
        {
            pNode->pNext = m_pNodeFree;
            m_pNodeFree = pNode;
        }
    }
    ASSERT(m_pNodeFree != NULL);
 
    //移除释放列表中第一个节点
    void* pNode = m_pNodeFree;
    m_pNodeFree = m_pNodeFree->pNext;
 
    LeaveCriticalSection(&m_protect);
    return pNode;
}
 
void CFixedAlloc::Free(void* p)
{
    if (p != NULL)
    {
        EnterCriticalSection(&m_protect);
 
        //把节点返回到释放列表中
        CNode* pNode = (CNode*)p;
        pNode->pNext = m_pNodeFree;
        m_pNodeFree = pNode;
        LeaveCriticalSection(&m_protect);
    }
}
 
 
         在Alloc()中,代码会检查要释放的内存池是否为空,如果为空,则创建一个新的内存块(CPlex),并设置要释放节点列表,在此,因为是直接放置到每个块中,所以就没有多余的空间来放置节点信息。当然了,要正常工作,m_nAllocSize必须要大过sizeof(CNode),这也是为什么要在构造函数里进行检查的原因:ASSERT(nAllocSize >= sizeof(CNode))。
         如果大家看过CRT代码,可能有点搞不清楚CFixedAlloc的代码为什么会很少,从算法的观点来看,主要是因为内存块的大小是固定的,比方说,在可进行变量分配的堆中,经过多次分配与释放之后,堆中会有碎片,过不多久就必须扫描一次堆,把邻近的小的剩余空间合并为大的空间,以提高内存使用的效率。
         下面是使CFixedAlloc更便于使用的宏:
 
 
// DECLARE_FIXED_ALLOC -- used in class definition
#define DECLARE_FIXED_ALLOC(class_name) /
public: /
    void* operator new(size_t size) /
    { /
        ASSERT(size == s_alloc.GetAllocSize()); /
        UNUSED(size); /
        return s_alloc.Alloc(); /
    } /
    void* operator new(size_t, void* p) /
        { return p; } /
    void operator delete(void* p) { s_alloc.Free(p); } /
    void* operator new(size_t size, LPCSTR, int) /
    { /
        ASSERT(size == s_alloc.GetAllocSize()); /
        UNUSED(size); /
        return s_alloc.Alloc(); /
    } /
protected: /
    static CFixedAlloc s_alloc /
 
// IMPLEMENT_FIXED_ALLOC -- used in class implementation file
#define IMPLEMENT_FIXED_ALLOC(class_name, block_size) /
CFixedAlloc class_name::s_alloc(sizeof(class_name), block_size) /
 
 
         DECLARED_FIXED_ALLOC()宏担当了new与delete操作,这样,无需修改现有代码就可以应用CFixedAlloc;而IMPLEMENT_FIXED_ALLOC()则负责指定块大小。
 
 
         如何使用CFixedAlloc
         1、把“fixalloc.h”包含在需要修改且含有类定义的头文件中。
         2、在类声明中添加DECLARE_FIXED_ALLOC()宏。
         3、在含有类定义的CPP文件中添加IMPLEMENT_FIXED_ALLOC()宏。
         4、重新编译。
         5、微调块的大小,以获得最佳效果。
 
         因为CFixedAlloc是一个私有类,所以还要在编译器选项中添加一个额外的include目录,其必须指向MFC源代码目录。(因此,在安装Visual Studio时就要选择安装MFC源代码了,还好这通常情况下都是默认安装的。)
 
         大功告成,现在在自己的类中就可以使用MFC CFixedAlloc了,提醒一句:如果编译时代码中有#define _DEBUG,CFixedAlloc宏将不会展开,最终结果还是会和以前一样,就好像什么也没动过。
         对上面的第五步,还要补充一下,块的大小非常重要,如果太大,就会浪费内存;如果太小,则不会得到应有的性能提升,然而,即便这个值非常小,仍会减少对CRT分配函数的调用次数。块大小的理想值应恰好等于要分配的对象数,当然了,通常这是不可能的。
 
 
         关于Visual C++ 2005 的警告信息
         另外,不得不提一点,如果用Visual C++ 2005编译使用了CFixedAlloc的程序时,可能会遇到如下警告信息:
 
 
./CFixedAllocDemoDlg.cpp(237) : warning C4995:
    'CFixedAlloc': name was marked as #pragma deprecated
./CFixedAllocDemoDlg.cpp(240) : warning C4995:
'CFixedAlloc': name was marked as #pragma deprecated
 
 
         这表明从Visual C++ 2005开始,微软已不推荐使用CFixedAlloc类,可我们一时也找不出什么可以代替它的方法,如果你觉得使用CFixedAlloc会让程序大有改观,大可以忽略这些信息,如果怕微软以后从MFC中移除CFixedAlloc类,带来程序上的兼容性问题,那建议你复制一份CFixedAlloc文件加以保存。
         这也表明,微软开始慢慢疏远CFixedAlloc了,回过头来看MFC6,CFixedAlloc却用在了每个临时句柄映射对象类中,如CWnd和CGdiObject,有以下为证:
 
 
class CTempGdiObject : public CGdiObject
{
    DECLARE_DYNCREATE(CTempGdiObject)
    DECLARE_FIXED_ALLOC(CTempGdiObject);
};
 
 
         但这些类从MFC7.1(VC++2003)中就已被移除,也许微软MFC开发小组有其自己的理由吧。
 
 
         内存释放
         当某个对象被删除,且“释放列表”在同一分配的CPlex中时,CFixedAlloc会返回一个节点到一个“释放列表”中,因为CPlex对象是单向链接的,所以在整个程序生命期都会保持已分配状态(或直到它们的表头被删除)。事实上,CFixedAlloc不仅仅是进行内存分配,它是一个再循环器:按需进行分配但并不释放,为后续的分配请求作保留。
         通常,这在频繁地使用new/delete/new/delete情况下,会带来更好的速度提升,但对于短时间内需要大量内存的这种情况,可能会有些问题,这时,程序会一直占有这部分内存,直到CPlex表头被删除,除非用户在使用完内存后,显式地调用CFixedAlloc::FreeAll()来释放这部分内存。也就是说,如果之前已删除所有分配的对象,就可在任意时候调用CFixedAlloc::FreeAll()来自己释放内存,否则,内存会直到CFixedAlloc对象销毁时才会被释放。提示一下:全局对象是在进入WinMain()之前的启动代码中创建的,而在程序退出WinMain()后、自身卸载前才会被销毁。
 
 
         CFixedAlloc 与继承
         在与派生类一起使用CFixedAlloc时,还必须多一点了解,比如说以下情况:
 
 
class Base
{
    DECLARE_FIXED_ALLOC(Base);
};
 
class Child : public Base
{
};
 
 
         如果后面代码中用了Child *p = new Child,将会调用Child::operator new()而不是CFixedAlloc,这时该怎么办呢,请看下面:
 
 
class Base
{
    DECLARE_FIXED_ALLOC(Base);
};
 
class Child : public Base
{
    DECLARE_FIXED_ALLOC(Child);
};
 
 
         当然了,如果类是抽象类,而我们又把Base类声明为FIXED_ALLOC,这怎么也说不过去,因此,关于在类继承中使用CFixedAlloc,必须知道delete操作符为static但不为virtual,再看以下代码:
 
 
Base *p = new Child;
delete p;
 
 
         在这种情况中,将会调用Base::operator delete(),这可就麻烦大了。以下是可行的解决方案:
 
 
class Base
{
public:
    virtual void Destroy() {delete this;}
    DECLARE_FIXED_ALLOC(Base);
};
 
class Child : public Base
{
public:
    virtual void Destroy() {delete this;}
    DECLARE_FIXED_ALLOC(Child);
};
 
Base *p = new Child;
p->Destroy();
 
 
         示例程序
         示例程序非常简单,仅是声明两个几乎一样的类:一个使用了CFixedAlloc而另一个没用。接下来,创建了大量的对象,创建过程是计时的,完成之后,会显示结果。要计算未用CFixedAlloc的类大小开销,在此使用了__V6_HEAP,这种情况非常少见,因此计算值可能不完全准确,但一般来看,应该是比较正确了。
 
 
#define ITER_NUM 1000*1024
 
class A
{
public:
    A( A *next ) : m_next(next) {}
    A *m_next;
    int dummy1;
    int dummy2;
};
 
class B
{
public:
    B( B *next ) : m_next(next) {}
    B *m_next;
    int dummy1;
    int dummy2;
 
    DECLARE_FIXED_ALLOC(B);
};
 
IMPLEMENT_FIXED_ALLOC(B,ITER_NUM);
 
void CCFixedAllocDemoDlg::OnTestTimebutton()
{
    //可在此添加控件通知处理代码
    A *curAObj, *firstAObj = NULL;
    B *curBObj, *firstBObj = NULL;
    DWORD startA, endA, startB, endB;
    register int i;
    {
        CWaitCursor wait;
 
        startA = GetTickCount();
 
        for( i = 0; i < ITER_NUM; i++ )
        {
            firstAObj = new A(firstAObj);
        }
        while( firstAObj )
        {
            curAObj = firstAObj->m_next;
            delete firstAObj;
            firstAObj = curAObj;
        }
 
        startB = endA = GetTickCount();
 
        for( i = 0; i < ITER_NUM; i++ )
        {
            firstBObj = new B(firstBObj);
        }
        while( firstBObj )
        {
            curBObj = firstBObj->m_next;
            delete firstBObj;
            firstBObj = curBObj;
        }
 
        endB = GetTickCount();
    }
 
    displayResult( endA-startA,endB-startB );
}
 
#define BYTES_PER_PARA 16
 
void CCFixedAllocDemoDlg::displayResult( DWORD timeA, DWORD timeB )
{
    TCHAR buf[1024];
 
    /*
     * 每个A对象占用32字节。
     */
    int overheadA = (32-sizeof(A))*ITER_NUM;
 
    /*
     *首先计算已分配大小,再减去所请求大小。
     */
    int overheadB =
      (8+sizeof(B)*ITER_NUM + sizeof(CPlex) + (BYTES_PER_PARA-1))
                    & ~(BYTES_PER_PARA - 1);
    overheadB    -= sizeof(B)*ITER_NUM;
 
    wsprintf( buf, __TEXT("Creating and destroying %d objects/n")
                   __TEXT("without CFixedAlloc/t: %4d ms/n")
                   __TEXT("with CFixedAlloc/t: %4d ms/n")
                   __TEXT("You saved %d bytes with CFixedAlloc"),
                   ITER_NUM, timeA, timeB,
                   overheadA - overheadB );
    MessageBox(buf,__TEXT("Results"));
}
 
 
         建议
         如果移除临界区,把CFixedAlloc作为单线程版本,还可进一步提高性能,这点微软也想到了,从MFC7(VC++.NET 2002)开始,就添加了对单线程的支持,最新的Visual C++ 2008当然也不例外。如果想使用单线程版本的CFixedAlloc,就可使用以下宏:
 
 
// DECLARE_FIXED_ALLOC -- used in class definition
#define DECLARE_FIXED_ALLOC_NOSYNC(class_name) /
public: /
    void* operator new(size_t size) /
    { /
        ASSERT(size == s_alloc.GetAllocSize()); /
        UNUSED(size); /
        return s_alloc.Alloc(); /
    } /
    void* operator new(size_t, void* p) /
        { return p; } /
    void operator delete(void* p) { s_alloc.Free(p); } /
    void* operator new(size_t size, LPCSTR, int) /
    { /
        ASSERT(size == s_alloc.GetAllocSize()); /
        UNUSED(size); /
        return s_alloc.Alloc(); /
    } /
protected: /
    static CFixedAllocNoSync s_alloc /
 
// IMPLEMENT_FIXED_ALLOC_NOSYNC -- used in class
// implementation file
#define IMPLEMENT_FIXED_ALLOC_NOSYNC(class_nbame, block_size) /
CFixedAllocNoSync class_name::s_alloc(sizeof(class_name),
                                      block_size) /
 
 
         不过在IMPLEMENT_FIXED_ALLOC_NOSYNC()宏中有一个小错误,宏参数class_nbame应该为class_name(注:这个错误在Visual C++ 2008中依然存在)。看起来在MFC中并没有使用这些宏,要不然早就发现这个语法错误了,同时,这也意味着这个非同步版本可能未被MFC开发小组所测试。不管怎样,这个错误很小,而且在修正之后丝毫不影响使用,也正是因为MFC中没有使用这个宏,所以修正之后也无需再重新编译MFC。
 
 
/*
 *取消第一行define语句前的注释符号就可以使用单线程版本的CFixedAlloc
*在重新编译之前,必须修正fixalloc.h中的语法错误
* IMPLEMENT_FIXED_ALLOC_NOSYNC()宏参数中的class_nbame应为class_name
*
 *注意:单线程版本只可用在VC++.NET及以上版本中
*/
 
//#define _USE_SINGLE_THREAD
 
#ifndef _USE_SINGLE_THREAD
class B
{
public:
    B( B *next ) : m_next(next) {}
    B *m_next;
    int dummy1;
    int dummy2;
 
    DECLARE_FIXED_ALLOC(B);
};
 
IMPLEMENT_FIXED_ALLOC(B,ITER_NUM);
#else
class B
{
public:
    B( B *next ) : m_next(next) {}
    B *m_next;
    int dummy1;
    int dummy2;
 
    DECLARE_FIXED_ALLOC_NOSYNC(B);
};
 
IMPLEMENT_FIXED_ALLOC_NOSYNC(B,ITER_NUM);
#endif
 
 
         测试程序表明非同步版本相对于原CFixedAlloc版本,带来了40%的速度提升,可别犯迷糊,非同步版本可不能用在多线程程序中喔。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值