频繁地分配和回收内存会严重地降低程序的性能。性能降低的原因在于默认的内存管理是通用的。应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价。通过开发专用的内存管理器可以解决这个问题。对专用内存管理器的设计可以从多个角度考虑。我们至少可以想到两个方面:大小和并发。

从大小的角度分为以下两种:

(1)、固定大小:分配固定大小内存块的内存管理器。

(2)、可变大小:分配任意大小内存块的内存管理器。所请求分配的大小事先是未知的。

类似的,从并发的角度也分为以下两种:

(1)、单线程:内存管理器局限在一个线程内。内存被一个线程使用,并且不越出该线程的界限。这种内存管理器不涉及相互访问的多线程。

(2)、多线程:内存管理器被多个线程并发地使用。这种实现需要包含互斥执行的代码段。无论什么时候,只能有一个线程在执行一个代码段。

全局函数new()和delete():从设计上来说,默认的内存管理器是通用的。当调用全局函数new()和delete()时,我们使用的正是默认内存管理器。这两个函数的实现不能作任何简化假设。它们在进程范围内管理内存。既然一个进程可能产生多个线程,new()和delete()就必须能够在多线程环境中运行。而且,每次请求分配的内存大小是不同的。这种灵活性以速度为代价。所做的计算越多,消耗的周期就越多。

通常情况下,客户端代码不需要全局函数new()和delete()的全部功能。它可能只(或通常)需要特定大小的内存块。客户端代码很可能在单线程环境中运行,在这种环境中并不真正需要默认的new()和delete()所提供的并发保护。这样的话,使用这两个函数的全部功能就会浪费CPU周期。通过调整内存分配策略来更好地匹配特定需求,可以明显地提高性能。

灵活性以速度的降低为代价.随着内存管理的功能和灵活性的增强,执行速度将降低.

全局内存管理器(由new()和delete()执行)是通用的,因此代价高。

专用内存管理器比全局内存管理器快一个数量级以上。

如果主要分配固定大小的内存块,专用的固定大小内存管理器将明显地提升性能。

如果主要分配限于单线程的内存块,那么内存管理器也会有类似的性能提高。由于省去了全局函数new()和delete()必须处理的并发问题,单线程内存管理器的性能有所提高。

以下是测试代码(single_threaded_memory_pool.cpp):

  1. #include "single_threaded_memory_pool.hpp"
  2. #include <iostream>
  3. #include <chrono>
  4. #include <string>
  5. namespace single_threaded_memory_pool_ {
  6. // reference: 《提高C++性能的编程技术》:第六章:单线程内存池
  7. class Rational1 {
  8. public:
  9. Rational1(int a = 0, int b = 1) : n(a), d(b) {}
  10. private:
  11. int n; // 分子
  12. int d; // 分母
  13. }; // class Rational1
  14. // 专用Rational2内存管理器
  15. // 为避免频繁地使用内存管理器,Rational2类要维护一个预分配的Rational2对象的静态连接列表,该列表列出空闲的可用对象.
  16. // 当需要Rational2对象时,可以从空闲列表中取出一个,使用后再把它放回空闲列表以便今后分配.
  17. // 声明一个辅助结构来连接空闲列表的相邻元素
  18. class NextOnFreeList {
  19. public:
  20. NextOnFreeList* next;
  21. }; // class NextOnFreeList
  22. // 空闲列表被声明为一个由NextOnFreeList元素组成的列表
  23. class Rational2 {
  24. public:
  25. Rational2(int a = 0, int b = 1) : n(a),d(b) {}
  26. inline void* operator new(size_t size);
  27. inline void operator delete(void* doomed, size_t size);
  28. static void newMemPool() { expandTheFreeList(); }
  29. static void deleteMemPool();
  30. private:
  31. static NextOnFreeList* freeList; // Rational2对象的空闲列表
  32. static void expandTheFreeList();
  33. enum { EXPANSION_SIZE = 32};
  34. int n; // 分子
  35. int d; // 分母
  36. }; // class Rational2
  37. NextOnFreeList* Rational2::freeList = nullptr;
  38. // new()在空闲列表中分配一个Rational2对象.如果列表为空,则扩展列表
  39. inline void* Rational2::operator new(size_t size)
  40. {
  41. if (nullptr == freeList) { // 如果列表为空,则将其填满
  42. expandTheFreeList();
  43. }
  44. NextOnFreeList* head = freeList;
  45. freeList = head->next;
  46. return head;
  47. }
  48. // delete()把Rational2对象直接添加到空闲列表的头部,以返回Rational2对象
  49. inline void Rational2::operator delete(void* doomed, size_t size)
  50. {
  51. NextOnFreeList* head = static_cast<NextOnFreeList*>(doomed);
  52. head->next = freeList;
  53. freeList = head;
  54. }
  55. // 当空闲列表用完后,需要从堆上分配更多的Rational2对象.
  56. // Rational2和NextOnFreeList之间的类型转换是有危险的,必须确保空闲列表的元素足够大以支持任意一种类型
  57. // 当我们用Rational2对象填充空闲列表时,要记得比较Rational2和NextOnFreeList的大小,并且分配较大的那一个
  58. void Rational2::expandTheFreeList()
  59. {
  60. // 本质上,expandTheFreeList的实现并不是最优的.因为空闲列表中每增加一个元素,就调用一次new. 如果只调用一次new
  61. // 获得一大块内存,然后把它切分给多个元素,这样会更高效。孤立地看,这种想法很正确。然而我们创建内存管理器时,
  62. // 认为它不会频繁扩展和收缩,否则必须重新查看代码实现并修正它
  63. // 我们必须分配足够大的对象以包含下一个指针
  64. size_t size = sizeof(Rational2) > sizeof(NextOnFreeList*) ? sizeof(Rational2) : sizeof(NextOnFreeList*);
  65. NextOnFreeList* runner = static_cast<NextOnFreeList*>((void*)new char[size]);
  66. freeList = runner;
  67. for (int i = 0; i < EXPANSION_SIZE; ++i) {
  68. runner->next = static_cast<NextOnFreeList*>((void*)new char[size]);
  69. runner = runner->next;
  70. }
  71. runner->next = nullptr;
  72. }
  73. void Rational2::deleteMemPool()
  74. {
  75. NextOnFreeList* nextPtr;
  76. for (nextPtr = freeList; nextPtr != nullptr; nextPtr = freeList) {
  77. freeList = freeList->next;
  78. delete [] nextPtr;
  79. }
  80. }
  81. ///
  82. // 固定大小对象的内存池: 观察Rational2内存管理器的实现,会很清楚地发现内存管理逻辑实际上独立于特定的Rational2类.
  83. // 它唯一依赖的是类对象的大小----这正是用模板实现内存池的原因
  84. template<class T>
  85. class MemoryPool1 {
  86. public:
  87. MemoryPool1(size_t size = EXPANSION_SIZE);
  88. ~MemoryPool1();
  89. // 从空闲列表中分配T元素
  90. inline void* alloc(size_t size);
  91. // 返回T元素到空闲列表中
  92. inline void free(void* someElement);
  93. private:
  94. // 空闲列表的下一元素
  95. MemoryPool1<T>* next;
  96. // 如果空闲列表为空,按该大小扩展它
  97. enum { EXPANSION_SIZE = 32 };
  98. // 添加空闲元素至空闲列表
  99. void expandTheFreeList(int howMany = EXPANSION_SIZE);
  100. };
  101. // 构造函数初始化空闲列表,参数size指定空闲列表的初始化长度
  102. template<class T>
  103. MemoryPool1<T>::MemoryPool1(size_t size)
  104. {
  105. expandTheFreeList(size);
  106. }
  107. // 析构函数遍历空闲列表并且删除所有元素
  108. template<class T>
  109. MemoryPool1<T>::~MemoryPool1()
  110. {
  111. MemoryPool1<T>* nextPtr = next;
  112. for (nextPtr = next; nextPtr != nullptr; nextPtr = next) {
  113. next = next->next;
  114. delete [] static_cast<char*>(static_cast<void*>(nextPtr));
  115. }
  116. }
  117. // alloc函数为T元素分配足够大的空间,如果空闲列表用尽,则调用expandThrFreeList函数来扩充它
  118. template<class T>
  119. inline void* MemoryPool1<T>::alloc(size_t size)
  120. {
  121. if (!next) {
  122. expandTheFreeList();
  123. }
  124. MemoryPool1<T>* head = next;
  125. next = head->next;
  126. return head;
  127. }
  128. // free函数把T元素放回空闲列表,以此来释放它
  129. template<class T>
  130. inline void MemoryPool1<T>::free(void* doomed)
  131. {
  132. MemoryPool1<T>* head = static_cast<MemoryPool1<T>*>(doomed);
  133. head->next = next;
  134. next = head;
  135. }
  136. // expandTheFreeList函数用来向空闲列表添加新元素,首先从堆上分配新元素,然后把它们连接到列表中
  137. // 该函数在空闲列表用尽时被调用
  138. template<class T>
  139. void MemoryPool1<T>::expandTheFreeList(int howMany)
  140. {
  141. // 必须分配足够大的对象以包含下一个指针
  142. size_t size = sizeof(T) > sizeof(MemoryPool1<T>*) ? sizeof(T) : sizeof(MemoryPool1<T>*);
  143. MemoryPool1<T>* runner = static_cast<MemoryPool1<T>*>((void*)(new char[size]));
  144. next = runner;
  145. for (int i = 0; i < howMany; ++i) {
  146. runner->next = static_cast<MemoryPool1<T>*>((void*)(new char[size]));
  147. runner = runner->next;
  148. }
  149. runner->next = nullptr;
  150. }
  151. // Rational3类不再需要维护它自己的空闲列表,这项任务委托给了MemoryPool1类
  152. class Rational3 {
  153. public:
  154. Rational3(int a = 0, int b = 1) : n(a),d(b) {}
  155. void* operator new(size_t size) { return memPool->alloc(size); }
  156. void operator delete(void* doomed, size_t size) { memPool->free(doomed); }
  157. static void newMemPool() { memPool = new MemoryPool1<Rational3>; }
  158. static void deleteMemPool() { delete memPool; }
  159. private:
  160. int n; // 分子
  161. int d; // 分母
  162. static MemoryPool1<Rational3>* memPool;
  163. };
  164. MemoryPool1<Rational3>* Rational3::memPool = nullptr;
  165. /
  166. // 单线程可变大小内存管理器:
  167. // MemoryChunk类取代之前版本中使用的NextOnFreeList类,它用来把不同大小的内存块连接起来形成块序列
  168. class MemoryChunk {
  169. public:
  170. MemoryChunk(MemoryChunk* nextChunk, size_t chunkSize);
  171. // 析构函数释放构造函数获得的内存空间
  172. ~MemoryChunk() { delete [] mem; }
  173. inline void* alloc(size_t size);
  174. inline void free(void* someElement);
  175. // 指向列表下一内存块的指针
  176. MemoryChunk* nextMemChunk() { return next; }
  177. // 当前内存块剩余空间大小
  178. size_t spaceAvailable() { return chunkSize - bytesAlreadyAllocated; }
  179. // 这是一个内存块的默认大小
  180. enum { DEFAULT_CHUNK_SIZE = 4096 };
  181. private:
  182. MemoryChunk* next;
  183. void* mem;
  184. // 一个内存块的默认大小
  185. size_t chunkSize;
  186. // 当前内存块中已分配的字节数
  187. size_t bytesAlreadyAllocated;
  188. };
  189. // 构造函数首先确定内存块的适当大小,然后根据这个大小从堆上分配私有存储空间
  190. // MemoryChunk将next成员指向输入参数nextChunk, nextChunk是列表先前的头部
  191. MemoryChunk::MemoryChunk(MemoryChunk* nextChunk, size_t reqSize)
  192. {
  193. chunkSize = (reqSize > DEFAULT_CHUNK_SIZE) ? reqSize : DEFAULT_CHUNK_SIZE;
  194. next = nextChunk;
  195. bytesAlreadyAllocated = 0;
  196. mem = new char[chunkSize];
  197. }
  198. // alloc函数处理内存分配请求,它返回一个指针,该指针指向mem所指向的MemoryChunk私有存储空间中的可用空间。
  199. // 该函数通过更新该块中已分配的字节数来记录可用空间的大小
  200. void* MemoryChunk::alloc(size_t requestSize)
  201. {
  202. void* addr = static_cast<void*>(static_cast<char*>(mem) + bytesAlreadyAllocated);
  203. bytesAlreadyAllocated += requestSize;
  204. return addr;
  205. }
  206. // 在该实现中,不用担心空闲内存段的释放。当对象被删除后,整个内存块将被释放并且返回到堆上
  207. inline void MemoryChunk::free(void* doomed)
  208. {
  209. }
  210. // MemoryChunk只是一个辅助类,ByteMemoryPoll类用它来实现可变大小的内存管理
  211. class ByteMemoryPool {
  212. public:
  213. ByteMemoryPool(size_t initSize = MemoryChunk::DEFAULT_CHUNK_SIZE);
  214. ~ByteMemoryPool();
  215. // 从私有内存池分配内存
  216. inline void* alloc(size_t size);
  217. // 释放先前从内存池中分配的内存
  218. inline void free(void* someElement);
  219. private:
  220. // 内存块列表,它是我们的私有存储空间
  221. MemoryChunk* listOfMemoryChunks = nullptr;
  222. // 向我们的私有存储空间添加一个内存块
  223. void expandStorage(size_t reqSize);
  224. };
  225. // 虽然内存块列表可能包含多个块,但只有第一块拥有可用于分配的内存。其它块表示已分配的内存。
  226. // 列表的首个元素是唯一能够分配可以内存的块。
  227. // 构造函数接收initSize参数来设定一个内存块的大小,即构造函数借此来设置单个内存块的大小。
  228. // expandStorage方法使listOfMemoryChunks指向一个已分配的MemoryChunk对象
  229. // 创建ByteMemoryPool对象,生成私有存储空间
  230. ByteMemoryPool::ByteMemoryPool(size_t initSize)
  231. {
  232. expandStorage(initSize);
  233. }
  234. // 析构函数遍历内存块列表并且删除它们
  235. ByteMemoryPool::~ByteMemoryPool()
  236. {
  237. MemoryChunk* memChunk = listOfMemoryChunks;
  238. while (memChunk) {
  239. listOfMemoryChunks = memChunk->nextMemChunk();
  240. delete memChunk;
  241. memChunk = listOfMemoryChunks;
  242. }
  243. }
  244. // alloc函数确保有足够的可用空间,而把分配任务托付给列表头的MemoryChunk
  245. void* ByteMemoryPool::alloc(size_t requestSize)
  246. {
  247. size_t space = listOfMemoryChunks->spaceAvailable();
  248. if (space < requestSize) {
  249. expandStorage(requestSize);
  250. }
  251. return listOfMemoryChunks->alloc(requestSize);
  252. }
  253. // 释放之前分配的内存的任务被委派给列表头部的MemoryChunk来完成
  254. // MemoryChunk::free不做任何事情,因为ByteMemoryPool的实现不会重用之前分配的内存。如果需要更多内存,
  255. // 我们将创建新的内存块以便今后分配使用。在内存池被销毁时,内存释放回堆中。ByteMemoryPool析构函数
  256. // 释放所有的内存块到堆中
  257. inline void ByteMemoryPool::free(void* doomed)
  258. {
  259. listOfMemoryChunks->free(doomed);
  260. }
  261. // 若遇到内存块用尽这种不太可能的情况,我们通过创建新的内存块并把它添加到内存块列表的头部来扩展它
  262. void ByteMemoryPool::expandStorage(size_t reqSize)
  263. {
  264. listOfMemoryChunks = new MemoryChunk(listOfMemoryChunks, reqSize);
  265. }
  266. class Rational4 {
  267. public:
  268. Rational4(int a = 0, int b = 1) : n(a),d(b) {}
  269. void* operator new(size_t size) { return memPool->alloc(size); }
  270. void operator delete(void* doomed, size_t size) { memPool->free(doomed); }
  271. static void newMemPool() { memPool = new ByteMemoryPool; }
  272. static void deleteMemPool() { delete memPool; }
  273. private:
  274. int n; // 分子
  275. int d; // 分母
  276. static ByteMemoryPool* memPool;
  277. };
  278. ByteMemoryPool* Rational4::memPool = nullptr;
  279. /
  280. int test_single_threaded_memory_pool_1()
  281. {
  282. using namespace std::chrono;
  283. high_resolution_clock::time_point time_start, time_end;
  284. const int cycle_number1{10000}, cycle_number2{1000};
  285. { // 测试全局函数new()和delete()的基准性能
  286. Rational1* array[cycle_number2];
  287. time_start = high_resolution_clock::now();
  288. for (int j =0; j < cycle_number1; ++j) {
  289. for (int i =0; i < cycle_number2; ++i) {
  290. array[i] = new Rational1(i);
  291. }
  292. for (int i = 0; i < cycle_number2; ++i) {
  293. delete array[i];
  294. }
  295. }
  296. time_end = high_resolution_clock::now();
  297. fprintf(stdout, "global function new/delete time spent: %f seconds\n",(duration_cast<duration<double>>(time_end - time_start)).count());
  298. }
  299. { // 专用Rational2内存管理器测试
  300. Rational2* array[cycle_number2];
  301. time_start = high_resolution_clock::now();
  302. Rational2::newMemPool();
  303. for (int j = 0; j < cycle_number1; ++j) {
  304. for (int i = 0; i < cycle_number2; ++i) {
  305. array[i] = new Rational2(i);
  306. }
  307. for (int i = 0; i < cycle_number2; ++i) {
  308. delete array[i];
  309. }
  310. }
  311. Rational2::deleteMemPool();
  312. time_end = high_resolution_clock::now();
  313. fprintf(stdout, "specialized rational2 memory manager time spent: %f seconds\n",(duration_cast<duration<double>>(time_end - time_start)).count());
  314. }
  315. { // 固定大小对象的内存池测试
  316. Rational3* array[cycle_number2];
  317. time_start = high_resolution_clock::now();
  318. Rational3::newMemPool();
  319. for (int j = 0; j < cycle_number1; ++j) {
  320. for (int i = 0; i < cycle_number2; ++i) {
  321. array[i] = new Rational3(i);
  322. }
  323. for (int i = 0; i < cycle_number2; ++i) {
  324. delete array[i];
  325. }
  326. }
  327. Rational3::deleteMemPool();
  328. time_end = high_resolution_clock::now();
  329. fprintf(stdout, "fixed-size object memory pool time spent: %f seconds\n",(duration_cast<duration<double>>(time_end - time_start)).count());
  330. }
  331. { // 单线程可变大小内存管理器测试
  332. Rational4* array[cycle_number2];
  333. time_start = high_resolution_clock::now();
  334. Rational4::newMemPool();
  335. for (int j = 0; j < cycle_number1; ++j) {
  336. for (int i = 0; i < cycle_number2; ++i) {
  337. array[i] = new Rational4(i);
  338. }
  339. for (int i = 0; i < cycle_number2; ++i) {
  340. delete array[i];
  341. }
  342. }
  343. Rational4::deleteMemPool();
  344. time_end = high_resolution_clock::now();
  345. fprintf(stdout, "single-threaded variable-size memory manager time spent: %f seconds\n",(duration_cast<duration<double>>(time_end - time_start)).count());
  346. }
  347. return 0;
  348. }
  349. } // namespace single_threaded_memory_pool_

执行结果如下:

GitHub: https://github.com/fengbingchun/Messy_Test

转载自:https://blog.csdn.net/fengbingchun/article/details/84497625

Multi-thread提高C++性能的编程技术笔记:单线程内存池+测试代码相关推荐

  1. 提高C++性能的编程技术笔记:编码优化+测试代码

    缓存:在现代处理器中,缓存经常与处理器中的数据缓存和指令缓存联系在一起.缓存主要用来存储使用频繁而且代价高昂的计算结果,这样就可以避免对这些结果的重复计算.如,循环内对常量表达式求值是一种常见的低性能 ...

  2. 提高C++性能的编程技术笔记:引用计数+测试代码

    引用计数(reference counting):基本思想是将销毁对象的职责从客户端代码转移到对象本身.对象跟踪记录自身当前被引用的数目,在引用计数达到零时自行销毁.换句话说,对象不再被使用时自行销毁 ...

  3. 提高C++性能的编程技术笔记:内联+测试代码

    内联类似于宏,在调用方法内部展开被调用方法,以此来代替方法的调用.一般来说表达内联意图的方式有两种:一种是在定义方法时添加内联保留字的前缀:另一种是在类的头部声明中定义方法. 虽然内联方法的调用方式和 ...

  4. 提高C++性能的编程技术笔记:临时对象+测试代码

    类型不匹配:一般情况是指当需要X类型的对象时提供的却是其它类型的对象.编译器需要以某种方式将提供的类型转换成要求的X类型.这一过程可能会产生临时对象. 按值传递:创建和销毁临时对象的代价是比较高的.倘 ...

  5. 提高C++性能的编程技术笔记:跟踪实例+测试代码

    当提高性能时,我们必须记住以下几点: (1). 内存不是无限大的.虚拟内存系统使得内存看起来是无限的,而事实上并非如此. (2). 内存访问开销不是均衡的.对缓存.主内存和磁盘的访问开销不在同一个数量 ...

  6. 提高C++性能的编程技术笔记:总结

    <提高C++性能的编程技术>这本书是2011年出版的,书中有些内容的介绍可能已经过时,已不再适用于现在的C++编程中,但大部分内容还是很有参考意义的. 这里是基于之前所有笔记的简单总结,笔 ...

  7. 提高C++性能的编程技术笔记:设计优化/可扩展性/系统体系结构相关+测试代码

    1. 设计优化 我们可以粗略地将性能优化分为两种类型:编码优化和设计优化.编码优化定义为不需要完整理解要解决的问题或者应用程序的执行流程就能实施的优化.通过定义看出,编码优化用于局部代码,同时该过程不 ...

  8. 提高C++性能的编程技术笔记:标准模板库+测试代码

    标准模板库(Standard Template Library, STL)是容器和通用算法的强效组合. 渐近复杂度:算法的渐近复杂度是对算法性能的近似估计.它是算法集到特定性能标准集的映射.如果需要对 ...

  9. 提高C++性能的编程技术笔记:多线程内存池+测试代码

    为了使多个线程并发地分配和释放内存,必须在分配器方法中添加互斥锁. 全局内存管理器(通过new()和delete()实现)是通用的,因此它的开销也非常大. 因为单线程内存管理器要比多线程内存管理器快的 ...

  10. 提高C++性能的编程技术笔记:单线程内存池+测试代码

    频繁地分配和回收内存会严重地降低程序的性能.性能降低的原因在于默认的内存管理是通用的.应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价.通过开发专用的内存管理器可以解决这个问 ...

最新文章

  1. C++实现int与string之间的相互转换
  2. mysql启动失败的一个解决方法
  3. python正则匹配ip地址_Python正则表达式匹配ip地址实例
  4. C# 时间格式(血淋淋的教训啊。。。)
  5. 定价错误: 必要条件 MWST 丢失
  6. notepad++修改背景色
  7. Spring Boot中使用Spring Security进行安全控制
  8. Bootstrap3 按钮状态提示
  9. 茅台酒是不是勾兑的?
  10. Linux内核源代码分析——中断(一鞭一条痕)(上)
  11. 软件产品三步曲(内容、可用性、视觉)
  12. 地理必修一三大类岩石_90后地理老师4天收700封情书!这波操作,绝了……
  13. 关于RadUpload上传问题总结
  14. 使用UniWebView时的内存管理
  15. 数学基础知识总结 —— 4. 常见函数图像
  16. 实时数仓-数据采集层_1
  17. linux核显显示独显内容,Linux驱动显示Intel第12代核显新特性:显示状态缓存
  18. Word 2016 插入尾注之后删除方法
  19. 开发版linux随身wifi,让linux下无线网卡变身随身wifi
  20. Python - 列表补充(二)

热门文章

  1. MacOS系统自带截图快捷键
  2. 容器编排技术 -- Kubernetes 应用连接到 Service
  3. Python-DDoS攻击
  4. idea中git分支、合并与使用
  5. HTML连载70-相片墙、盒子阴影和文字阴影
  6. Java Socket TeXT_FULL_WRITING 等问题解决
  7. Js Vue 对象数组的创建方式
  8. 关于博客园内嵌入bilibili视频
  9. C#开发笔记之21-C#解析Json(序列化/反序列化)的最佳实践。
  10. 通用职责分配软件原则之7-纯虚构原则