Memory Pool
http://reborn2266.blogspot.tw/2011/10/memory-pool.html http://www.codinglabs.net/tutorial_memory_pool.aspx
Why you should use
- Avoid the memory fragment
- Speed up the program
- Saving the time to find valid memory spaces
When should you use
Frequently using new
and delete
.
Basic Idea
DebugOnly.h
// file: DebugOnly.h
#ifndef DebugOnly_h_
#define DebugOnly_h_
#include <iostream>
#include <sstream> // std::stringstream
#include <string> // std::string
#define DEBUG true
void Log(std::string msg)
{
DEBUG && std::cout << msg << std::endl;
}
template<class T>
std::string ToAddressString(const T& t)
{
const void* address = static_cast<const void*>(&t);
std::stringstream ss;
ss << address;
return ss.str();
}
#endif // #ifndef DebugOnly_h_
Data.h
// file: Data.h
#ifndef Data_h_
#define Data_h_
#include "DebugOnly.h"
// Data Structure for Demo
// ===================================
struct Data
{
int mValue;
std::string mString;
Data(int val, std::string str)
: mValue(val)
, mString(str)
{
Log("<Data> Constructor");
}
~Data()
{
Log("<Data> Deconstructor");
}
};
// Helper functions
// ===================================
void PrintData(Data* data)
{
Log("[" + ToAddressString(*data) + "] " + // address
std::to_string(data->mValue) + " " + // value
data->mString); // string
}
#endif // #ifndef Data_h_
Basic.h
// file: Basic.h
#ifndef Basic_h_
#define Basic_h_
#include "DebugOnly.h"
#include <queue> // std::queue
template<typename T>
class BasicPool
{
public:
BasicPool()
{
Log("<BasicPool> BasicPool");
};
// Disallow the copy constructor
BasicPool(const BasicPool &)=delete;
// The move constructor
BasicPool(BasicPool&& other)
: mChunks(std::move(other.mChunks))
{
Log("<BasicPool> BasicPool << Move");
}
~BasicPool()
{
Log("<BasicPool> ~BasicPool");
Clear();
}
// Allocate a chunk of memory for the class object
void* Allocate()
{
Log("<BasicPool> Allocate");
// If there is no more available memory chunk,
// then we create a new one
if (mChunks.empty()) {
return ::operator new(sizeof(T));
}
// Otherwise, return a free memory chunk
void* chunk = static_cast<void*>(mChunks.front());
mChunks.pop();
return chunk;
}
// Reclaim the memory space from those no longer used objects
void Deallocate(void* obsolete)
{
Log("<BasicPool> Deallocate");
mChunks.push(static_cast<T*>(obsolete));
}
// Release all of the free memory chunks
void Clear()
{
Log("<BasicPool> Clear");
while (!mChunks.empty()) {
::operator delete(mChunks.front());
mChunks.pop();
}
}
private:
// Holding the pointers to the free memory chunks
std::queue<T*> mChunks;
};
#endif // #ifndef Basic_h_
main.cpp
// file: main.cpp
#include "Basic.h"
#include "Data.h"
#include "DebugOnly.h"
// Main Program
// ===================================
int main() {
// Declare a pool object
BasicPool<Data> pool;
// Create an instance of Data from the pool
void* freeSpace = pool.Allocate();
Data* data = new (freeSpace) Data(57, "Hello");
// Print it
PrintData(data);
// Destroy it
data->~Data();
pool.Deallocate(data);
// Print it again
PrintData(data);
// Create another instance of Data from the pool
void* freeSpace2 = pool.Allocate();
Data* data2 = new (freeSpace2) Data(75, "World");
// Print it
PrintData(data2);
// Destroy it
data2->~Data();
pool.Deallocate(data2);
// Print it again
PrintData(data2);
return 0;
}
The result is:
<BasicPool> BasicPool
<BasicPool> Allocate
<Data> Constructor
[0x7fdbd2500000] 57 Hello
<Data> Deconstructor
<BasicPool> Deallocate
[0x7fdbd2500000] 57 Hello
<BasicPool> Allocate
<Data> Constructor
[0x7fdbd2500000] 75 World
<Data> Deconstructor
<BasicPool> Deallocate
[0x7fdbd2500000] 75 World
<BasicPool> ~BasicPool
<BasicPool> Clear
The two objects have same address 0x7fdbd2500000
.
More General
// Create an instance of Data from the pool
void* freeSpace = pool.Allocate();
Data* data = new (freeSpace) Data(57, "Hello");
We need to ask a memory space and then construct our data there.
// Destroy it
data->~Data();
pool.Deallocate(data);
And we also need to deconstruct our data and then return the memory space.
Maybe we can write functions to do these jobs.
Create_and_Destroy.h
// file: Create_and_Destroy.h
#ifndef CreateAndDestroy_h_
#define CreateAndDestroy_h_
#include "DebugOnly.h"
#include <queue> // std::queue
template<typename T>
class BasicPool
{
public:
BasicPool()
{
Log("<BasicPool> BasicPool");
};
// Disallow the copy constructor
BasicPool(const BasicPool &)=delete;
// The move constructor
BasicPool(BasicPool&& other)
: mChunks(std::move(other.mChunks))
{
Log("<BasicPool> BasicPool << Move");
}
~BasicPool()
{
Log("<BasicPool> ~BasicPool");
Clear();
}
// Allocate a chunk of memory for the class object
void* Allocate()
{
Log("<BasicPool> Allocate");
// If there is no more available memory chunk,
// then we create a new one
if (mChunks.empty()) {
return ::operator new(sizeof(T));
}
// Otherwise, return a free memory chunk
void* chunk = static_cast<void*>(mChunks.front());
mChunks.pop();
return chunk;
}
// Reclaim the memory space from those no longer used objects
void Deallocate(void* obsolete)
{
Log("<BasicPool> Deallocate");
mChunks.push(static_cast<T*>(obsolete));
}
// ******************************************************
// * Create and Destroy *
// ******************************************************
// Create a new class object
template<typename... Args>
T* Create(Args&& ...args)
{
T* space = (T*)(Allocate());
try {
new (space) T(std::forward<Args>(args)...);
} catch (...) { // Reclaim allocated memory if new operation failed.
mChunks.push(space);
throw;
}
return space;
}
// Destroy the obsoleted object and reclaim its memory space
void Destroy(T* obsolete)
{
obsolete->~T();
Deallocate(obsolete);
}
// ******************************************************
// Release all of the free memory chunks
void Clear()
{
Log("<BasicPool> Clear");
while (!mChunks.empty()) {
::operator delete(mChunks.front());
mChunks.pop();
}
}
private:
// Holding the pointers to the free memory chunks
std::queue<T*> mChunks;
};
#endif // #ifndef CreateAndDestroy_h_
main.cpp
// file: main.cpp
// #include "Basic.h"
#include "Create_and_Destroy.h"
#include "Data.h"
#include "DebugOnly.h"
// Main Program
// ===================================
int main() {
// Declare a pool object
BasicPool<Data> pool;
// Create an instance of Data from the pool
// void* freeSpace = pool.Allocate();
// Data* data = new (freeSpace) Data(57, "Hello");
Data* data = pool.Create(57, "Hello");
// Print it
PrintData(data);
// Destroy it
// data->~Data();
// pool.Deallocate(data);
pool.Destroy(data);
// Print it again
PrintData(data);
// Create another instance of Data from the pool
// void* freeSpace2 = pool.Allocate();
// Data* data2 = new (freeSpace2) Data(75, "World");
Data* data2 = pool.Create(75, "World");
// Print it
PrintData(data2);
// Destroy it
// data2->~Data();
// pool.Deallocate(data2);
pool.Destroy(data2);
// Print it again
PrintData(data2);
return 0;
}
The result is:
<BasicPool> BasicPool
<BasicPool> Allocate
<Data> Constructor
[0x7fb8cac04b30] 57 Hello
<Data> Deconstructor
<BasicPool> Deallocate
[0x7fb8cac04b30] 57 Hello
<BasicPool> Allocate
<Data> Constructor
[0x7fb8cac04b30] 75 World
<Data> Deconstructor
<BasicPool> Deallocate
[0x7fb8cac04b30] 75 World
<BasicPool> ~BasicPool
<BasicPool> Clear
The result is same except the address is changed to 0x7fb8cac04b30
.
Overriding operator new and delete
Actually, the create
and destroy
are similar to new
and delete
.
Recall what they done in new and delete chapter.
We can override new
and delete
to do that jobs.
New_and_Delete.h
// file: New_and_Delete.h
#ifndef NewAndDelete_h_
#define NewAndDelete_h_
#include "DebugOnly.h"
#include <queue> // std::queue
template<typename T>
class BasicPool
{
public:
BasicPool()
{
Log("<BasicPool> BasicPool");
};
// Disallow the copy constructor
BasicPool(const BasicPool&)=delete;
// The move constructor
BasicPool(BasicPool&& other)
: mChunks(std::move(other.mChunks))
{
Log("<BasicPool> BasicPool << Move");
}
~BasicPool()
{
Log("<BasicPool> ~BasicPool");
Clear();
}
// Allocate a chunk of memory for the class object
void* Allocate()
{
Log("<BasicPool> Allocate");
// If there is no more available memory chunk,
// then we create a new one
if (mChunks.empty()) {
return ::operator new(sizeof(T));
}
// Otherwise, return a free memory chunk
void* chunk = static_cast<void*>(mChunks.front());
mChunks.pop();
return chunk;
}
// Reclaim the memory space from those no longer used objects
void Deallocate(void* obsolete)
{
Log("<BasicPool> Deallocate");
mChunks.push(static_cast<T*>(obsolete));
}
// Release all of the free memory chunks
void Clear()
{
Log("<BasicPool> Clear");
while (!mChunks.empty()) {
::operator delete(mChunks.front());
mChunks.pop();
}
}
private:
// Holding the pointers to the free memory chunks
std::queue<T*> mChunks;
};
template<typename T, template<typename = T> class Pool = BasicPool>
class MemoryPool
{
public:
// Override new
static void* operator new(size_t size)
{
Log("<MemoryPool> new");
return mPool.Allocate();
}
// Override delete
static void operator delete(void* p)
{
Log("<MemoryPool> delete");
mPool.Deallocate(p);
}
// Clear the pool
static void Clear()
{
Log("<MemoryPool> Clear");
mPool.Clear();
}
private:
// Allow only the inherited objects to call it
MemoryPool()
{
Log("<MemoryPool> MemoryPool");
};
MemoryPool(const MemoryPool&)
{
Log("<MemoryPool> MemoryPool << Move");
};
// Make the inherited objects to access its private members
friend T;
static Pool<T> mPool;
};
// Define the static pool variable that will be called
// in constructor and deconstructor
template<typename T, template<typename = T> class Pool>
Pool<T> MemoryPool<T, Pool>::mPool;
#endif // #ifndef NewAndDelete_h_
Data.h
// file: Data.h
#ifndef Data_h_
#define Data_h_
#include "New_and_Delete.h" // For MemoryPool
#include "DebugOnly.h"
// Data Structure for Demo
// ===================================
struct Data: public MemoryPool<Data> // Inherit to override new and delete
{
int mValue;
std::string mString;
Data(int val, std::string str)
: mValue(val)
, mString(str)
{
Log("<Data> Constructor");
}
~Data()
{
Log("<Data> Deconstructor");
}
};
// Helper functions
// ===================================
void PrintData(Data* data)
{
Log("[" + ToAddressString(*data) + "] " + // address
std::to_string(data->mValue) + " " + // value
data->mString); // string
}
#endif // #ifndef Data_h_
main.cpp
// file: main.cpp
#include "Data.h"
#include "DebugOnly.h"
// Main Program
// ===================================
int main() {
// Create an instance of Data from our pool
Data* data = new Data(99, "It seems normal");
// Print it
PrintData(data);
// Destroy it
delete data;
// Print it again
PrintData(data);
// Create another instance of Data from our pool
Data* data2 = new Data(999, "but it's different!");
// Print it
PrintData(data2);
// Destroy it
delete data2;
// Print it again
PrintData(data2);
return 0;
}
The result is:
<BasicPool> BasicPool
<MemoryPool> new
<BasicPool> Allocate
<MemoryPool> MemoryPool
<Data> Constructor
[0x7fb0d1404b30] 99 It seems normal
<Data> Deconstructor
<MemoryPool> delete
<BasicPool> Deallocate
[0x7fb0d1404b30] 99 It seems normal
<MemoryPool> new
<BasicPool> Allocate
<MemoryPool> MemoryPool
<Data> Constructor
[0x7fb0d1404b30] 999 but it's different!
<Data> Deconstructor
<MemoryPool> delete
<BasicPool> Deallocate
[0x7fb0d1404b30] 999 but it's different!
<BasicPool> ~BasicPool
<BasicPool> Clear
Again, same address 0x7fb0d1404b30
is used for two objects.
Performance Test
Now, we compare the time using memory pool mechanism or not.
New_and_Delete.h
#ifndef NewAndDelete_h_
#define NewAndDelete_h_
#include <queue> // std::queue
template<typename T>
class BasicPool
{
public:
BasicPool()
{
};
// Disallow the copy constructor
BasicPool(const BasicPool&)=delete;
// The move constructor
BasicPool(BasicPool&& other)
: mChunks(std::move(other.mChunks))
{
}
~BasicPool()
{
Clear();
}
// Allocate a chunk of memory for the class object
void* Allocate()
{
// If there is no more available memory chunk,
// then we create a new one
if (mChunks.empty()) {
return ::operator new(sizeof(T));
}
// Otherwise, return a free memory chunk
void* chunk = static_cast<void*>(mChunks.front());
mChunks.pop();
return chunk;
}
// Reclaim the memory space from those no longer used objects
void Deallocate(void* obsolete)
{
mChunks.push(static_cast<T*>(obsolete));
}
// Release all of the free memory chunks
void Clear()
{
while (!mChunks.empty()) {
::operator delete(mChunks.front());
mChunks.pop();
}
}
private:
// Holding the pointers to the free memory chunks
std::queue<T*> mChunks;
};
template<typename T, template<typename = T> class Pool = BasicPool>
class MemoryPool
{
public:
// Override new
static void* operator new(size_t size)
{
return mPool.Allocate();
}
// Override delete
static void operator delete(void* p)
{
mPool.Deallocate(p);
}
// Clear the pool
static void Clear()
{
mPool.Clear();
}
private:
// Allow only the inherited objects to call it
MemoryPool() {};
MemoryPool(const MemoryPool&) {};
// Make the inherited objects to access its private members
friend T;
static Pool<T> mPool;
};
// Define the static pool variable that will be called
// in constructor and deconstructor
template<typename T, template<typename = T> class Pool>
Pool<T> MemoryPool<T, Pool>::mPool;
#endif // #ifndef NewAndDelete_h_
DataTest.h
#ifndef DataTest_h_
#define DataTest_h_
#include "New_and_Delete.h"
// Data Structure for Test
// ===================================
struct Data
{
int mValue;
std::string mString;
Data(int val, std::string str)
: mValue(val)
, mString(str)
{
}
~Data() {};
};
struct DataWithPool: public MemoryPool<DataWithPool>
{
int mValue;
std::string mString;
DataWithPool(int val, std::string str)
: mValue(val)
, mString(str)
{
}
~DataWithPool() {};
};
#endif // #ifndef DataTest_h_
main.cpp
#include <iostream>
#include "DataTest.h"
#include "DebugOnly.h"
#define TIMES 40000
#define SIZE 1000
// Main Program
// ===================================
int main() {
// Without the Memory Pool Mechanism
Data* set[SIZE];
const clock_t beginTime1 = std::clock();
for (unsigned int i = 0 ; i < TIMES ; i++) {
// New the objects
for (unsigned int j = 0 ; j < SIZE ; j++) {
set[j] = new Data(99, "Hello World");
}
// Release the objects
for (unsigned int j = 0 ; j < SIZE ; j++) {
delete set[j];
}
}
std::cout << "time: " << float( std::clock() - beginTime1 ) / CLOCKS_PER_SEC << std::endl;
// With the Memory Pool Mechanism
DataWithPool* poolSet[SIZE];
const clock_t beginTime2 = std::clock();
for (unsigned int i = 0 ; i < TIMES ; i++) {
// New the objects
for (unsigned int j = 0 ; j < SIZE ; j++) {
poolSet[j] = new DataWithPool(99, "Hello World");
}
// Release the objects
for (unsigned int j = 0 ; j < SIZE ; j++) {
delete poolSet[j];
}
}
std::cout << "time: " << float( std::clock() - beginTime2 ) / CLOCKS_PER_SEC << std::endl;
return 0;
}
The results:
time: 5.3889
time: 3.63267
The number itself is meaningless.
It really depends on your computer environment.
The important thing is the comparison.
The result reveals the fact that
the new
and delete
with memory pool mechanism
is faster(3.63267) than without it(5.3889)!