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

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

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

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

执行结果如下:

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

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

猜你喜欢

转载自blog.csdn.net/baidu_38172402/article/details/88803840