代码先锋网 代码片段及技术文章聚合

《提高C++性能的编程技术》阅读随笔 —— 第六章 单线程内存池

频繁的分配和回收内存会严重降低程序的性能。原因在于默认的内存管理器是通用的、应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价。通过开发专用的内存管理器可以解决这个问题。
两个方面考虑内存管理器的设计:大小和并发
固定大小、可变大小
单线程:内存管理器局限在一个线程内,内存被一个线程使用
多线程:内存管理器被多个线程并发的使用,需包含互斥执行的代码段。无论什么时候,只有一个线程在执行一个代码段

  1. 全局内存管理器(由new()和delete()执行)是通用的。因此代价较高
  2. 专用内存管理器,如单线程内存管理器,其可以省去new和delete的并发问题
    (指的是对于一个类重载实现他的new和delete方面。分配的大小根据sizeof,然后用new char[]来分配,delete[] 来删除)

示例:

  1. 全局的new和delete
class Rational{
public:
  Rational (int a = 0, int b = 1) : n(a), d(b) {}
private:
  int n; //分子
  int d; //分母
};

Rational *array = new Rational(i);
delete array;  // 循环执行100万次,1500ms

版本1:专用的Rational内存管理器
避免频繁使用默认内存管理器,Rational需要维护一个预先分配的Rational对象的静态链接链表,列出空闲的可用对象,当需要Rational对象时,从空闲列表取出一个即可,使用后再把它放回空闲列表以便今后分配。

class NextOnFreeList{
public:
  NextOnFreeList *next;
};

空闲列表被声明为一个有NextOnFreeList元素组成的列表,这样空闲列表的每个元素都声明为NextOnFreeList结构,但他们也是Rational对象。
为了遍历对象,每个Rational对象的前几个字节用于指向空闲列表的下一个对象(next)。可以通过将Rational对象转换成指向NextOnFreeList类型的指针来实现。这样空闲列表双重身份:Rational对象的序列andNextOnFreeList元素的序列

class Rational{
  ...
  static NextOnFreeList *freelist;
};

空闲列表声明为Rational静态成员,Rational的New和delete操作符可以管理该静态列表,重载全局的new和delete。

class Rational{
public:
  inline void *operator new (size_t size);
  inline void operator delete (void *doomed, size_t size);
  static void newMemPool() { expandTheFreeList(); }
  static void deleteMemPool();
private:
  static NextOnFreeList *freelist;
  static void expandTheFreeList();
  enum { EXPANSION_SIZE = 32};
  int n;
  int d;
};

new()是在空闲列表中分配一个Rational对象,列表为空就扩展列表。先去掉空闲列表的头部,调整完指针后再返回head。

inline void* Rational::operator new(szie_t size) {
  if(0 == freelist) {
    expandTheFreeList();
  }
  NextOnFreeList *head = freelist;
  freelist = head->next;
  return head; 
}

delete()把Rational对象直接添加到空闲列表的头部,以返回一个Rational对象。这样每次delete都会使得freelist变为delete调的空间,而其next则指向之前的freelist,因此可以继续形成一条链

inline void Rational::operator delete(void *doomed, size_t size) {
  NextOnFreeList *head = static_cast<NextOnFreeList *> doomed;
  head->next = freelist;
  freelist = head;
}

Rational和NextOnFreeList之间的类型转换有危险。必须确保空闲列表的元素足够大以支持任意一种类型。因此,当我们用Rational对象填充空闲列表时,需要比较两个类型之间的大小,从而分配较大的一个。

void Rational::expandTheFreeList() {
  size_t size = (sizeof(Rational) > sizeof(NextOnFreeList *)) ? 
                sizeof(Rational) : sizeof(NextOnFreeList *);
  NextOnFreeList *runner = static_cast<NextOnFreeList *> new char[size];
  freelist = runner;
  for(int i = 0; i < EXPANSION_SIZE; i++) {
    runner->next = static_cast<NextOnFreeList *> new char[size];
    runner = runner->next;
  }
  runner->next = 0;
}
void Rational::deleteMemPool() {
  NextOnFreeList *nextPtr;
  for(nextPtr = freelist; nextPtr != NULL; nextPtr = freelist) {
    freelist = freelist->next;
    delete []nextPtr;
  }
}

重复性能测试

for(500){
  for(1000){
    array[1-1000] = new Rational(i);
  }
  for(1000){
    delete array[1-1000];
  }

执行时间从1500描述将至43ms。

版本2:固定大小对象的内存池
版本1只可以管理Rational对象,想管理多个对象的话,用模板。因为Rational可以看出,内存管理逻辑是独立于Rational之外的。

template <class T>
class MemoryPool {
public:
  MemoryPool (size_t size = EXPANSION_SIZE) ;
  ~MemoryPool ();
  inline void* alloc (size_t size);
  inline void free (void *someElement);
private:
  MemoryPool<T> *next;
  void expandTheFreeList (int howMany = EXPANSION_SIZE);
};

template <class T>
MemoryPool<T>:: MemoryPool (size_t size = EXPANSION_SIZE) {
  expandTheFreeList(size);
}

template <class T>
MemoryPool<T>::  ~MemoryPool () {
  MemoryPool<T> *nextPtr = next;
  for(nextPtr =next; nextPtr != NULL; nextPtr = next) {
   next = next->next;
    delete []nextPtr;
  }
}

template <class T>
 inline void* MemoryPool<T>:: alloc (size_t size) {
  if(!next) expandTheFreeList();
  MemoryPool<T> *head = next;
  next = head->next;
  return head;
}

template <class T>
inline void MemoryPool<T>:: free (void *doomed) {
  MemoryPool<T> *head = static_cast<MemryPool<T> *> doomed;
  head->next = next;
  next = head;
}

template <class T>
void MemoryPool<T>:: expandTheFreeList (int howMany = EXPANSION_SIZE) {
  size_t size = (sizeof(T) > sizeof(MemoryPool<T> *)) ? 
                sizeof(T) > sizeof(MemoryPool<T> *);
  MemoryPool<T> *runner = static_cast< MemoryPool<T> *> new char[size];
 next = runner;
  for(int i = 0; i < howmany; i++) {
    runner->next = static_cast< MemoryPool<T> *> new char[size];
    runner = runner->next;
  }
  runner->next = 0;
}

上述代码解析:与版本1类似,alloc函数负责扩充,free函数负责把T元素放回空闲列表,以此来释放它。
Rational就不需要再维护自己的空闲列表,因此Rational变为

class Rational{
public:
  Rational (int a = 0, int b = 1) : n(a), d(b) {}
  void* operator new(size_t size) { return memPool->alloc(szie); } 
  void operator delete (void *doomed, size_t size) { memPool->free(doomed); }
  static void newMemPool() { memPool = new MemoryPool<Rational>(); }
  static void deleteMemPool() { delete memPool; }
private:
  static MemryPool<Rational> *memPool;
  int n;
  int d;
};
MemoryPool<Rational> *Rational::memPool = 0;
int main(){
  Rational *array[1000];
  Rational::newMemPool();
  for(01-500)
    for(1-1000)
      array[i] = new Rational(i);
    for(1-1000)
      delete array[i];

  Rational::deleteMemPool();
}

这里的耗时大约是63ms
注意:之前的static_cast的类型转换在编译时可能报错,可以采用强制类型转换

版本3:单线程可变大小的内存管理器
版本2分配的是固定大小的内存,但有些程序需要分配可变大小的内存。
下面是Web服务器中常用的方式:
MemoryChunk类取代了之前版本中使用的NextOnFreeList类,他把不同大小的内存块连接起来形成块序列。
完整代码如下(#####可运行

#include<iostream>
using namespace std;
#include <string>

class MemoryChunk{
public:
    MemoryChunk(MemoryChunk *nextChunk, size_t chunkSize);
    ~MemoryChunk();
    inline void *alloc(size_t size);
    inline void free(void* someElement);
    MemoryChunk *nextMemChunk(){ return next; }
    size_t spaceAvailable(){ return chunkSize - bytesAlreadyAllocated; }
    enum {DEFAULT_CHUNK_SIZE = 4096};
private:
    MemoryChunk *next;
    void *mem;
    size_t chunkSize;
    size_t bytesAlreadyAllocated;
};
// MemoryChunk是NextOnFreeList的一个更为简洁的版本。将next指针从用于已分配对象的实际内存中分离出来。MemoryChunk使用显式的next和mem指针因此不需要转换。
//MemoryChunk首先确定内存块大小(mem指向),然后next链接下一个MemoryChunk内存块
MemoryChunk::MemoryChunk(MemoryChunk *nextChunk, size_t reqSize){
    chunkSize = (reqSize > DEFAULT_CHUNK_SIZE) ? reqSize : DEFAULT_CHUNK_SIZE;
    next = nextChunk;
    bytesAlreadyAllocated = 0;
    mem = new char[chunkSize];
}
MemoryChunk::~MemoryChunk() {
    delete[]mem;
}

void* MemoryChunk::alloc(size_t requestSize){
    void *addr = (void*)((size_t)mem + bytesAlreadyAllocated);
    bytesAlreadyAllocated += requestSize;
    return addr;
}
inline void MemoryChunk::free(void *doomed){}//无需担心空闲内存段的释放,对象被删除,内存块也就被释放了

//MemoryChunk只是辅助类,ByteMemoryPool类用它首先可变大小的内存管理。
class ByteMemoryPool{
public:
    ByteMemoryPool(size_t initSize = MemoryChunk::DEFAULT_CHUNK_SIZE);
    ~ByteMemoryPool();
    inline void* alloc(size_t size);
    inline void free(void* someElement);
private:
    MemoryChunk *listOfMemChunks;
    void expandStorage(size_t reqSize);
};
//内存块列表可能包含多个块,但只有第一块可用于分配的内存,其他块表示已分配的内存。即列表的首个元素是唯一能够分配可用内存的块。
ByteMemoryPool::ByteMemoryPool(size_t initSize){
    expandStorage(initSize);
}
ByteMemoryPool::~ByteMemoryPool(){
    MemoryChunk *memChunk = listOfMemChunks;
    while (memChunk){
        listOfMemChunks = memChunk->nextMemChunk();
        delete memChunk;
        memChunk = listOfMemChunks;
    }
}

//alloc如果确保有足够的可用空间,则会把分配任务托付给列表头的MemoryChunk:
void* ByteMemoryPool::alloc(size_t requestSize){
    size_t space = listOfMemChunks->spaceAvailable();
    if (space < requestSize){
        expandStorage(requestSize); //表示如果当前块的大小不够的话就需要另外扩充新的request大小的MemoryChunk,并把它放在列表头部返回
    }
    return listOfMemChunks->alloc(requestSize);
}
inline void ByteMemoryPool::free(void *doomed){ listOfMemChunks->free(doomed); }

void ByteMemoryPool::expandStorage(size_t reqSize){
    listOfMemChunks = new MemoryChunk(listOfMemChunks, reqSize);
}
//这里需要修改Rational类的实现,用ByteMemoryPool代替MemoryPool<Rational>对象。
class Rational{
public:
    Rational(int a = 0, int b = 1) : n(a), d(b){}
    void *operator new(size_t size){ return memPool->alloc(size); }
    void operator delete(void *doomed){ memPool->free(doomed); }
    static void newMemPool(){ 
        memPool = new ByteMemoryPool(); 
    }
    static void deleteMemPool(){ delete memPool; }
    
private:
    static ByteMemoryPool *memPool;
    int n, d;
    
};

ByteMemoryPool* Rational::memPool = nullptr;
int main(){
    Rational *array[1000];
    
    Rational::newMemPool();
    for (int j = 0; j < 500; j++){
        for (int i = 0; i < 1000; i++){
            array[i] = new Rational(i);
        }
        for (int i = 0; i < 1000; i++){
            delete array[i];
        }
    }
    Rational::deleteMemPool();
}
版权声明:本文为weixin_34380781原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34380781/article/details/90999044

智能推荐

C# 第六章

1. 常量 2.枚举 【public】enum 枚举名 枚举名命名规则要和Pascal 枚举跟Int之间是可以相互转换的 所有类型都可以转换成string类型 注意:如果要转换的数字不在枚举里,会原样输出; 如果在枚举里找没有的数值,会抛出异常 3.结构体 结构体嵌套结构体的话,嵌套的内容的变量要用下划线开头 4. 数组 5.函数 在类中定义函数,函数中不能在定义函数,这是错误的; 如果main跟...

【JAVA书单】-《JAVA多线程核心技术》-第六章 单列模式与多线程

此文是对《JAVA多线程编程核心技术》的一点总结,如果想要了解具体细节可以去看原书。 第六章 单列模式与多线程 我之前最经常使用的单列: 使用静态内置类实现单列模式...

Java EE开发第六章:JDBC开发之DBCP连接池的使用

前言:连接池管理数据库的连接,作用就是提高项目的性能。就是在连接池初始化的时候存入一定数量的连接,用的时候通过方法获取,不用的时候归还连接即可。常用的连接池是DBCP和C3P0,这一章节我们先来认识一下DBCP连接池的用法。 --------使用步骤------ 1.导入jar包(commons-dbcp-1.4.jar和commons-pool-1.5.6.jar)(请自行百度下载) 2.使用a...

《提高C++性能的编程技术》阅读随笔 —— 第八章 内联基础

内联类似于宏,在调用方法放入内部展开被调用方法,以此方法来代替方法的调用(内联显著提升性能,但同时增加了编译时间) 示例:方法调用代价(通常方法非调用/返回机制都会对寄存器进行存储和恢复操作) 寄存器介绍: *指令指针(Instruction Pointer,IP),也成为程序计数器 :存放吓一跳将要执行的指令地址。方法调用时,跳转到被调用方法的指令并修改IP。 *链接寄存器(Link Regis...

第六章 内存管理:堆和栈

6.1.1 C++词法作用域 C++中有六种作用域: (1)块作用域 (2)函数作用域 (3)文件作用域 (4)整个程序作用域 (5)类作用域 (6)名字空间作用域 块作用域也是右开闭花括号定界的,块作用域和函数作用域的区别是函数有参数和名字。在程序执行时候调用函数时 ,就进入了函数作用域,块作用域是不能被调用的。在执行了位于前面的语句之后,块才能被执行。 在这里,类型Account的作用域是整个...

猜你喜欢

第六章 java反射、多线程

一、java反射 sinat_38259539的博客-Java基础之—反射(非常重要): https://blog.csdn.net/sinat_38259539/article/details/71799078 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法...

第六章 Java 多线程

多线程简介...

遍历 names.txt 文件,输出长度最长的回文人名

判断是否是回文有三种方法: 一、 先使指针指向字符串中第一个元素和最后一个元素,进行比较:   如果不相同,则不是回文,返回 False;   如果相同,两边指针各往中间移动一个单位,继续比较。   若两指针相遇,证明字符串对称的位置元素相同,也就是回文,返回True 代码如下: 二、回文字符串的递归判断:   假设只...

坏的代码习惯引起的bug

chart_left1是一个全局变量,而 并没有这句代码,所以当我在本地登录成功后,画的图还是还是最初第一个图(正确的图被隐藏了)...