The first problem which can occur is excessive memory use, resulting in reduced performance. This can occur when the malloc() function allocates memory using fixed size buckets. In this case, wasted space within buckets accumulates, the result being that the overall memory working set can become quite large, with excessive paging activity occurring if the amount of physical memory is insufficient.
A second problem can occur where the malloc() function uses a best fit allocation strategy. In this case, the time taken in coallescing blocks of deallocated memory back together, can become quite excessive. The standard malloc() library provided with SunOS has been found to be quite bad in this respect.
To improve performance for allocation of small objects, a layer on top of operator new() is provided with the library. This layer implements a set of common memory pools for small objects. In addition to the common memory pool, a class pool, memory arena, object clustering and heap style allocators are provided for direct use. Used correctly, these can result in your application using less memory and execute with either a greater speed, or speed compareable to existing memory allocation schemes.
The common memory pools implemented by the library are accessed by using the class OTC_CommonPool. To make use of the common memory pool, your class must override operator new() and operator delete(). Alternatively, you can inherit your class from a base class which overrides operator new() and operator delete() in the required fashion. In the latter case, the base class must have a virtual destructor. This is necessary to ensure that correct information about the size of the derived class is passed to the overridden version of operator delete().
The implementations of operator new() and operator delete() for the class should be as follows:
#include <OTC/memory/cmmnpool.hh>It is preferable that the implementations are inline, as this will avoid the overhead of an extra function call.
class EX_Foo
{
public:
virtual ~EX_Foo();
void* operator new(size_t theSize)
{ return OTC_CommonPool::allocate(theSize); }
void operator delete(
void* theMemory, size_t theSize
)
{ OTC_CommonPool::release(theMemory,theSize); }
};
When operator new() is used on this class, the function OTC_CommonPool::allocate() will be called with the size of the object. If the size of the object is less than or equal to what is handled by the memory pools, the appropriate memory pool will be used to allocate the memory. If the size of the object is larger than what the memory pools are capable of handling, ::operator new() will be used to perform the allocation.
When you delete the object, the function OTC_CommonPool::release() is called. In this case, the pointer to the memory used by the object, and the size of the class are passed as arguments. The size is used to determine to which memory pool the memory should be returned, or if ::operator delete() should be called to dispose of the memory.
If you expect that your class will be used as a base class for other classes, you must make the destructor of your class virtual. If this is not done, and an instance of the derived class is deleted through a pointer to the base class, the size of the base class object, and not the derived class object, will be passed to operator delete(). If the derived class was larger than the base class, this could result in the memory being returned to the wrong memory pool, or being returned to a memory pool when it should have been disposed of using operator delete().
At present, eight separate memory pools are used. The memory returned by the memory pools varies in size from the size of the type double, up to eight times the size of the type double. The pools vary in size by the size of the type double. The size of the pools is based on the size of the type double as it generally has the worst case alignment requirements of all the builtin types. For a 32 bit architecture, this results in the memory pools varying in size from 8 bytes, up to 64 bytes, in 8 byte increments.
When a request for memory occurs, and the memory pool corresponding to the required size has no free memory, a single chunk of memory is allocated using ::operator new(). This block of memory is cut into blocks corresponding to the size of memory managed by that pool, and placed on an internal free list. A free block is then returned by the memory pool for use. At present the size of the block allocated using ::operator new() is 2040 bytes. The size of the block is not a power of 2, to allow for overhead involved in the malloc() library for tracking allocated blocks of memory. If you desire, you can change the default value of 2040 bytes by setting the environment variable OTCLIB_POOLBLOCKSIZE to the desired value before running your program.
When you return a block of memory to the common pool, if the size of the memory mean't that it was allocated using ::operator new(), it will be disposed of by calling ::operator delete(). If the block was allocated from one of the memory pools, it will be returned to the free list of the memory pool. At no time are blocks held by a specific memory pool returned to the malloc() library by calling ::operator delete().
If you are using memory analysis tools such as Purify or Sentinel, the fact that memory allocated by the common memory pools is not returned to the malloc() library, means that memory leaks and access to memory which is not in use will not be detected. In order that such problems are detected, the common memory pools must be disabled when you are running an application embedding the support provided by these tools. To disable the common memory pools you need to define the environment variable OTCLIB_NOCOMMONPOOL before running your program. When defined this will result in the OTC_CommonPool class ignoring the memory pools and allocating memory for each request using ::operator new(). When memory is given back the to the common pool set, the OTC_CommonPool class will immediately dispose of the memory using ::operator delete().
The common memory pool mechanism is utilised in the class library for all commonly allocated small objects. These include the buckets used in the linked list structures, and the nodes in AVL tree structures. This means that you need not do anything special if you are only using the collection classes in the library. If you are creating your own data structures though, you may wish to use this mechanism.
To make the memory pools easier to use, any class for which you wish the memory to be allocated from the memory pools, can be derived from the class OTC_MPObject. The OTC_MPObject class provides versions of operator new() and operator delete(). The versions of these functions will automatically be inherited by your class. The OTC_MPObject class also provides a virtual destructor. The virtual destructor ensures that information passed to operator delete(), about the size of a class, is always correct.
If you are using the ObjectStore OODBMS through its library interface, the OTC_MPObject class provides additional versions of operator new(), corresponding to the different sets of arguments which can be passed to operator new() when ObjectStore is used. Memory pools are not used for allocating objects in the database. Memory pools will only be used for transient objects when ObjectStore is used. When using ObjectStore through its DML interface, the OTC_MPObject does not define any versions of operator new() or operator delete(). Not defining these functions disables the use of memory pools for transient objects when using the ObjectStore DML interface.
To use the OTC_Pool class, you need to add a static member variable to your class. This member variable will point to the instance of the OTC_Pool class specific to your class type. In addition to the static member variable, you still need to override operator new() and operator delete(). If you expect that your class will be used as a base class for other classes, and a derived class may be deleted using a pointer to the base class, the destructor of your class must be virtual.
The additions to your class should be as given below:
#include <OTC/memory/pool.hh>As the memory pool only returns memory of one size, the overridden operator new() and operator delete() must check for allocations and deletions of memory which are greater than the size of memory which the memory pool is giving out. The check is performed against the value returned by the elementSize() member function of the OTC_Pool class rather than sizeof(EX_Foo). This is done as the memory pool will always round up the size of the memory to be a multiple of the memory alignment factor of the type double. Because of the rounding, the memory pool may return a few more bytes than required by the base class, however, that extra few bytes may be enough to hold extra information contained in a derived class. Making use of this extra memory when possible, means that the more expensive call to ::operator new() can be avoided. Rounding up to a multiple of the alignment factor of the type double is performed as the type double generally has the worst memory alignment requirements.
class EX_Foo
{
public:
virtual ~EX_Foo();
void* operator new(size_t theSize);
void operator delete(void* theMemory, size_t theSize);
private:
static OTC_Pool* myPool;
};
OTC_Pool* EX_Foo::myPool = 0;
void* EX_Foo::operator new(size_t theSize)
{
if (myPool == 0)
{
myPool = new OTC_Pool(sizeof(EX_Foo));
OTCLIB_ASSERT(myPool != 0);
}
if (theSize <= myPool->elementSize())
return myPool->allocate();
else
return ::operator new(theSize);
}
void EX_Foo::operator delete(
void* theMemory, size_t theSize
)
{
OTCLIB_ASSERT(myPool != 0);
if (theSize <= myPool->elementSize())
myPool->release(theMemory);
else
::operator delete(theMemory);
}
If you are using the ObjectStore OODBMS through the DML interface, you are not going to be able to use class specific memory pools. This stems from a deficiency in ObjectStore whereby type information for the class being allocated is not available in an overridden version of operator new(). The case where this causes a problem is where operator new() is inherited from a base class, and an instance of the derived class is created. Under these circumstances, the overridden operator new() cannot know that it should really be creating a typespec object file a derived class. As a result, all memory pool related code should be surrounded with the preprocessor check:
#if !defined(__OS_DML)If you are using the ObjectStore OODBMS through its library interface, you need to add additional checks to the overridden version of operator delete(). You also need to add additional versions of operator new() to cope with requests to create an instance of your class in an ObjectStore database, segment or cluster. Assuming that you take the simple course of only using the memory pool for objects in transient memory, the overridden operator delete() will be as follows:
...
#endif
void EX_Foo::operator delete(The overridden version of operator new() given above does not need to change. The three additional versions of operator new() which you will need to add are:
void* theMemory, size_t theSize
)
{
if (os_segment::of(theMemory) !=
os_segment::get_transient_segment())
{
::operator delete(theMemory);
return;
}
OTCLIB_ASSERT(myPool != 0);
if (theSize <= myPool->elementSize())
myPool->release(theMemory);
else
::operator delete(theMemory);
}
void* EX_Foo::operator new(If you are using memory pools for objects allocated in the database, as well as in transient memory, the above will need to modified. The change required, will be that rather than calling ::operator new() for persistant objects, you would use the in database memory pool mechanism to allocate the memory.
size_t theSize,
os_segment* theSegment,
os_typespec* theTypeSpec
)
{
if (theSegment != os_segment::get_transient_segment())
return ::operator new(theSize,theSegment,theTypeSpec);
if (myPool == 0)
{
myPool = new OTC_Pool(sizeof(EX_Foo))
OTCLIB_ASSERT(myPool != 0);
}
if (theSize <= myPool->elementSize())
return myPool->allocate();
else
return ::operator new(theSize,theSegment,theTypeSpec);
}
void* EX_Foo::operator new(
size_t theSize,
os_database* theDatabase,
os_typespec* theTypeSpec
)
{
return ::operator new(theSize,theDatabase,theTypeSpec);
}
void* EX_Foo::operator new(
size_t theSize,
os_object_cluster* theCluster,
os_typespec* theTypeSpec
)
{
return ::operator new(theSize,theCluster,theTypeSpec);
}
When a request for memory is made to the memory pool, and the memory pool has no free memory, a single chunk of memory is allocated using ::operator new(). This block of memory is cut into blocks corresponding to the size of memory managed by that pool, and placed on an internal free list. A free block is then returned by the memory pool for use. At present the size of the block allocated using ::operator new() is 2040 bytes. The size of the block is not a power of 2, to allow for overhead involved in the malloc() library for tracking allocated blocks of memory. If you desire, you can change the default value of 2040 bytes by setting the environment variable OTCLIB_POOLBLOCKSIZE to the desired value before running your program.
When you return a block of memory to a memory pool, it will be returned to the free list of the memory pool. Blocks of memory held by a memory pool are only returned to the malloc() library by calling ::operator delete() when the memory pool is deleted. As the memory pool will never be deleted in the way it is being used here, the memory will subsequently never be returned to the malloc() library. As this is the case, tools such as Purify and Sentinel will not be able to detect memory leaks and references to memory no longer in use. As a result you may consider providing a mechanism to allow the memory pools to be disabled.
When a request is made for a block of memory from an instance of the OTC_Arena class, and insufficient memory is currently available, it will take one of two actions. If the size of the memory block which has been requested is less than a preset block size, a new block of memory of the preset block size will be allocated. A block of memory of the requested size will be allocated from the front of this block and returned. If the requested block is larger in size than the preset block size, the block of memory will be allocated using operator new(). When there is sufficient free memory in a previously allocated block of memory, the requested memory will be allocated from that block.
By default, the preset block size is 2040 bytes. The default value can be changed by setting the environment variable OTCLIB_ARENABLOCKSIZE prior to executing your application. You can also override this value on a per class basis using the appropriate constructor of the OTC_Arena class. A further default parameter of the OTC_Arena class is that when the amount of available memory in a block decreases below 16 bytes, that block will not subsequently used for further allocations. This strategy eliminates the wasted time which would otherwise be used up in searching through a list of memory blocks with only a small amount of memory left. The default value of 16 bytes for this slop factor can be changed by setting the environment variable OTCLIB_ARENASLOPSIZE to an appropriate value prior to executing your application. This value may also be set on a per instance basis by passing a value as the appropriate argument of the constructor of the OTC_Arena class.
Any memory returned from an instance of the OTC_Arena class, is by default aligned according to the alignment requirements of the type double. The alignment factor of type double is used as in general it has the worst case alignment requirements. The alignment for the type double usually corresponds to that of the size of the type. On 32 bits systems, the type double is nearly always 8 bytes. If you want memory to be aligned on byte boundaries other than 8, you can pass this information as the last argument to the constructor of the OTC_Arena class. An instance where this would be advantageous is where numerous string values are being packed into a memory arena. Specifying memory alignment of 1 byte will ensure that no wasted space will exist between the strings. For example:
OTC_Arena theArena(1);
OTC_List<char const*> theList;
char theBuffer[1024];
ifstream theStream("/usr/dict/words");
while (theStream.good())
{
theStream >> theBuffer;
if (!theStream.fail())
{
char* theString =
theArena.allocate(strlen(theBuffer)+1);
strcpy(theString,theBuffer);
theList.addLast(theString);
}
}
To allocate an object, and have it associated with a specific cluster, an overloaded version of operator new() is used. For example:
OTC_Cluster cluster;The overloaded version of operator new() takes as argument the cluster in which the object should be allocated. For this to work, all classes to be allocated in a cluster, must be derived from the class OTC_MCObject. It is from this base class, that the overloaded version of operator new() is inherited by your class.
EX_Foo* foo = new (cluster) EX_Foo;
A class which derives from OTC_MCObject can only be allocated in a cluster. It will not be possible to allocate instances of your class in normal heap memory, on the stack, as global data, or as member variables of other classes. Due to these limitations, use of the OTC_Cluster and OTC_MCObject classes is quite specialised, your classes only being able to be used for this purpose.
Once an object has been allocated in a cluster, you should not explicitly delete the object. All objects held in a cluster will be automatically deleted, and memory used by the objects reclaimed, when the instance of the OTC_Cluster class is destroyed. If you do delete an object held in a cluster, the object will get destroyed a second time when the cluster is destroyed. The result of the object being destroyed twice will be undefined.
To prevent an object from being able to be explicitly deleted, your class should define the destructor of the class with protected access. This will ensure that the compiler flags as an error, an attempt to delete the object.
By default, the preset block size of memory allocated by the arena class held by the cluster, is 2040 bytes. The default value can be changed by setting the environment variable OTCLIB_ARENABLOCKSIZE prior to executing your application. You can also override this value on a per class basis using the appropriate constructor of the OTC_Cluster class. A further default parameter of the OTC_Arena class is that when the amount of available memory in a block decreases below 16 bytes, that block will not subsequently used for further allocations. This strategy eliminates the wasted time which would otherwise be used up in searching through a list of memory blocks with only a small amount of memory left. The default value of 16 bytes for this slop factor can be changed by setting the environment variable OTCLIB_ARENASLOPSIZE to an appropriate value prior to executing your application. This value may also be set on a per instance basis by passing a value as the appropriate argument of the constructor of the OTC_Cluster class.
Note that when memory is obtained from the arena class by the cluster, the arena class will return a piece of memory the size of which is a multiple of the alignment factor for the type double. This is done to ensure that the start of memory returned from the arena is always appropriately aligned. Therefore, defining the block size to be the sum of the sizes of the objects you intend placing in the cluster, may result in one block being of insufficient size to hold the objects. To ensure that one block of memory will be sufficient to hold all your objects, the size of the objects should be rounded up to a multiple of the alignment factor of the type double before the sizes are summed. For example:
u_int const TYSZ = sizeof(EX_Foo);
u_int const ALSZ = OTC_Alignment::ofDouble();
u_int const NOBJ = 20;
u_int theBlockSize =
NOBJ * (((TYSZ + ALSZ - 1) / ALSZ) * ALSZ);
OTC_Cluster cluster(theBlockSize);
The method used to track allocations is the boundary tag method. Due to memory overhead of this method, the memory block must provide additional space for initial free list information. The amount of memory required for this is returned by the function OTC_Heap::minimum(). Memory returned by the OTC_Heap class will be aligned according to the alignment factor of the type double.
Using the OTC_Heap class, the above example would be rewritten as:
int const HEAPSIZE = 0x80000;The OTC_Heap class is only a basic heap style allocator. Compared to malloc() libraries which are available, it would probably be less efficient. Thus if you are after an efficient memory allocator, you would be better looking elsewhere.
char* theMemory = new char[HEAPSIZE];
OTC_Heap theHeap(theMemory,HEAPSIZE);
OTC_List<char const*> theList;
char theBuffer[1024];
ifstream theStream("/usr/dict/words");
while (theStream.good())
{
theStream >> theBuffer;
if (!theStream.fail())
{
char* theString = theHeap.allocate(strlen(theBuffer)+1);
strcpy(theString,theBuffer);
theList.addLast(theString);
}
}
...
delete [] theMemory;
The class OTC_Alignment is provided as a means of finding the memory alignment factors of the standard types, structures and pointers. Values returned by member functions of the class, represent the small number for which the initial memory address of a type, must be a multiple. An example of using the class is given in the following code. The code prints out the alignment factors for the standard types, structures and pointers.
cout << "CHAR " << OTC_Alignment::ofChar() << endl;Memory allocators can use this class to ensure that memory returned is aligned appropriately. As the type double usually has the worst case alignment requirements, memory returned by a memory allocator would always start at an address which is a multiple of the value returned by the function OTC_Alignment::ofDouble().
cout << "SHORT " << OTC_Alignment::ofShort() << endl;
cout << "INT " << OTC_Alignment::ofInt() << endl;
cout << "LONG " << OTC_Alignment::ofLong() << endl;
cout << "FLOAT " << OTC_Alignment::ofFloat() << endl;
cout << "DOUBLE " << OTC_Alignment::ofDouble() << endl;
cout << "STRUCT " << OTC_Alignment::ofStruct() << endl;
cout << "WPTR " << OTC_Alignment::ofWPtr() << endl;
cout << "BPTR " << OTC_Alignment::ofBPtr() << endl;
static Object* object = 0;If the pointer to the object is a member variable of a class, it is relatively clear at what point the object should be deleted, that is, in the destructor of the class. When the pointer to the object is a static or local variable, deletion of the object is complicated. In the case of a static variable, deletion of the object needs to be tied to the destructor of a specific static object. Alternatively the object is not deleted at all. In the case of a local variable, every exit point from the function must perform a check of the pointer and if it is not zero delete the object at that point.
void function()
{
if (object == 0)
object = new Object;
cout << object->id() << endl;
}
To simplify use of this approach to reducing memory usage by delaying allocation, two template classes are provided. These classes are OTC_Ptr and OTC_VecPtr. To use these classes, they are parameterised with the type of the object and an instance of the pointer class created. At the point that they are created, they are initialised to zero. An object of the type required is only created when an attempt is made to access the object. When the pointer object is deleted, the object it points at will be deleted automatically. The above example would be rewritten as shown below.
#include <OTC/memory/ptr.hh>As the object can be created at any time, there is no mechanism to provide arguments to the object when it is constructed. The object therefore, must provide a constructor which takes no arguments, or which has default arguments.
static OTC_Ptr<Object> object;
void function()
{
cout << object->id() << endl;
}
The OTC_VecPtr class is similar to OTC_Ptr except that it is specifically for arrays of objects. When an instance of the OTC_VecPtr class is created, it is necessary for the size of the array to be specified. As an array of objects is being created, the object must again provide a constructor which takes no arguments, or which has default arguments.
As an attempt to access the object pointer would result in the objects creation if it did not exist, a comparison against zero cannot be used to determine if the object exists. For both classes, if it is necessary to determine if the object or array had been created, the isUndefined() member function must be used. This is illustrated below:
#include <OTC/memory/vecptr.hh>
static OTC_VecPtr<Object> array(8);
void function()
{
if (!array.isUndefined())
{
for (int i=0; i<array.size(); i++)
cout << array[i]->id() << endl;
}
}