c++内存分配优先使用内存池,而不是new,delete
容健行@2008-2-1
转载请注明出处
trackback:http://www.devdiv.net/home/space.php?uid=125&do=blog&id=364
认识一下new和delete的开销:
new和delete首先会转调用到malloc和free,这个大家应该很熟识了。很多人认为malloc是一个很简单的操作,其实巨复杂,它会执行一个系统调用(当然不是每一次,windows上是按页算),该系统调用会锁住内存硬件,然后通过链表的方式查找空闲内存,如果找到大小合适的,就把用户的进程地址映射到内存硬件地址中,然后释放锁,返回给进程。
如果在多线程环境下,进程内的分配也会上锁,跟上面类似,不过不是以页,而是以分配的内存为单位。
delete是一个反过程。
相对的,如果不是使用堆分配,而是直接在栈上分配,比如类型int,那么开销就是把sp这个寄存器加上sizeof(int)。
内存池模式:
内存池就是预先分配好,放到进程空间的内存块,用户申请与释放内存其实都是在进程内进行,SGI-STL的alloc遇到小对象时就是基于内存池的。只有当内存池空间不够时,才会再从系统找一块很大的内存。
内存池模式是如此之重要,以至于让我想不明白为什么四人帮那本《设计模式》没有把内存池列为基本模式,目前其它的教材,包括学院教材,实践教材都没有列出这个模式(讲线程池模式的教材倒非常多)。可能他们认为这不属于设计,而属于具体实现吧。但我觉得这样的后果是间接把很多c++ fans带向低效的编码方式。
sun公司就挺喜欢搞一些算法,用c++实现与java实现一遍,结果显示c++的效率有时甚至比java低,很多c++高手看了之后都会觉得很难解,其实有玄机:java的new其实是基于内存池的,而c++的new是直接系统调用。
c++内存池模式的发展:
c++98标准之前,基本上大多数程序员没用使用内存池,c++98 标准之后,内存池的使用也只是停留在STL内部的使用上,并没有得到推广。
其实我认为,STL的内存分配模式是一场变革,它不但包含内存分配的革命,也包含了内存管理(这个话题先放一边)的革命,只是这场变革被很多人忽略了。也有一些人认为STL的内存分配方案有潜在问题,就是只管从系统分配,但却永远不会调用系统级的释放,如果使用不当,程序拿住的内存会越来越多。我自己工作过的项目没遇上过这样的问题,但之前营帐报表组的一个容灾项目倒是遇上了。不过STL的内存模式没有推广最大的原因还是因为alloc不是标准组件,以至于被人忽略了。
STL之后,一些c++ fans们开始搞出了几套内部使用的内存池。为了项目需要,我自己也曾经做过一个。但这些都没有很正式的公开,而且也不完美。
大概在200x年(-_-!),主导c++标准的一群牛人发起了一个叫boost的项目,才正式的把内存池带到实用与标准化阶段。
插入一点题外话:关于boost,很多人(包括我自己也曾经)产生误解,认为它是准标准库,是下一代标准库。其实boost是套基础建设,用来证明哪些方案是可行,哪些是不可行的,它里面的一些组件有可能会出局,也有可能不是以库的方式存在,而是以语言核心的方式存在,下一代标准库名字叫TR1,再一下代叫TR2(我对使用TR这个名字很费解,为什么不统一叫STL呢)。
new,delete调用与内存池调用的效率对比:
讲了这么多费话,要到关键时候了,用事例来证明为什么要优先使用内存池。下面这段代码是我很久以前的一段测试案例,细节上可能有点懂难,但流程还是清晰的:
#include <time.h>
#include <boost/pool/object_pool.hpp>
struct CCC
{
CCC() {}
char data[10];
};
struct SSS
{
SSS() {}
short data[10];
};
struct DDD
{
DDD() {}
double data[10];
};
// 把new,delete封装为一个与boost::object_pool一样的接口,以便于测试
template <typename element_type, typename user_allocator = boost::default_user_allocator_malloc_free>
class new_delete_alloc
{
public:
element_type* construct() { return new element_type; }
void destroy(element_type* const chunk) { delete chunk; }
};
template
<
template<typename, typename>
class allocator
>
double test_allocator()
{
// 使用了一些不规则的分配与释放,增加内存管理的负担
// 但总体流程还是很规则的,基本上不产生内存碎片,要不然反差效果会更大。
allocator<CCC> c_allc;
allocator<SSS> s_allc;
allocator<DDD> d_allc;
double re = 0; // 随便作一些运算,仿止编译器优化掉内存分配的代码
for (unsigned int i = 0; i < 10000; ++i)
{
for (unsigned int j = 0; j < 10000; ++j)
{
CCC* pc = c_allc.construct();
SSS* ps = s_allc.construct();
re += pc->data[2];
c_allc.destroy(pc);
DDD* pd = d_allc.construct();
re += ps->data[2];
re += pd->data[2];
s_allc.destroy(ps);
d_allc.destroy(pd);
}
}
return re;
}
int main(int argc, char* argv[])
{
double re1 = 0;
double re2 = 0;
// 运行内存池测试时,基本上对我机器其它进程没什么影响
time_t begin = time(0);
re1 = test_allocator<boost::object_pool>(); // 使用内存池boost::object_pool
time_t seporator = time(0);
// 运行到系统调用测试时,感觉机器明显变慢,
// 如果再加上内存碎片的考虑,对其它进程的影响会更大。
std::cout << long(seporator - begin) << std::endl;
re2 = test_allocator<new_delete_alloc>(); // 直接系统调用
std::cout << long(time(0) - seporator) << std::endl;
std::cout << re1 << re2 << std::endl;
}
总结:
在一个100000000次的循环中,使用内存池是3秒,使用系统调用是93秒。
可能会有人觉得100000000这个数很大,93秒没什么,但想一下,一个表有几千万行是很正常的,如果每行有十多列,每列有数据类型,数据长度,数据内容。如果在这样的一个循环错误的使用了new和delete。
而且以上测试还没有考虑到碎片的影响,以及运行该程序时对其它程序的影响。而且还有一点,就是机器的内存硬件容量越大,内存分配时,需要搜索的时间就可能越长,如果内存是多条共同工作的,影响就再进一步。
什么算是错误的使用呢,比如返回一个std::string给用户,有人觉得new出来返回指针给用户会更好,你可能会想到如果new的话,只产生一次string的构造,如果直接返回对象可能需要多次构造,所以new效率更高。但事实不是这样,虽然在构造里会有字符串的分配,但其实这个分配是在内存池中进行的,而你直接的那个new就肯定是系统调用。
当然,有些情况是不可说用什么就用什么的,但如果可选的话,优先使用栈上的对象,其次考虑内存池,然后再考虑系统调用。
相关推荐
内存管理主要包括动态内存分配和释放,其中new和delete是C++中进行动态内存操作的关键关键字。 首先,理解new和delete的基本用法是至关重要的。new用于在堆上分配内存,同时调用构造函数来初始化新创建的对象。例如...
- 对于大对象,考虑使用池内存分配策略以减少碎片并提高效率。 - 使用`std::make_unique`和`std::make_shared`替代直接的`new`操作,以避免裸指针和忘记`delete`的问题。 最后,对于复杂的内存管理问题,C++11及...
如果分配的是对象数组,记得使用`delete[]`而不是单个`delete`。 2. **智能指针**:C++11引入了智能指针(如`std::unique_ptr`,`std::shared_ptr`和`std::weak_ptr`),它们自动管理所指向的对象的生命周期,避免...
例如,对于频繁使用的小型对象,应优先考虑栈存储,以减少内存分配的开销;而对于大型对象或需要长期存在的对象,则更适合使用堆存储。同时,使用智能指针如`std::unique_ptr`和`std::shared_ptr`可以自动管理堆内存...
### C++内存的动态申请与释放 #### 一、引言 在C++编程中,内存管理是一项重要的技能。合理地管理和使用内存不仅能提高程序的性能,还能避免各种潜在的错误,如内存泄漏等问题。本文将详细介绍C++中两种常用的内存...
C++允许程序员直接控制内存分配和释放,因此理解如何查看和分析内存使用对于优化程序性能、避免内存泄漏以及提升软件稳定性具有重要意义。 本代码示例提供了查看内存使用情况的功能,它可能是通过操作系统提供的API...
在实际编程中,建议优先使用`new/delete`来管理对象的内存,而对于简单的数据类型则可以根据情况选择`malloc/free`或`new/delete`。同时,开发者还需要遵循良好的编码习惯,比如对指针的有效性进行检查、合理初始化...
- **堆内存**:通过`new`操作符动态分配,手动通过`delete`释放。适用于生命周期长或大小不固定的对象,但过度使用可能导致内存泄漏。 - **静态内存**:全局变量和静态局部变量存储于此,生命周期贯穿整个程序运行...
C++中的new和delete操作符用于动态内存的申请和释放。 4. 请求页式存储管理: 请求页式存储管理是一种内存管理策略,它将物理内存分割成固定大小的页,同时将磁盘上的虚拟地址空间也划分为页。当进程需要访问的...
在开发Windows应用程序时,Visual C++(简称VC)是一个常用工具,而内存管理是程序设计中的关键环节。内存泄露是程序开发中常见的问题,当程序动态分配了内存但未能正确释放时,就会导致内存泄露。长期积累,内存...
在编程语言C和C++中,内存...在C++中,建议优先使用new和delete,而不是直接使用malloc和free,尤其是在处理类对象时,这样做可以充分利用C++的语言特性和优势,确保资源的正确初始化和释放,有效避免内存泄漏等问题。
在C++代码中,使用了`malloc`来动态分配内存,`new`关键字用于申请特定类型数据的内存空间,而`delete`关键字用于释放内存。这与操作系统中的内存管理机制相呼应,虽然在实际操作系统中,这些操作由更底层的库函数或...
- `malloc` 只负责分配内存,不调用构造函数,且在分配失败时不会调用内存分配失败处理程序 `new_handler`,而 `new` 会。 - 通常建议优先使用 `new`,除非有特殊需求。 4. **`new` 的三种形态** - **`new ...
Cocos2D-X中,大部分对象使用new运算符在堆上分配内存,因此需要使用delete来释放。不恰当的内存分配和释放可能导致内存泄漏或悬挂指针。 3. 引用计数:Cocos2D-X引入了引用计数机制,每个对象都有一个引用计数。当...
2. **优先使用替代**:C++的`iostream`库提供了更高级别的输入输出操作,支持自定义类型的输入输出,比传统的`scanf`和`printf`更灵活。 3. **使用new和delete替代malloc和free**:`new`和`delete`能够调用对象的...
这是因为C++的异常处理机制负责调用匹配的`operator delete`来释放内存,而不是程序员直接调用。当构造函数在堆中创建对象时抛出异常并被捕获,匹配的`operator delete`会被自动调用以释放内存。如果找不到匹配的`...
- **使用new和delete代替malloc和free**:C++的`new`和`delete`操作符不仅分配和释放内存,还调用构造函数和析构函数,这使得资源管理更加自动化和安全。 - **一致使用new和delete**:在类中,如果成员变量通过`new`...
在这里,我们将深入探讨动态内存分配的概念、使用方法以及它在实际编程中的应用。 动态内存分配允许程序员在程序执行期间决定内存的大小和生命周期。在C++中,我们通常使用以下四个函数进行动态内存管理: 1. **`...
相比之下,C语言的`malloc`和`free`仅处理内存分配与释放,不涉及对象的构造与析构,因此使用`new`和`delete`更符合C++的对象导向编程理念。 3. **在析构函数中对指针成员调用delete**: 当对象被销毁时,析构函数...