Preface
In software development, we often face problems while our application is under high stress. Most of the problem is due to issues in memory management. So developers should take some extra care so that an application will be optimized and follows proper memory management principles.
Introduction
Object Pooling is a service that is documented in MTS but is fully supported by Microsoft .NET. The MTS Glossary defines Pooling as "A performance optimization based on using collections of pre-allocated resources, such as objects or database connections." It is also mentioned that pooling results in more efficient resource allocation.
Background
Let�s assume an application which needs to allocate some memory for its full execution and this application is multithreaded. This means simultaneously more threads can allocate the same amount of memory. Consider that the application needs 100K memory for its execution and there are 50 threads running simultaneously. All 50 threads will call a method of the application simultaneously.
All the threads will try to allocate 100000 bytes in the heap simultaneously. The OS may not get enough time to swap pages. Thus the application will be under heavy concurrent access, and the application may fail due to misleading memory management!
An interesting question that can come to your mind regarding the use of pinning (i.e. GC.AddrOfPinnedObject()
).
Solution to the problem
There are two solutions for this problem:
- We can request 50 times 100K bytes at runtime suddenly. But here the usage of memory will be huge.
- WE can also create a pool of as many as the expected high volume byte arrays in the heap, at startup. That way, say we create 100 times 100K bytes in memory at startup. If it fails, well enough - we know that right at startup! So there is no surprise at runtime, when the application is live! So we can reduce that number to 75 (from 100) and try again to start our application. Once up and running, we know that there will be barely a need for it to allocate more memory at runtime, as we already have the memory for 75 or 100 arrays allocated. It just uses that as "an object pool", pulls from it, and when done, returns the byte arrays for subsequent use. Chances are that unless truly "concurrent", the already allocated byte arrays will be re-used over and over again, and at runtime, no extra memory will be allocated. If all 75 or 100 arrays are in use, and a 101st request comes in, it will need to allocate an additional memory of only one array - which is easy for the OS to manipulate in RAM.
So the second method is preferable because this process will use the memory in a more optimized way, by which we can achieve our target of concurrent access.
Using the code
Implementing object pooling involves creating a "queue" which is a wrapper class that will hold, at runtime, the initially allocated 75 or 100 byte arrays. Each thread, when needed, will request a byte array from it. It will traverse its list of arrays to find out which one is "free". It will give that array for usage to the thread, and mark it as "locked" (so that it does not give the same array to the next requestor). When this thread is done, it will notify the queue, and the queue will simply make the array available for the next requestor. If a request for an array comes at a point of time when there are none free, then the queue object will create an additional array and give it back to the byte pool, increasing the size of the pool by 1.
public class ByteArray
{
private byte [] byArray;
private int iAddress;
private bool bLocked;
private GCHandle GC;
public ByteArray( int iMaxSize )
{
byArray = null;
iAddress = 0;
bLocked = false;
try
{
byArray = new byte [ iMaxSize ];
}
catch( Exception ex )
{
throw new Exception( "Error trying" +
" to create byte array: " + ex.Message );
}
}
public byte [] GetByteArray()
{
return byArray;
}
public int GetAddressOfByteArray()
{
return iAddress;
}
private void Reset()
{
byArray.Initialize();
}
public bool Lock()
{
...
}
public void Unlock()
{
...
}
public bool IsLocked()
{
return bLocked;
}
}
The initial size (MaxConcurrentHits
) of the pool is very important and this number should only be the maximum number of concurrent hits. Under a steady (along the time axis) load, even if it is high volume, concurrency matters - so keeping it to a value like 100 is absolutely safe.
At the same time, the maximum memory size (MaxSize
) is also very important. Because each byte array is allocated a size equal to that. For different purposes, set a different value.
public class PoolByteArrays
{
private ArrayList byarArray;
private int iCurrentPoolSize;
private int iMaxByteArraySize;
public PoolByteArrays( int iMaxConcurrentThreads, int iMaxSize )
{
byarArray = null;
iCurrentPoolSize = 0;
iMaxByteArraySize = iMaxSize;
if( iMaxConcurrentThreads < 1 )
throw new Exception( "Construction of " +
"PoolByteArrays failed - iMaxConcurrentThreads" +
" is less than 1" );
try
{
byarArray = new ArrayList( iMaxConcurrentThreads );
for( int iCnt = 0; iCnt < iMaxConcurrentThreads; iCnt++ )
byarArray.Add( new ByteArray( iMaxSize ) );
iCurrentPoolSize = iMaxConcurrentThreads;
}
catch( Exception ex )
{
throw new Exception( "Construction of" +
" PoolByteArrays failed - " + ex.Message );
}
}
public int GetAddressOfAFreeByteArray( ref int iAddress )
{
try
{
for( int jCnt = 1; jCnt <= 2; jCnt++ )
{
for( int iCnt = 0; iCnt < iCurrentPoolSize; iCnt++ )
{
ByteArray byarInstance = ( ByteArray ) byarArray[ iCnt ];
if( byarInstance.IsLocked() )
continue;
if( ! byarInstance.Lock() )
continue;
iAddress = byarInstance.GetAddressOfByteArray();
return iCnt;
}
}
}
catch( Exception ex )
{
throw new Exception( "Exception trying" +
" to locate and lock a free byte array." +
" Details: " + ex.Message );
}
...
...
}
public string GetStringStoredInByteArray( int iID )
{
return( ( new ASCIIEncoding()).GetString( ( ( ByteArray )
byarArray[ iID ] ).GetByteArray() ) ).TrimEnd( new char [] {
Convert.ToChar( ( int ) 0 ) } );
}
public void DoneWithByteArray( int iID )
{
try
{
( ( ByteArray ) byarArray[ iID ] ).Unlock();
}
catch( Exception ex )
{
throw new Exception( "Exception trying to unlock" +
" a byte array. Details: " + ex.Message );
}
}
public void DestroyPool()
{
for( int iCnt = 0; iCnt < iCurrentPoolSize; iCnt++ )
{
ByteArray byarInstance = ( ByteArray ) byarArray[ iCnt ];
byarInstance.Unlock();
}
byarArray.Clear();
}
}
An application which implements the above classes can have a source code as below:
public PoolByteArrays poolByteArrays;
...
poolByteArrays = null;
...
...
poolByteArrays = new PoolByteArrays( 100, 2000 );
...
int iAddressOfAByteArray = 0;
int iByteArrayID =
poolByteArrays.GetAddressOfAFreeByteArray( ref iAddressOfAByteArray );
...
string strOutput = "";
unsafe
{
CallUnmanagedFunction(iAddressOfAByteArray);
strOutput = poolByteArrays.GetStringStoredInByteArray( iByteArrayID );
}
...
poolByteArrays.DoneWithByteArray( iByteArrayID );
...
poolByteArrays.DestroyPool();
Points of Interest
In the Microsoft .NET framework, the Garbage Collector (GC) plays an important role in releasing non-reference allocated memory in regular intervals. After the GC frees the required memory, it does a defragmentation and changes the base address of the referred memory location. But there could be some scenario where a base address of a memory location is referred to by a certain object or variable and after defragmentation, the same base address of the assigned memory is required. How can developers keep this address unchanged?
To pin the base address, the developer has to pin the memory at the time of allocation by which GC can know that this memory location is already pinned (using GCHandleType.Pinned
) and GC will not change this memory.
GC = GCHandle.Alloc( byArray, GCHandleType.Pinned );
Another interesting point, the conversion of a byte array to string, can be accomplished by using the ASCIIEncoding
class.
public string GetStringStoredInByteArray( int iID )
{
return( ( new ASCIIEncoding()).GetString( ( ( ByteArray )
byarArray[ iID ] ).GetByteArray() ) ).TrimEnd( new char [] {
Convert.ToChar( ( int ) 0 ) } );
}