优化内存分配

引入优化的必要性

C++中内存分配是一种类型化的操作:new为特定的类型分配内存,并在新分配的内存中构造该类型对象。 new表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象。 ## Reason1 ## new基于每个对象分配内存的事实可能会对某些类强加不可接收的运行开销,这样的类可能需要使 用户级别的类类型对象分配 的能够更快一些 ,所以,这些类使用的通用策略是:预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个元素

Reason2

另外一些类希望按最小尺寸为自己的数据成员分配需要的内存。

例如:标准库中的vector类预先分配额外内存一保存加入的附加元素,将新元素加入到这个保留容量中。将元素保持在连续内存的时候,预先分配的元素使vector能够高效地加入加入元素

在预先分配内存以保存用户级对象或保存类的内部数据时,都需要将内存分配与对象构造分离开来。这样做的明显理由是: 在预先分配的内存中构造对象很浪费,可能会创建从不使用的对象。当实际使用预先分配的对象时,被使用的对象必须重新赋值。更微妙的是,如果预先分配的内存必须被构造,某些类就不能使用它。

例如:考虑vector,它使用了预先分配策略。如果必须构造预先分配的内存中的对象,就不能为没有默认构造函数的基类型 构造一个vector—-vector没办法知道怎样构造这些对象

进入正题

C++ 中的内存分配

首先得明确接管内存分配时,必须处理这两个任务:

  • 分配原始内存时 , 必须在该内存中构造对象
  • 在释放该内存之前 , 必须保证适当地撤销这些对象

注意:对为构造的内存中的对象进行赋值而不是初始化,其行为是未定义的。对许多类而言,这样做会引起运行时的崩溃因为赋值涉及到删除现存对象,赋值操作符中的动作就会引起灾难性后果!!!

–好了, 开始引入猪脚——

C++提供下面两种方法分配和释放为构造的原始内存:

  • 1> allocator类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存 保存对象
  • 2> 标准库中的 operator newoperator delete ,他们分配和释放需要大小的原始的、为类型化的内存(注意,这里必须与 newdelete 区分开来,完全不用哦)

C++还提供不同的方法在原始内存中构造和撤销对象:

  • 1> allocator 类定义了名为constructdestroy 的成员,其操作正如他们的名字所指的那样:construct用来在尾构造内存 中初始化对象(调用复制构造函数 , 而不是构造函数哦) , destroy则在对象上运行适当的析构函数
  • 2> 定位new表达式 接收指向未构造内存的指针,并在该空间初始化一个对象或数组
  • 3> 可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象被所在的内存!!
  • 4> 算法uninitialzed_copyuninitialzed_fill 像fill 和 copy算法一样执行,当然有一点不同的是:他们可以在目的地构造对象而不是给对象赋值

// 现代C++程序一般使用allocator类来分配内存。他更安全、灵活,但是在构造对象的时候,用new表达式比allocator::construct
//成员更灵活。有几种情况必须使用new表达式

allocator

1
2
3
4
5
6
7
8
9
10
11
12
==========================================================================================================================================
allocator<T> a; 定义名为a的allocator对象,可以分配内存或构造T类型的对象
a.allocate(n) 分配原始的未构造内存以保存T类型的n个对象
a.deallocate(p , n) 在名为p 的T* 指针中包含的地址处保存T类型的n个对象释放内存,
运行调用deallocate之前在该内存中构造的任意对象的destroy是用户的责任
a.construct(p , t) 在T* 指针p所指内存中构造一个新元素。运行T类型的复制构造函数用t初始化该对象
a.destroy(p) 运行T* 指针p所指对象的析构函数
uninitialzed_copy(b , e , b2) 从迭代器b和e指出的输入范围将元素复制到从迭代器b2开始的未构造的原始内存中。
该函数在目的地构造元素,而不是给他们赋值。假定由b2指出的目的地足以保存输入范围中元素的副本
uninitialzed_fill(b , e , t) 将由迭代器b和e指出的范围中的对象初始化为t的副本。假定该范围是未构造的原始内存。使用复制构造函数构造对象
uninitialzed_fill_n(b , e , t , n) 将由迭代器b和e指出的范围中至多n个对象初始化为t的副本
==========================================================================================================================================

使用allocator管理类成员数据

首先看看vector类中的内存分配:

vector将元素保存在连续的存储空间中。为了获得可接受的性能,vector预先分配比所需元素更多的元素。每个元素加到容器 中的vector成员检查是否有可用空间以容纳另一元素。如果有,该成员在预先分配内存中下一个可用位置初始化一个对象;如果没有自由元素,就重新分配vector:vector获取新的空间,将现存的元素复制到新空间中,增加新元素,并释放旧空间

vector所用存储开始是未构造内存,它还没有保存任何对象。将元素复制或增加到这个预分配空间的时候,必须使用allocator类的construct成员构造函数

我们通过定义一个类vectorVector来理解这些概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T>
class Vector
{
public:
Vector():elements(0) , first_free(0) , end(0){}
void push_back(const &T);
T& operator[](const size_t index)
{
return *(elements + index);
}
private:
static std::allocator<T> alloc; //为什么是静态呢?当然是因为这个成员需要在对象还没有构造之前就要使用啦,而且对象析构之后还得继续使用这个成员
void reallocate();//自定义的重新分配空间函数
T* elements; //指向分配内存数组的首地址
T* first_free; //指向数组本身之后的那个元素
T* end; //指向数组本身之后的那个元素
};

如图:

array:  0 |1 |2 |3 |4 | 未构造的元素|
       ^elements       ^ first_free  ^end

Vector的size(实际使用的元素的数目)等于first_free - elements

Vectorcapacity(在必须重新分配Vector之前,可以定义的元素的总数)等于 end - elements 自由空间(在需要重新分配之前,可以增加的元素数目)是 end - first_free

定义push_back()成员,将新元素加到Vector末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <class T>
void Vector<T>::push_back(const T & t)
{
if(first_free != end)
reallocare();
alloc.construct(first_free , t);
++first_free;
}
template <class T>
void Vector<T>::reallocate()
{
std::ptrdiff_t size = first_free - elements;
std::ptrdiff_t newcapacity = 2 * max(size , 1);//如果Vector为空时,分配两个空间 ,否则将原先分配的内存翻倍分配
T* newelements = alloc.allocate(newcapacity);//分配好空间了,接下来干什么呢?当然是复制元素啦
uninitialzed_copy(elements , first_free , newelements);
//当然原先的内存必须撤啦,不可以占着茅坑不拉屎!
for(T* p = firt_free ; p != elements ; --p)
destroy(p);//调用析构函数析构对象,记住,这还不够彻底,必须还要释放内存啊
if(elements)//当然必须先检查该地址是否有效啦,这是国际惯例
{
alloc.deallocate(elements , end - elements);
}
//好了,收尾啦
elements = newelements;
first_free = elements + size;
end = elements + newcapacity;
}

注意:deallocate期待指向由allocate分配空间的指针,传给deallocate一个零指针是不合法的

当然,以次为例,读者可以自行定义 vector中 的reserveresize 以及const和非const下标操作符

operator new 函数和 operator delete 函数

1
string *s = new string("hello!");

new的工作原理:

  1. 调用名为 operator new的标准库函数,分配足够大的原始的位类型化的内存,以保存指定的一个对象
  2. 接下来,运行该类型的一个构造函数,用于指定初始化构造对象
  3. 返回指向新分配并构造的对象的指针
1
delete s;

deletw的工作原理

  1. 对该指针指向的对象运行适当的析构函数
  2. 通过调用operator delete 的标准库函数释放该对象的内存

operator newoperator delete接口

operator newoperator delete 函数有两个重载版本:

1
2
3
4
5
void *operator new (size_t); //allocate an object
void *operator new[](size_t);//allicate an array
void *operator delete(void *);//free an object
void *operator delete[](void *);//free an array

虽然operator newoperator delete 函数设计意图是供new表达式使用,可以使用它们获得为构造内存,类似allocator类 中的allocatedeallocate 成员。

转化例子:

1
2
3
4
5
T* newelements = alloc.allocate(newcapacity);
=> T* newelements = static_cast<T*>(operator new[](newcapacity * sizeof(T)));
alloc.deallocate(elements , end - elen=ments);
=> operator delete[](elements);

这些函数与allocator不同的是:它们在void* 指针而不是类型化指针上进行操作
//一般而言,使用allocator比直接使用operator newdelete函数更为类型安全

定位new 表达式

类似于construct成员,有第三种new表达式,称为定位new表达式。

定位new表达式在已分配的原始内存初始化一个对象,不分配内存。相反,他接收指向已分配但为构造内存的指针,并在内存中初始化对象
表达形式:

1
2
new (place_address) type
new (place_address) type (initialzer_list)

其中place_adress必须是一个指针,而initialzer_list 提供初始化列表,以便在构造新分配的对象时使用

1
2
3
4
alloc.construct(first_free , t);
=> new (first_free) T (t);
//定位new表达式比construct更加灵活。定位new表达式初始化一个对象的时候,它可以使用任何构造函数,
//并直接建立对象,construct函数总是使用复制构造函数

例如,从一对迭代器初始化一个已分配内存但未初始化的string对象:

1
2
3
4
allocator<string> alloc;
string *sp = alloc.allocate(2);
new (sp) string(b , e); //construct directly in place
alloc.construct(sp+1 , string(b , e)); //build and copy temporary

所以: 对于没有复制构造函数的类只能使用定位new表达式!!!

显示析构函数的调用

allocator类中destroy成员的低级选择是 显示调用析构函数

1
2
3
4
5
for(T *p = first_free ; p != elements ; --p)
destroy(p);
=>
for(T *p = first_free ; p != elements ; --p)
p-> ~T();

切记:析构函数只是清楚对象本身,并不是包括释放内存,而operator delete 函数不会运行析构函数,只负责释放内存

理解:就像你吃糖,你把糖给‘析构’了,你糖纸还留着啊,要想把‘糖纸’给销毁,就得运行 operator delete函数!!!

类特定的newdelete

为了优化内存分配,我们必须从 newdelete 这两个函数入手

默认情况下,new表达式通过调用由标准库定义的 operator new版本分配内存。 所以, 通过定义自己的名为operator newoperator delete 的成员,类可以管理用于自身类型的内存

原理:编译器看到new或者delete表达式的时候,它查看该类是否有operator newoperator delete成员,如果类定义(或继承)了自己的成员newdelete函数,则使用那些函数为对象分配和释放内存,否则,调用标准库版本
//优化newdelete的行为的时候,只需要定义operator newoperator delete 的新版本,newdelete表达式自己
//照管对象的构造和撤销

成员newdelete函数

operator newoperator delete 的形参以及返回值原理分析

  • <1>类成员operator new函数必须具有返回类型void*并接受size_t类型的形参。由new表达式用 以字节计算的分配内存量 初始化函数的size_t形参 (也就是说,在该成员函数内部就不用管size_t这个形参啦)
  • <2>类成员operator delete 函数必须具有返回类型void。它可以定义为接受单个void*类型形参,也可以定义接受两个形参,即void*size_t类型。 由delete表达式用 被delete的指针 初始化void*形参,该指针可以使空指针。如果提供 size_t形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t形参(当然,那是编译器的是,咱不管)

这些函数隐式地为静态函数,因为他们要么在构造对象之前使用,要么在撤销对象之后使用,因此,这些函数没有成员可操纵newdelete只能访问所属类的静态成员

operator new[]operator delete[]

还有数组操作符 new[]delete[]

  • <1>类成员operator new[] 必须具有返回类型void*,并接受的第一个形参类型为size_t,用 表示存储特定类型给定数目元素的 数组的字节数值 自动初始化操作符的size_t形参
  • <2>类成员operator delete[] 必须具有返回类型void,并且第一个形参为void* 类型。用 表示数组存储的其实位置的值自动初始化操作符的void*形参。 当然,类的操作符delete[]也可以提供 两个形参,第二个形参为 size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参

一个内存分配器的基类

改进内置库的newdelete函数

自由列表:预先分配一块原始内存来保存未构造的对象,创建新元素的时候,可以再一个预先分配的对象中构造;释放元素的时候, 将它们放回预先分配的块中,而不是将内存实际返还给系统。

在这里我们定义一个CachedObj的新类来管理自由列表。

CachedObj类有简单的接口:它的工作只是分配和管理已分配但未构造对象的自由列表

成员operator new :返回自由列表的下一个元素,并将该元素从自由列表中删除,当自由列表为空的时候,operator new将分配新的原始内存成员operator delete:在撤销对象时将所占用的内存返回自由列表

因为这里的CachedObj打算当基类使用,所以将给它一个public虚析构函数
//因为CachedObj类没有办法根据对象的实际类型分配大小不同的对象,它的自由列表保存着单一大小的对象。因此,它只能用于不作
//基类使用的类

CachedObj

我们希望为任意类型使用自由列表方法,所以讲该类定义为一个模板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class T>
class CachedObj
{
public:
void * operator new(std::size_t);
void operator delete(void* , std::size_t);
virtual ~CachedObj(){};
protected:
T * next;
private:
static void add_to_freelist(T*); //将对象放在自由列表
static std::allocator<T> alloc_mem;
static T *freestore; //该指针指向自由列表的表头
static const std::size_t chunk;//该成员指定每当自由列表为空时将要分配的对象的数目
};

最复杂的部分是理解这个模板类的形参,我要告诉你这个模板类中的形参T(template <class T>)指的一定是派生类,你能信?
而且绝对不可能是你想的内置类型,哈哈,不信吧?往下看解释:

  • 首先,CachedObj类的设计最初理念就是把它作为基类使用,
  • 其次,当继承CachedPbj类的时候,用来实例化该类的模板类型将是派生类本身。。
  • 最后,为了重用CachedObj类的自由列表管理而继承该类,但是,CachedObj类保存了指向它管理的对象类型的一个指针,该指针的类型 是指向CachedObj的派生类指针

例如:

为了优化 Screen 类的内存管理(自己随便定义个Screen类啊)

1
2
class Screen: public CachedObj<Screen>
{};

对于模板类从CachedObj类派生(自定义一个 QueueItem<type>类)

1
2
template <class Type>
class QueueItem: public CachedObj< QueueItem<TYpe> >{};

现在CachedObj类不需要做任何改变,就使的派生类具有自动内存分配,并且使用自由列表 ### 分配怎样工作: ###

1
QueueItem<Type> *pt = new QueueItem<Type>(val);

此表达式分配并构造一个新的QueueItem对象,具体工作:

  1. 使用QueueItem::operator new 函数从自由列表中分配一个对象
  2. 为类型T使用元素类型的复制构造函数,在该内存中构造一个对象 //为什么是复制构造函数啊!!!

类似地,

1
delete pt;
  1. 删除一个QueueItem指针时,运行该类的析构函数消除pt指向的对象
  2. 调用operator delete ,将元素所用的内存放回自由列表

定义operator new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class T>
void CachedObj<T>::operator new (size_t sz)
{
if(sz != sizeof(T)) //检查是否分配数量的空间(强调了我们的设计意图:CachedObj类应该只 被不是基类的类 使用 ,也就是说因继承而相关的类几乎总是定义大小不同的对象)
throw std::runtime_error("CachedOj: wrong size object in operator new");
if(!freestore)//检查列表是否为空
{
T * array = allic_mem.allocate(chunk);
for(size_t i = 0 ; i != chunk ; ++i)
add_to_freelist(&array[i]);
}
T *p = freestore;
freestore = freestore->CachedObj<T>::next;
return p;
}

定义operator delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T>
void CachedObj<T>::operator delete(void *p , size_t)
{
if(p != 0)
add_to_freelist(static_cast<T*> p); //为什么要强制转化?看看形参,p指针先前是void* ,但要往自由列表中放,是不是应该转化一下呢?
}
--add_to_freelist成员
这个成员的任务是设置next指针,并且在将对象加到自由列表时更新freestore指针
template <class T>
void CachedObj<T>::add_to_freelist(T * p)
{
p->CachedObj<T>::next = freestore;
freestore = p; //每次将元素插入自由列表的表头
}
/*唯一复杂的部分是next成员的使用。回忆一下,我们打算将CachedObj作为基类使用,被分配对象不是CachedObj类型的,相反
那些对象是CachedObj的派生类型的,因此,类型T将是派生类型,指针P是指向T的指针 , 不是指向CachedObj的指针。如果派生类
型有自己的名为next成员,则编写p->next将获得派生类的成员!但我们希望在基类--CachedOgj类中设置next (总而言之,保险起见嘛!!)*/

定义静态成员

1
2
3
template <class T> allocator<T> CachedObj<T>::alloc_mem;
template <class T> T* CachedObj<T>::freestore = 0;
template <class T> const size_t CachedObj<T>::chunk = 24;

热评文章