0%

有用的c++内存相关库

c++对内存的原始控制是其优势,同时带来一系列灾难性问题,例如野指针问题、内存泄漏问题、内存碎片问题,在c++世界这是非常常见而棘手的问题,其实这些问题早有成熟应对方案,就以boost为例,早就包含相关库,有些已经成为c++标准库。c++解决内存方面的技术有:

(1)智能指针:解决野指针、内存泄漏问题;

(2)内存池:提升内存分配效率,解决内存碎片问题;

(3)flyweight:解决大量重复对象对内存的浪费。

下面通过一些实例介绍boost对上述技术的实现。

boost智能指针

boost智能指针已经成为c++11标准,是时候告别裸指针了。boost智能指针包括如下内容:

  • 智能指针模板类
    • scoped_ptr,scoped_array
    • shared_ptr,shared_array(deprecated)
    • weak_ptr
    • intrusive_ptr
    • local_shared_ptr
  • 实用函数和类
    • make_shared
    • make_unique
    • allocate_unique
    • enable_shared_from_this
    • pointer_to_other
    • static_pointer_cast
    • intrusive_ref_counter
    • atomic_shared_ptr

使用示例

scoped_ptr

scoped_ptr适用于作用域内的指针管理,所有权不能转移出去,而标准库中的unique_ptr可以转移,这是两者的主要区别。

先看不使用智能指针的例子,在每个退出或异常的地方需要不厌其烦的检查动态分配的对象是否释放,一旦漏掉一个地方就会导致内存或资源泄露。

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
32
33
34
35
36
37
38
void fun()
{
int *pInt = NULL;
try
{
pInt = new int(1);

bool bOk = false;

//do something

if (!bOk)
{
if (pInt != NULL)
{
delete pInt;
pInt = NULL;
}
return;
}

//do something
//maybe throw exception

delete pInt;
pInt = NULL;
}
catch(...)
{
if (pInt != NULL)
{
delete pInt;
pInt = NULL;
}
}

return;
}

使用智能指针改写后的例子,不但代码更加简洁,也完全杜绝了内存泄露的后顾之忧。

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
void fun()
{
int *pInt = NULL;
try
{
boost::scoped_ptr<int> pInt(new int(1));

bool bOk = false;

//do something

if (!bOk)
{
return;
}

//do something
//maybe throw exception

}
catch(...)
{
}

return;
}

上述例子充分体现了智能指针中对RAII(resource acquisition is initialization)思想的运用,由一个临时对象持有裸指针,在临时对象销毁时同时释放裸指针指向的资源,而编译器可以很好的保证临时对象在各种情况下正确的销毁。从scoped_ptr的名字可以看出,它应用于离开某个作用域后需要资源自动释放的场景,因此scoped_ptr不像std::auto_ptr那样具有难以使用的所有权转让语义,也不像shared_ptr那样具有共享所有权语义,它将资源的生命周期仅仅限定在某个作用域内,这样的设计使它意图非常明确和简单,实际上scoped_ptr是不可复制的,当然就不能应用在容器中,因为它不需要转让所有权或共享所有权,因此在编译期就可以避免不正确的使用(不能将一个scoped_ptr赋值或拷贝构造给另一个scoped_ptr,也不能将scoped_ptr存储在容器中)。

选择使用scoped_ptr还是std::auto_ptr?scoped_ptr和auto_ptr很像,只是auto_ptr多了所有权转让语义,例如可以作为函数返回值,除此之外,他们都是用栈上的对象管理堆上的对象,都不能共享所有权,因此都不能保存在容器中,但是scoped_ptr作了严格的控制(赋值和拷贝构造函数是私有的),确保了使用者不会误入歧途,而对于auto_ptr,只要你愿意,你还是可以将其放入容器中。所以,一般情况下最好使用scoped_ptr,如果你确实需要所有权转让语义,可以使用auto_ptr,但必须非常小心。

shared_ptr

不使用智能指针的例子:

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
32
33
34
35
36
37
38
std::vector<int *> vecIntPtr;

vecIntPtr.push_back(new int(1));
vecIntPtr.push_back(new int(2));
boost::mutex mtx;

//thread1
{
{
boost::mutex::scoped_lock lock(mtx);
for(std::vector<int *>::iterator it = vecIntPtr.begin(); it != vecIntPtr.end(); ++it)
{
delete *it;
}
vecIntPtr.clear();
}

}

//thread2
{
int *pInt = NULL;
{
boost::mutex::scoped_lock lock(mtx);
if (!vecIntPtr.empty())
{
pInt = *vecIntPtr.begin();
}

//maybe need long time

if (pInt)
{
*pInt = 3;
}

}
}

使用shared_ptr改写后:

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
32
33
34
35
36
typedef boost::shared_ptr<int> IntPtr;

std::vector<IntPtr> vecIntPtr;

vecIntPtr.push_back(IntPtr(new int(1)));
vecIntPtr.push_back(boost::make_shared<int>(2));
boost::mutex mtx;

//thread1
{
{
boost::mutex::scoped_lock lock(mtx);
vecIntPtr.clear();
}

}

//thread2
{
IntPtr pInt;
{
boost::mutex::scoped_lock lock(mtx);
if (!vecIntPtr.empty())
{
pInt = *vecIntPtr.begin();
}
}

//maybe need long time

if (pInt)
{
*pInt = 3;
}

}

上例展示了多线程情形下访问同一份指针的情况。看出两份代码之间的区别了吗?

  • 前面的代码需要显式的调用delete,而后面的代码不需要,其好处是显而易见的,在上述简单的例子中,也许你觉得不可能忘了delete 指针,但是在更复杂的系统中,忘了delete是屡见不鲜的,而查找此类问题需要花费高昂的代价;

  • 前面的代码需要在使用指针的整个过程加锁,即使从容器中获取了某个指针,以防其他线程将其删除,在使用指针的整个过程都需要加锁,如果这个过程非常耗时,那么可能带来系统效率的低下,而后面的代码仅仅是在从容器获取指针的过程需要加锁,这是由容器的性质决定的,STLport容器保证多线程读同一个容器是安全的,但不保证多线程写同一个容器是安全的,因此从容器获取指针的过程必须人为保证其安全性,但是一旦获取到了指针,后面对指针的使用过程就不需要加锁了,这是如何保证的呢, shared_ptr为每个裸指针维护一个引用计数,所有shared_ptr对象(临时对象或容器中的对象)共享裸指针和这个引用计数,创建一个shared_ptr对象时引用计数加1,对象销毁时引用计数减1,当引用计数为0时就说明已经没有人需要此裸指针了,此时正是裸指针生命结束的时候。因此只要你获取了shared_ptr对象,在使用过程中引用计数就肯定不可能为0,所以你可以放心的使用此指针。

需要注意的是:shared_ptr确保了指针本身使用的安全,指针内部数据的安全性仍需要使用者自己来保证,这不是智能指针关注的事情。

另外注意到boost::make_shared的使用了吗?使用make_shared一方面可以去除new的显式调用,更重要的是可以获得性能的提升(原因见后),建议尽量使用make_shared,如果编译器支持右值引用,make_shared可以完美的将参数传给构造函数而没有任何性能的损失。

关于shared_ptr的使用有几点是需要注意的:

  • 禁止some_operation (boost::shared_ptr(new T), return_int_operation())用法,因为参数求值顺序是不确定的,可能先执行new T,然后return_int_operation(),然后构造shared_ptr,如果return_int_operation()抛异常,那么就会出现内存泄漏;
  • 避免对shared_ptr所管理内存直接操作,以免重复释放;
  • 关于类型转换,只要 T* 能被隐式地转换到 U*,则 shared_ptr<T>就能被隐式地转换到shared_ptr<U>。特别是,shared_ptr<T>隐式转换到shared_ptr<T const>,当U是T的一个可访问基类的时候,还能转换到shared_ptr<U>,以及转换到shared_ptr<void>,另外可用如下函数类型转换:
1
2
3
4
5
6
7
8
template<class T, class U>
shared_ptr<T> static_pointer_cast(shared_ptr<U> const & r); // never throws

template<class T, class U>
shared_ptr<T> const_pointer_cast(shared_ptr<U> const & r); // never throws

template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const & r); // never throws
  • 所有共享同一裸指针的shared_ptr必须同源,例如不能出现如下代码:
1
2
3
int *p = new int(10);   
boost::shared_ptr<int> sp1(p);
boost::shared_ptr<int> sp2(p);

应该是:

1
2
3
int *p = new int(10);   
boost::shared_ptr<int> sp1(p);
boost::shared_ptr<int> sp2 = sp1;
  • 不要直接使用容器中的智能指针。

weak_ptr

看出下面的代码有什么问题了吗?初始后临时对象father和son的引用计数都是2,当father和son销毁后,引用计数变为1,之后再也没有可能变成0了,于是产生了内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CFather;
class CSon;
typedef boost::shared_ptr<CFather> FatherPtr;
typedef boost::shared_ptr<CSon> SonPtr;
class CFather
{
public:
SonPtr m_son;
};

class CSon
{
public:
FatherPtr m_father;
};

FatherPtr father(new CFather);
SonPtr son(new CSon);
father->m_son = son;
son->m_father = father;

为了解决循环引用带来的内存无法释放的问题,weak_ptr产生了,只要将循环引用中的一环改为weak_ptr,问题就迎刃而解了。

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
class CFather;
class CSon;
typedef boost::shared_ptr<CFather> FatherPtr;
typedef boost::shared_ptr<CSon> SonPtr;
typedef boost::weak_ptr<CSon> SonWeakPtr;
class CFather
{
public:
SonWeakPtr m_son;
};

class CSon
{
public:
FatherPtr m_father;
};

FatherPtr father(new CFather);
SonPtr son(new CSon);
father->m_son = son;
son->m_father = father;

//how to access weak_ptr
{
SonPtr son = father->m_son.lock();
if (son)
{
//do something
}
}

我们来看看它是如何工作的:

  • weak_ptr必须由shared_ptr或其他weak_ptr初始化,weak_ptr只是作为shared_ptr的观察者,不会导致shared_ptr的引用计数加1;

  • 在每次使用指针时,还是要获取shared_ptr,方法是调用lock成员函数,实际上,weak_ptr没有重载*和->,也没有提供get来获取裸指针,所以它是安全的。

那么重写后的代码,是不是解决了内存泄露的问题呢?初始后father的引用计数为2,son为1,son销毁后引用计数变为0,从而会释放son持有的CSon指针,同时CSon的成员m_father也会析构,father的引用计数变为1,father销毁后引用计数变为0,最终father持有的CFather指针得到释放。

intrusive_ptr

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
32
33
34
35
36
37
38
39
40
41
42
class CSharedObject
{
public:
CSharedObject() : m_ulCnt(0)
{

}

void AddRef()
{
InterlockedIncrement((long *)&m_ulCnt);
}

unsigned long ReleaseRef()
{
return InterlockedDecrement((long *)&m_ulCnt);
}

private:
unsigned long m_ulCnt;
};

class CMyObject : public CSharedObject
{

};


typedef boost::intrusive_ptr<CMyObject> MyObjectPtr;

void intrusive_ptr_release(CMyObject *p)
{
if (p->ReleaseRef() == 0)
{
delete p;
}
}

void intrusive_ptr_add_ref(CMyObject *p)
{
p->AddRef();
}

intrusive_ptr用于存储带有侵入式引用计数对象的指针,使用时定义boost::intrusive_ptr<T>的同时,还要定义void intrusive_ptr_add_ref(T*) 和void intrusive_ptr_release(T *),intrusive_ptr保证需要增加引用计数时调用intrusive_ptr_add_ref,需要减小引用计数时调用intrusive_ptr_release。

使用intrusive_ptr的主要原因有:

  • 一些已有的 frameworks 和操作系统提供带有侵入式引用计数的对象;

  • intrusive_ptr 的内存占用量和相应的裸指针一样;

  • intrusive_ptr<T> 能够从任意一个类型为 T * 的裸指针构造出来。

scoped_array

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// scoped_array
typedef boost::scoped_array<int> IntArrayPtr;

IntArrayPtr intArrayPtr(new int[10]);
for (int i = 0; i < 10; i++)
{
intArrayPtr[i] = i;
}

intArrayPtr[10] = 10; //no check

// scoped_ptr to vector
typedef boost::scoped_ptr<std::vector<int> > IntArrayPtr;

IntArrayPtr intArrayPtr(new std::vector<int>(10));

for (int i = 0; i < 10; i++)
{
(*intArrayPtr)[i] = i;
}

(*intArrayPtr)[10] = 10; //no check
intArrayPtr->at(10) = 10; //have check

// shared_array
typedef boost::shared_array<int> IntArrayPtr;

IntArrayPtr intArrayPtr(new int[10]);
for (int i = 0; i < 10; i++)
{
intArrayPtr[i] = i;
}

intArrayPtr[10] = 10; //no check

// shared_ptr to vector
typedef boost::shared_ptr<std::vector<int> > IntArrayPtr;

IntArrayPtr intArrayPtr(new std::vector<int>(10));

for (int i = 0; i < 10; i++)
{
(*intArrayPtr)[i] = i;
}

(*intArrayPtr)[10] = 10; //no check
intArrayPtr->at(10) = 10; //have check

scoped_array和shared_array是专门管理用new T[]分配的指针的,它们都重载了[]操作符,没有重载*和->操作符,使用scoped_array和shared_ptr确保了在删除时相应的使用delete []。实际上scoped_array和shared_array完全可以用vector来代替C数组,见上述示例代码。

原理

用一个自己实现的简单版本来说明一下shared_ptr的原理:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
template<typename T>
class MySmartPtr
{
public:
T * operator ->() const
{
return m_pData;
}

T & operator *()
{
return *m_pData;
}

MySmartPtr()
{
m_pData = NULL;
m_piCnt = NULL;
}

explicit MySmartPtr(T * pData)
{
m_pData = pData;
m_piCnt = new int(0);
InterlockedIncrement((long*)m_piCnt);
}

MySmartPtr & operator=(const MySmartPtr & other)
{
if (this != &other && m_pData != other.m_pData)
{
destroy();
m_pData = other.m_pData;
m_piCnt = other.m_piCnt;
InterlockedIncrement((long*)m_piCnt);
}
return *this;
}

MySmartPtr(const MySmartPtr & other)
{
m_pData = NULL;
m_piCnt = NULL;
*this = other;
}

~MySmartPtr()
{
destroy();
}

private:
void destroy()
{
if (m_piCnt && *m_piCnt > 0)
{
if (InterlockedDecrement((long*)m_piCnt) == 0)
{
delete m_pData;
m_pData = NULL;
delete m_piCnt;
m_piCnt = NULL;
}
}
}
T *m_pData;
int *m_piCnt;
};

智能指针所运用的基本原理包括:

(1) 重载*和->操作符,是其用法和裸指针的使用方法一致;

(2) 智能指针对象的析构函数中判断裸指针是否删除,RAII思想的运用;

(3) shared_ptr使用一个共享的引用计数决定何时删除裸指针(见示例)。

性能

shared_ptr的性能是使用者比较关心的问题。而这个问题也早已经过广泛的讨论和试验,问题的核心集中在引用计数的实现方式,boost文档中记述的一些开发人员所做的试验,试验中测试了5种实现方式:

  1. Counted pointer using a heap allocated reference count, this is referred to as simple counted.
  2. Counted pointer using a special purpose allocator for the reference count - special counted.
  3. Counted pointer using an intrusive reference count - intrusive.
  4. Linked pointer as described above - linked.
  5. Cyclic pointer, a counted implementation using a std::deque for allocation with provision for weak pointers and garbage collection of cycles of pointers - cyclic.

从两个方面进行了试验:

Two tests were run: the first aimed to obtain timings for two basic individual operations:

  1. Initial construction from raw pointer.
  2. An amortized copy operation consisting of half an assignment and half a copy construction - designed to reflect average usage.

The second attempted to gain more insight into normal usage by timing the fill and sort algorithms for vectors and lists filled with the various smart pointers.

试验环境:

on two compilers:

  1. MSVC 6.0 service pack 3, using default release optimization mode (/O2 - optimized for speed, no inlining of functions defined outside a class body unless specified as inline).
  2. gcc 2.95.2 using full optimization (-O3 -DNDEBUG).

Additionally, generated pointer sizes (taking into account struct alignment) were compared, as were generated code sizes for MSVC mainly by manual inspection of generated assembly code - a necessity due to function inlining.

All tests were run on a PII-200 running Windows NT version 4.0

第一个试验结果:

MSVC speed graph

GCC speed graph

单位:纳秒

MSVC

initialization copy operation
simple counted 3000 +/- 170 104 +/- 31
special counted 1330 +/- 50 85 +/- 9
intrusive 1000 +/- 20 71 +/- 3
linked 970 +/- 60 136 +/- 10
cyclic 1290 +/- 70 112 +/- 12
dumb 1020 +/- 20 10 +/- 4
raw 1038 +/- 30 10 +/- 5

GCC

initialization copy operation
simple counted 4620 +/- 150 301 +/- 28
special counted 1990 +/- 40 264 +/- 7
intrusive 1590 +/- 70 181 +/- 12
linked 1470 +/- 140 345 +/- 26
cyclic 2180 +/- 100 330 +/- 18
dumb 1590 +/- 70 74 +/- 12
raw 1430 +/- 60 27 +/- 11

第二个试验结果:

单位:秒

GCC

vector list
fill sort fill sort
simple counted 46.54 2.44 47.09 3.22
special counted 14.02 2.83 7.28 3.21
intrusive 12.15 1.91 7.99 3.08
linked 12.46 2.32 8.14 3.27
cyclic 22.60 3.19 1.63 3.18
raw 11.81 0.24 27.51 0.77

MSVC

vector list
fill sort fill sort
simple counted 1.83 2.37 1.86 4.85
special counted 1.04 2.35 1.38 4.58
intrusive 1.04 1.84 1.16 4.29
linked 1.08 2.00 1.21 4.33
cyclic 1.38 2.84 1.47 4.73
raw 0.67 0.28 1.24 1.81

结论:

The timing results mainly speak for themselves: clearly an intrusive pointer outperforms all others and a simple heap based counted pointer has poor performance relative to other implementations. The selection of an optimal non-intrusive smart pointer implementation is more application dependent, however. Where small numbers of copies are expected, it is likely that the linked implementation will be favoured. Conversely, for larger numbers of copies a counted pointer with some type of special purpose allocator looks like a win. Other factors to bear in mind are: -

Deterministic individual, as opposed to amortized, operation time. This weighs against any implementation depending on an allocator.

Multithreaded synchronization. This weighs against an implementation which spreads its information as in the case of linked pointer.

根据以上试验可以看出相比于裸指针,shared_ptr性能地损失主要来自两方面:一方面是第一次初始化时需要额外分配引用计数,另一方面是赋值或拷贝时引用计数的更新等,而前者的损失是主要的。试验结果表明带有侵入式引用计数的实现胜过其他实现,但是大多数应用更依赖于非侵入式引用计数的实现。boost::shared_ptr就是非侵入式实现,其默认实现属于simple counted,通过传入自定义的分配器,也可以实现special counted方式引用计数,除非对时间特别关键的应用,默认的实现完全可以满足要求。另外在初始化时使用工厂方法boost::make_shared(或boost::allocate_shared)可以获得和侵入式接近的性能,因为boost::make_shared使用了一个placement new来分配T,这样相当于节省了分配引用计数的时间。关于是否可以将智能指针作为函数参数传递,上述试验中的simple counted实现正是用了一个默认的boost::shared_ptr,其拷贝的时间确实比裸指针多很多(在PII-200机器上是104纳秒,裸指针是10纳秒),但是毕竟也是纳秒级别,实际使用时的性能损失应该基本觉察不到。boost::shared_ptr的默认实现是采用lock-free的整数原子操作进行的引用计数增减,试验并未评估在复杂的多线程或异步环境中对系统造成的性能损失。

boost内存池

内存池用于管理大量小对象,避免频繁在堆上分配。boost内存池包含如下内容:

  • pool
  • object_pool
  • singleton_pool
  • pool_allocator

使用示例

pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boost::pool<> FixSizeMemPool(sizeof(int), 32);

int *pInt = (int *)FixSizeMemPool.malloc();

FixSizeMemPool.free(pInt);

pInt = (int *)FixSizeMemPool.malloc();

FixSizeMemPool.ordered_free(pInt);

int *pIntArray = (int *)FixSizeMemPool.ordered_malloc(10);

FixSizeMemPool.ordered_free(pIntArray, 10);

pIntArray = (int *)FixSizeMemPool.ordered_malloc(10);

FixSizeMemPool.free(pIntArray, 10);

boost::pool是用来快速分配固定大小内存快的内存池,构造函数的第一个参数是希望从内存池中每次获取的区块(chunk)的大小,第二个参数表示每次空间不够后再次分配的区块个数(next_chunk_num),为默认参数,默认值为 32。第一次调用malloc或ordered_malloc时进行第一次分配,分配能容纳next_chunk_num个区块的内存快(block),然后将next_chunk_num乘以2,也就是说下一次分配的内存快能容纳的区块个数翻倍,依次类推。malloc是从内存池中分配一个区块,如果失败返回0;ordered_malloc是从内存池中分配连续的区块,如果失败也是返回0;分配的区块不再使用后通过free返还给内存池;特别的ordered_free保证对返还后的空闲区块排序,以保证之后使用ordered_malloc分配连续区块的机会更大,所以ordered_free的时间复杂度不是O(1),如果你经常会使用ordered_malloc,最好在释放时使用ordered_free。FixSizeMemPool销毁时保证所有分配的内存得到释放,即使没有调用free或ordered_free,也因此boost::pool不是线程安全的,它不是设计用来多个模块共享的。

object_pool

1
2
3
4
5
6
7
8
9
10
11
12
13
struct X
{
public:
X(int i)
{

}
};
boost::object_pool<X> ObjectPool(32);

X *pX = ObjectPool.construct(1);

ObjectPool.destroy(pX);

boost::object_pool是从boost::pool继承而来,构造函数只有一个参数即next_chunk_num,和boost::pool的主要区别是boost::object_pool在获取到区块后,使用了一次placement new对区块进行了构造,之后返回了T *而不是void *,这是通过调用construct进行的,construct默认支持3个参数,如果想传入更多参数,需要修改boost/pool/detail/pool_construct_simple.inc文件。相应的调用destroy,会先进行析购,然后回收内存。当然也可以调用malloc/free(没有ordered_malloc/ordered_free),但不推荐,因为ObjectPool在销毁时会自动调用没有free的区块的析购函数,如果在malloc后,用户自己没有对区块进行构造,而又没有调用free,那么最后在其上调用一次析购函数可能产生错误。

singleton_pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MyPoolTag{};
typedef boost::singleton_pool<MyPoolTag, sizeof(int)> MyPool;

int *pInt = (int *)MyPool::malloc();

MyPool::free(pInt);

int *pIntArray = (int *)MyPool::ordered_malloc(10);

MyPool::ordered_free(pIntArray, 10);
struct OtherPoolTag{};
typedef boost::singleton_pool<OtherPoolTag, 10,
boost::default_user_allocator_new_delete,
boost::mutex,
32> OtherPool;

boost::singleton_pool是被设计用来在多个模块间共享的,所以是线程安全的。使用singleton模式实现,并且其singleton静态对象也是线程安全的。使用singleton_pool时不用定义对象,malloc/ordered_malloc/free/ordered_free都是静态的,调用方法和boost::pool相同。OtherPool的定义展示了boost::singleton_pool的全貌,10表示请求区块大小;boost::default_user_allocator_new_delete为分配器,默认值也是它,用户可定义其他的分配器;boost::mutex是为了保证线程安全使用的锁,默认为details::pool::default_mutex;32为next_chunk_num。

pool_allocator

1
2
3
4
5
6
7
8
9
10
std::vector<int, boost::pool_allocator<int> > vecInt;

for (int i = 0; i < 10000; i++)
{
vecInt.push_back(i);
}

boost::singleton_pool<boost::pool_allocator_tag, sizeof(int)>::release_memory();

std::vector<int, boost::pool_allocator<int, boost::default_user_allocator_new_delete, boost::mutex, 32> > otherVecInt;

boost::pool_allocator提供了符合标准的分配器,可用于STL容器。boost::pool_allocator内部实际是使用singleton_pool进行的内存分配,所以如果想手工释放内存可以像上例中使用boost::singleton_pool<boost::pool_alocator_tag, sizeof(int)>::release_memory()。

boost智能指针和内存池的结合使用

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
template <typename T>
class SmartPool : public boost::noncopyable
{
public:

typedef boost::shared_ptr<T> SmartObjectPtr;

SmartObjectPtr ConstructSmartObject()
{
boost::mutex::scoped_lock lock(m_mtx);
return SmartObjectPtr(m_objpool.construct(),
boost::bind(&SmartPool::DestroySmartObject, this, _1),
boost::pool_allocator<boost::detail::sp_counted_base>());
}

private:
void DestroySmartObject(T *p)
{
m_objpool.destroy(p);
}

boost::object_pool<T> m_objpool;
boost::mutex m_mtx;
};

struct X{};
SmartPool<X> XSmartPool;
SmartPool<X>::SmartObjectPtr objPtr = XSmartPool.ConstructSmartObject();

上例展示了将boost智能指针和内存池结合使用的方法。上述方法的限制是要求X有默认构造函数,另外必须XSmartPool在所有智能指针之后释放,当然这两个限制是有方法解决的,大家可以自己想一想。

原理

对于固定大小内存池的实现原理并不复杂,即预先分配一些内存块,每个内存快被划分成相同大小的区块,这些区块被链接在一个空闲链条中,当要分配内存时,从空闲区块头取出一个,如果没有空闲的,则分配新的内存快,当不再使用区块时,将其加入到空闲链表中即可,所以保证了分配和释放的时间复杂度是常量的(除第一次分配或之后追加分配时),肯定比直接使用new分配要快。boost内存池的增长方式是,第一次调用malloc*时分配一个内存快,可容纳若干(可配)个区块,以后每次不够时,再分配一个内存快,大小是前一次的两倍。boost内存池还提供了分配连续区块的接口,为了保证每次分配连续区块成功的机会更大,在释放时提供了排序的释放方法,当然其时间复杂度就不是常量了。boost内存池还保证了内存对齐,可广泛适用于不同平台。

内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	
struct Pool
{
void *firstBlock;
void *firstFreeChunk;
};

struct Block
{
union Chunk
{
void *nextChunk;
char chunk[chunk_size];
} Chunks[chunk_num];
void *nextBlock;
size_type nextBlockSize;
};

每个内存快(Block)的布局如上所示(上述代码仅是为了说明问题的伪代码,真实源代码并不是这样),首先是若干个连续的区块,然后是下一个内存快的指针,最后是下一个内存快的大小。这里比较有意思的一个技巧是复用了区块中的内存存储了下一个区块的指针,将区块链接起来。

内存对齐

boost内存对齐使用了最小公倍数方法(具体推导过程见boost文档),保证了在各种复杂环境下内存的对齐。这一点也是我们自己实现内存池容易忽视的一个问题。

boost::flyweight

在《设计模式》一书中描述了flyweight模式,boost::flyweight得名于此。

使用示例

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
struct user_entry
{
std::string first_name;
std::string last_name;
int age;
};


struct user_entry
{
boost::flyweight<std::string> first_name;
boost::flyweight<std::string> last_name;
int age;
};

user_entry ue1;
ue1.first_name = "zhang";
ue1.last_name = "xxx";

user_entry ue2;
ue2.first_name = "zhang";
ue2.last_name = "yyy";

user_entry ue3;
ue3.first_name = "zhang";
ue3.last_name = "yyy";

boost::flyweight的使用非常简单,大多数时候只需要修改一下结构体的定义,就能带来内存的节省,当冗余度越大时,内存节省越明显,例如上例,使用boost::flyweight后,内存中只有一份“zhang”,当有成千上万个这样的对象时,姓相同的比例非常大,这样就能节省很多内存。

-------------本文结束感谢您的阅读-------------