介绍自定义固定内存块分配器用于解决两种类型的内存问题。***,全局堆内存的分配和释放很慢,而且是非确定性的。您无法确定内存管理需要多长时间。其次,它减少了由堆碎片引起的内存分配失败的可能性(对于执行关键操作的系统尤其重要)。即使它们不是执行关键操作的系统,一些嵌入式系统也需要设计为运行数周甚至数年而无需重新启动。根据内存分配模式和堆内存的实现方式,长时间使用堆内存可能会导致堆内存错误。一个典型的解决方案是预先静态声明所有对象的内存,从而摆脱动态内存分配。但是,由于对象已经存在,即使不使用也会占用一部分内存,所以静态分配内存的方式会浪费内存存储空间。此外,使用动态内存分配实现的系统提供了更自然的设计框架,这与需要预先分配所有对象的静态内存分配不同。固定内存块分配器并不是一种新方法。长期以来,人们设计了各种自定义内存分配器。在这里,我提供了一个简单的C++内存分配器的实现,我已经在许多项目中成功使用了它。此分配器的实现具有以下特点:比全局堆内存更快消除堆内存碎片错误不需要额外的内存存储(除了几个字节的静态内存)易于使用且代码量小这里将提供一个应用程序,一个释放内存并包含上述功能的简单类。阅读本文后,请同时阅读Replacemalloc/freewithaFastFixedBlockMemoryAllocator以了解如何使用分配器替换CRT(C/C++运行时库)。回收内存存储内存管理模型的基本理念是能够在分配对象内存时回收内存。一旦在内存中创建了一个对象,它占用的内存就不能重新分配。同时,内存必须是可以回收的,允许同类型的对象重用这部分内存。我实现了一个名为Allocator的类来演示这些技巧。当应用程序使用Allocator类进行删除时,对象占用的内存空间被释放以供重用,但不会立即释放给内存管理器。这些记忆保存在一个称为“发布列表”的链表中。再次分配给同一类型的对象。对于每个内存分配请求,Allocaor类首先检查“释放列表”中是否有要释放的内存。只有在“空闲列表”中没有可用内存空间时才会分配新内存。根据Allocator类的所需行为,内存存储在三种操作模式下使用全局堆内存或静态内存池。1.堆内存2.堆内存池3.静态内存池heapmemoryvs.memorypool当“释放列表”为空时,Allocator类可以从堆内存或内存池中申请新的内存。如果使用内存池,则必须事先确定对象的数量。确保内存池足够大以容纳您需要使用的所有对象。另一方面,使用堆内存没有大小限制——您可以在内存允许的范围内构造尽可能多的对象。堆内存模式在全局堆内存上为对象分配内存。释放操作将这块内存放到“释放列表”中,以供重用。当“释放列表”为空时,需要在堆内存上创建新的内存。该方法提供动态内存分配和释放。优点是可以在运行时动态增加内存块。缺点是内存块创建周期不确定,有可能创建失败。堆内存池模式从全局堆内存中创建一个内存池。当创建Allocator类对象时,使用new操作符创建一个内存池。然后使用内存池中的内存块进行内存分配。静态内存池模式使用从静态内存分配的内存池。静态内存池由消费者分配,而不是由Allocator对象创建。堆内存池模式和静态内存池模式提供连续使用内存操作,因为内存分配器不需要分配单独的内存块。这个分配内存的过程非常快速且具有确定性。类设计类的接口很简单。Allocate()返回指向一块内存的指针,而Deallocate()释放内存以供重用。构造函数需要设置对象的大小,如果使用内存池,需要分配内存池空间。类的构造函数中的参数用于确定分配内存块的位置。size参数控制固定内存块的大小。objects参数设置要申请的内存块的数量。值为0表示从堆内存申请新的内存块,非0表示使用内存池(堆内存池或静态内存池)分配对象实例空间。memory参数是指向静态内存的指针。如果memory等于0且objects不为零,Allocator将从堆内存中创建一个内存池。静态内存池的内存大小必须是size*objectbytes。name参数命名内存分配器,用于收集分配器使用信息。classAllocator{public:Allocator(size_tsize,UINTobjects=0,CHAR*memory=NULL,constCHAR*name=NULL);...下面的例子展示了三种分配器模式下的构造函数是如何赋值的。//具有无限100字节块的堆块模式分配器allocatorHeapBlocks(100);//具有20、100字节块的堆池模式AllocatorallocatorHeapPool(100,20);//具有20、100字节块的静态池模式charstaticMemoryPool20[100]*;分配器allocatorStaticPool(100,20,staticMemoryPool);为了简化静态内存池方法,提供了AllocatorPool<>模板类。模板第一个参数设置申请内存对象类型,第二个参数设置申请对象个数。//具有20个MyClass大小块的静态池模式AllocatorPoolallocatorStaticPool2;Deallocate()将内存地址放入“堆栈”。这个“栈”的实现类似于单项链表(“释放链表”),但只能对头部的对象进行增删改查,其行为类似于栈的特性。使用“堆栈”使得分配和释放操作更快,因为不需要全链表遍历而只需要push和pop操作。void*memory1=allocatorHeapBlocks.Allocate(100);这样,内存块在“发布列表”中链接起来,而不需要添加额外的存储。比如我们在使用全局operatenew时,先申请内存,再调用构造函数。delete过程则相反,先调用析构函数,然后释放内存。调用析构函数后,在内存释放到堆之前,这块内存不再被原来的对象使用,而是放入“释放列表”中以供重用。由于Allocator类需要保存释放的内存块,所以在使用delete操作符时,我们将“释放列表”中的next指针指向被删除对象的内存地址。当应用程序再次使用此内存时,指针将被对象的地址覆盖。使用这种方法,不需要预先实例化内存空间。使用释放对象的内存将内存块连接在一起意味着对象的内存空间需要足够大以容纳指针的内存空间。构造函数初始化列表中的代码保证最小内存块大小不会小于指针占用的内存块大小。类的析构函数通过释放堆内存池或遍历“释放列表”,逐个释放内存块来实现内存的释放。由于Allocator类对象经常作为static来使用,所以Allocator对象的释放是在程序的最后。对于大多数嵌入式设备,应用程序只有在人们拔掉电源时才会结束。因此,对于这个嵌入式设备来说,析构函数的作用并不重要。如果使用堆内存块模式,除非所有分配的内存都链接在“空闲列表”中,否则分配的内存块在申请结束时无法释放。因此,所有对象都应该在程序结束时被“删除”(意思是放入“空闲列表”)。这似乎是内存泄漏,这也带来了一个有趣的问题。分配器是否应该跟踪正在使用和已释放的内存块?答案是否定的。认为一旦一块内存被应用程序通过指针使用,应用程序负责在程序结束前通过调用Deallocate()将内存块指针返回给Allocator。这样,我们只需要跟踪释放的内存块。代码使用Allocator易于使用,因此创建宏来自动实现客户端类中的接口。该宏提供了一个静态类型的Allocator实例和两个成员函数:operatornew和operatordelete。通过覆盖new和delete运算符,Allocator拦截并处理客户端类的所有内存分配行为。DECLARE_ALLOCATOR宏提供头文件接口,应包含在类定义中,如下所示:#include"Allocator.h"classMyClass{DECLARE_ALLOCATOR//remainingclassdefinition};operatornew函数调用Allocator创建类实例所需的内存空间。分配内存后,operatornew根据定义调用类的构造函数。重写的new只是修改了内存分配任务。构造函数调用由语言保证。当删除一个对象时,系统首先调用析构函数,然后调用执行操作符delete函数。operatordelete使用Deallocate()函数将内存块添加到“释放列表”。尽管没有明确说明,operatordelete是一个静态函数(静态函数只能调用静态成员)。因此它不能声明为虚拟的。通过基类的指针删除对象似乎不能达到删除真实对象的目的。毕竟,在指向基类的指针上调用静态函数只会调用基类的成员函数,而不是它的真实类型。但是,我们知道在调用operatordelete时首先调用析构函数。修饰为virtual的析构函数实际上会调用子类的析构函数。类的析构函数执行完毕后,调用子类的operatordelete函数。所以实际上,由于虚拟析构函数调用,重写的operatordelete在子类中被调用。因此,在使用基类指针删除对象时,必须将基类对象的析构函数声明为virtual。否则,将无法正确调用析构函数和运算符delete。IMPLEMENT_ALLOCATOR宏是接口的源文件实现部分,应该放在源文件中。IMPLEMENT_ALLOCATOR(MyClass,0,0)使用上面的宏后,可以如下创建和销毁类的实例,同时回收释放的内存空间。MyClass*myClass=newMyClass();删除我的班级;Allocator类支持单继承和多继承。比如Derived类继承自Base类,下面的代码是正确的。Base*base=newDerived;deletebase;runtime运行时,初始化Allocator时,“releaselist”中没有可重用的内存块。因此,第一次调用Allocate()会从内存池或堆中获取内存空间。随着程序的执行,系统继续使用导致分配器抖动的对象。并且只有当释放列表不能提供内存时,才会分配和创建新的内存。最终系统使用的对象实例是固定的,所以每次内存分配都会使用现有的内存空间,而不是从内存池或堆中申请。Allocator比使用内存管理器分配所有对象内存更高效。分配内存时,内存指针只是从“释放列表”中弹出,速度非常快。释放内存时,只是将内存指针放入“释放列表”,速度很快。基准在WindowsPC上使用Allocator和全局堆内存进行的比较性能测试显示了Allocator的高性能。测试20000个4096和2048大小的内存块的分配和释放,测试分配和释放内存的速度。所测试的算法在附件中的代码中有详细说明。分配器模式运行基准时间(mS)全局堆调试堆11640全局堆调试堆21864全局堆调试堆31855全局堆释放堆155全局堆释放堆247全局堆释放堆347分配器静态池119分配器静态池27分配器静态池37分配器堆块130分配器堆块27分配器堆块37Windows在调试模式下执行时使用调试堆内存。调试堆内存添加了会降低性能的额外安全检查。释放的堆内存性能更好,因为没有使用安全检查。通过在VisualStudio项目选项的[Debug]-[Environment]中设置_NO_DEBUG_HEAP=1来禁用调试内存模式。全局调试堆内存模式平均耗时1.8秒,最慢。内存模式的释放时间约为50毫秒,稍快一些。基准测试场景非常简单。在实践中,不同大小的内存块和随机申请和释放可能会产生不同的结果。然而,最简单的也是最有说服力的。内存管理器比Allocator内存分配器慢,并且在很大程度上取决于平台的实现能力。内存分配器Allocator采用静态内存模型,不依赖于堆内存的分配。一旦“空闲列表”包含内存块,其执行时间约为7毫秒。第一次用了19毫秒,防止内存池中的内存被Allocator分配器管理。当Aloocator使用堆内存模式时,当“释放列表”中有可重用内存时,其速度与静态内存模式一样快。堆内存模型依赖于全局堆来获取内存块,而是从一个“空闲列表”中回收内存。第一次需要申请堆内存,需要30毫秒。由于“空闲列表”中内存的重用,后续请求仅需7毫秒。以上基准测试结果表明,Allocator内存分配器效率更高,速度是Windows全局释放堆内存模式的7倍。对于嵌入式系统,我使用Keil在ARMSTM32F4CPU(168Hz)上运行相同的测试。由于资源限制,我将内存块的数量减少到500个,并将单个内存块的大小减少到32和16字节。以下是结果:分配器模式运行基准时间(mS)全局堆版本111.6全局堆版本211.6全局堆版本311.6分配器静态池10.85分配器静态池20.79分配器静态池30.79分配器Blocs1堆1块。20.79分配器堆块30.79基于ARM的基准测试表明,使用Allocator分配器的类性能要快15倍。这个结果会让Keil堆内存的性能相形见绌。基准测试分配了500个大小为16字节的内存块用于测试。每删除16字节内存后,再申请500个32字节内存块。全局堆内存需要11.6毫秒,并且在内存碎片之后,如果没有安全检查,内存管理器可能会花费更多时间。分配器解决方案第一个决定是您是否需要使用分配器。如果你的项目不关心执行速度和是否需要容错,那么你可能不需要自定义分配器,全局堆分配管理器就足够了。另一方面,如果您需要考虑执行速度和容错管理,分配器就会发挥作用。你需要根据项目的需要来选择allocator的模式。关键任务系统的设计可能要求使用全局堆内存。然而,动态分配内存在设计上可能更高效、更优雅。这种情况下,可以在调试和开发时使用堆内存模式获取内存使用参数,然后在发布时切换到静态内存池模式,避免内存分配带来的性能消耗。一些编译时宏可用于模式切换。或者,堆内存模型可能更适合应用程序。此模式利用堆获取新内存,同时防止堆碎片错误。当“释放列表”链接足够多的内存块时,可以加快内存分配效率。涉及未在源代码中实现的多线程的问题超出了本文的范围。系统运行一段时间后,可以方便地使用GetlockCount函数和GetName函数获取内存块的个数和名称。这些指标提供有关内存分配的信息。申请尽可能多的内存,给分配磁盘一些弹性,避免内存耗尽。调试内存泄漏调试内存泄漏非常困难,因为堆内存就像一个黑盒子,对于分配对象的类型和大小是不可见的。使用分配器,内存泄漏检查变得更容易一些,因为分配器会跟踪内存块的总数。为每个分配器实例重复GetBlockCount和GetName的输出(例如输出到终端)并比较它们可以让我们更好地理解分配器分配的内存。错误处理使用C++中的new_handler函数来处理内存分配错误。如果内存管理器在分配内存时遇到错误,将调用用户的错误处理函数。通过将用户错误处理函数的地址复制到new_handler,内存管理器可以调用用户定义的错误处理程序。为了使Allocator类的错误处理机制与内存管理器保持一致,分配器还通过new_handler调用错误处理函数来集中处理所有的内存分配错误。staticvoidout_of_memory(){//当池内存不足时由分配器调用的新处理函数assert(0);}int_tmain(intargc,_TCHAR*argv[]){std::set_new_handler(out_of_memory);...受限分配器类不支持数组对象的内存分配。不能保证为每个对象创建单独的内存,因为多次调用new不能保证内存块的连续性,而数组又需要这一点。所以Allocator只支持固定大小内存块的分配,对象数组不支持。移植问题Allocator在静态内存池耗尽时调用new_handle指向的函数,不适合某些系统。如果new_handle函数没有返回,比如死循环或者断言,调用这个函数是没有效果的。这在使用固定内存池时没有帮助。如需进一步阅读,请阅读相关文章:用快速的固定大小分配器替换malloc/free,了解如何使用Allocator更快地替换C++运行时工具中的malloc和free函数。下载源码:下载Allocator.zip–5.4KB翻译链接:http://www.codeceo.com/article/efficient-cpp-memory-allocator.html英文原文:AnEfficientC++FixedBlockMemoryAllocator