Another approach to speeding up allocation is to allocate in bulk and thus amortize the cost of memory allocation across many calls to operator new. The implementation of Links, Lists and HashTables is as before, except that a new class, LinkAllocator is used in place of FreeList. It has the same interface as FreeList, but its implementation is more involved. Besides keeping a list of recycled Links, it also has a separate list of blocks of links. Each block consists of a header of class Block and a block of 16 consecutive raw pieces of memory each the size of a Link.
class Link;
class LinkAllocator
{
enum { BlockLinks = 16 };
class Block
{
public:
Block * Next () { return _next; }
void SetNext (Block * next) { _next = next; }
private:
Block * _next;
};
public:
LinkAllocator () : _p (0), _blocks (0) {}
~LinkAllocator ();
void Purge ();
void * NewLink ();
void Recycle (void * link);
private:
Link * _p;
Block * _blocks;
};
|
This is how a new Link is created:
void * LinkAllocator::NewLink ()
{
if (_p == 0)
{
// use global operator new to allocate a block of links
char * p = ::new char [sizeof (Block) + BlockLinks * sizeof (Link)];
// add it to the list of blocks
Block * block = reinterpret_cast<Block *> (p);
block->SetNext (_blocks);
_blocks = block;
// add it to the list of links
p += sizeof (Block);
for (int i = 0; i < BlockLinks; ++i)
{
Link * link = reinterpret_cast<Link *> (p);
link->_pNext = _p;
_p = link;
p += sizeof (Link);
}
}
void * mem = _p;
_p = _p->_pNext;
return mem;
}
|
The first block of code deals with the situation when there are no unused links in the Link list. A whole block of 16 (BlockLinks) Link-sized chunks is allocated all at once, together with some room for the Block header. The Block is immediately linked into the list of blocks and then chopped up into separate Links which are added to the Link list. Once the Link list is replenished, we can pick a Link from it and pass it out.
The implementation of Recycle is the same as before--the links are returned to the Link list. Purge, on the other hand, does bulk deallocations of whole blocks.
void LinkAllocator::Purge ()
{
while (_blocks != 0)
{
// it was allocated as an array of char
char * mem = reinterpret_cast (_blocks);
_blocks = _blocks->Next();
::delete [] mem;
}
}
|
Only one call in 16 to new Link results in actual memory allocation. All others are dealt with very quickly by picking a ready-made Link from a list.
Array new
Even though class Link has overloaded operators new and delete, if you were to allocate a whole array of Links, as in new Link [10], the compiler would call global new to allocate enough memory for the whole array. It would not call the class-specific overload. Conversly, deleting such an array would result in the call to global operator delete--not it's class-specific overload.
Since in our program we never allocate arrays of Links, we have nothing to worry about. And even if we did, global new and delete would do the right thing anyway.
However, in the unlikely case when you actually want to have control over array allocations, C++ provides a way. It let's you overload operators new[] and delete[]. The syntax and the signatures are analogous to the overloads of straight new and delete.
void * operator new [] (size_t size);
void operator delete [] (void * p);
|
The only difference is that the size passed to new[] takes into account the total size of the array plus some additional data used by the compiler to distinguish between pointers to objects and arrays of objects. For instance, the compiler has to know the number of elements in the array in order to be able to call destructors on all of them when delete [] is called.
All four operators new, delete, new[] and delete[] are treated as static members of the class that overloads them (i.e., they don't have access to this).
Global new
Unlike class-specific new, global new is usually overloaded for debugging purposes. In some cases, however, you might want to overload global new and delete permanently, because you have a better allocation strategy or because you want more control over it.
In any case, you have a choice of overriding global new and delete or adding your own special versions that follow a slightly different syntax. Standard operator new takes one argument of type size_t. Standard delete takes one orgument of type void *. You can define your own versions of new and delete that take additional arguments of arbitrary types. For instance, you can define
void * operator new (size_t size, char * name);
void operator delete (void * p, char * name);
|
and call the special new using this syntax:
Foo * p = new ("special") Foo;
|
Unfortunately, there is no way to call the special delete explicitly, so you have to be sure that standard delete will correctly handle memory allocated using your special new (or that delete is never called for such objects).
So what's the use of the overloaded delete with special arguments? There is actually one case in which it will be called--when an exception is thrown during object construction. As you might recall, there is a contract implicit in the language that if an exception happens during the construction of an object, the memory for this object will be automatically deallocated. It so happens that during object's construction the compiler is still aware of which version of operator new was called to allocate memory. It is therefore able to generate a call to the corresponding version of delete, in case an exception is thrown. After the successful completion of construction, this information is no longer available and the compiler has no means to guess which version of global delete is appropriate for a given object.
Once you have defined an overloaded version of new, you can call it explicitly, by specifying additional argument(s). Or you can substitute all calls to new in your code with the overloaded version using macro substitution.
Macros
We haven't really talked about macros in this book--they are a part of standard C++, but their use is strongly discouraged. In the old times, they were used in place of the more sophisticated C++ features, such as inline functions and templates. Now that there are better ways of getting the same functionality, macros are fast becoming obsolete. But just for completeness, let me explain how they work.
A macro works through literal substitution. You may think of macro expansion as a separate process performed by the compiler before even getting to the main task of parsing C++ syntax. In fact, in older compilers, macro expansion was done by a separate program, the preprocessor.
There are two major types of macros. The first type simply substitutes one string with another, in the code that logically follows it (by logically I mean that, if the macro is defined in an include file, it will also work in the file that includes it, and so on). Let me give you an example that might actually be useful. Let's define the following macro in the file dbnew.h
#define new new(__FILE__, __LINE__)
|
This macro will substitute all occurrences of new that logically follow it with the string new (__FILE__, __LINE__). Moreover, the macro preprocessor will then substitute all occurrences of the special pre-defined symbol __FILE__ with the full name of the source file in which it finds it; and all occurrences of __LINE__ with the appropriate line number. So if you have a file c:\test\main.cpp with the contents:
#include "dbnew.h"
int main ()
{
int * p = new int;
return 0;
}
|
it will be pre-processed to produce the following code:
int main ()
{
int * p = new ("c:\test\main.cpp", 4) int;
return 0;
}
|
Now you can use your own overloaded operator new, for instance to trace all memory allocation. Here's a simple example of such implementation.
void * operator new (size_t size, char const * file, int line)
{
std::cout << file << ": " << line << std::endl;
return ::new char [size];
}
|
Notice that we have to make sure that our macro is not included in the file that defines this overload. Otherwise both occurrences of "new" would be substituted by "new(__FILE__, __LINE)" resulting in incorrect code.
The second type of macro also works through textual substitution, but it behaves more like an inline function-- it takes arguments. And again, since macro expansion works outside of the C++ compiler, there is no type checking and a possibility of unexpected side effects. A classic example is the max macro:
#define max(a, b) (((a) > (b))? (a): (b))
|
Notice the parentesis paranoia--a characteristic feature of macros. Programmers learned to put parentheses around macro parameters, because they might be expressions containing low precedence operators. Consider, for instance, what happens when you call
c = max (a & mask, b & mask)
|
Without the parentheses around parameters in the definition of max, the preprocessor would expand it into
c = a & mask > b & mask? a & mask: b & mask;
|
which, because of operator precedence rules, would be interpreted as:
c = (a & (mask > b) & mask)? (a & mask): (b & mask;)
|
The result of this calculation would most likely be erroneous.
Things get even worse, when you call a macro with expressions that have side effects. Consider for instance the expansion of max (a++, b++):
(((a++) > (b++))? (a++): (b++))
|
One of the variables will be incremented twice, the other once. This is probably not what the programmer expected.
By the way, there is one more gotcha--notice that I didn't put a space between.
Tracing Memory Leaks
|