BufferManagement in the Miscellaneous Utility Library

Namespace: MiscUtil.
Types involved: IBufferManager, IBuffer, CachingBufferManager, BufferAcquisitionException, CachedBuffer (internal)

Some programs make frequent use of potentially large (well, large enough to get on the Large Object Heap) byte arrays, particularly when processing streams of data. The buffer management classes in the miscellaneous utility library allow buffers to be reused to reduce pressure on the garbage collector.

Principal Interfaces

The library specifies the IBufferManager and IBuffer interfaces, currently supplying a single implementation for each of them. Each interface is very simple - IBufferManager has a single GetBuffer method returning an IBuffer, and IBuffer has a Bytes property to retrieve the underlying byte array, and implements IDisposable to enable the client to relinquish control of the buffer.

When a client requests a buffer, it specifies the minimum size of buffer. Buffer managers may return buffers larger than this, where appropriate. It is vital that clients call Dispose on a buffer when they have finished with it, so that the buffer manager may take control of the buffer, reuse it if appropriate etc. Clients must not use references to either the buffer or the underlying byte array after disposing of the buffer.

CachingBufferManager and CachingBufferManager.Options

The single IBufferManager implementation provided within the MiscUtil library is CachingBufferManager. This maintains a cache of buffers of increasing sizes, so that when a request is made, an existing buffer is reused if it is available. These buffers are effectively in "size bands", with the smallest size band of a large enough size being used - so with size bands of 1024, 2048 and 4096 bytes, for instance, a request for 1500 bytes would normally result in a 2048 byte buffer being returned. Each band has a maximum number of buffers associated with it. The buffers are lazily created as necessary - if only one buffer of a particular size is ever required at a time, only one buffer of that size will be created.

An overload of the constructor takes a CachingBufferManager.Options parameter to configure the exact behaviour. The CachingBufferManager.Options type has a number of properties, defined below along with the default behaviour. If the parameterless constructor for CachingBufferManager is used, the defaults are all used for the manager.

ActionOnBufferUnavailable
Defines what the manager should do if there are no buffers of the appropriate size available. The options are defined in the CachingBufferManager.Options.BufferUnavailableAction enum, with the following values:
ClearAfterUser
Determines whether the array is cleared (all elements set to 0) when a buffer is disposed. By default this is true, but if buffers are not going to be used for security-sensitive information, and so long as all clients are aware that they shouldn't expect a "clean" buffer, not clearing the array can improve performance slightly.
MaxBufferSize
The maximum size of buffer to create (in bytes). If a request exceeds this limit, a BufferAcquisitionException is thrown. The default is int.MaxValue, which is essentially "no limit" (as the requested buffer size is an int anyway).
MaxBuffersPerSizeBand
The maximum number of buffers to use for each size band before using the ActionOnBufferUnavailable action to determine the next step if all buffers in the requested size band are in use. Defaults to 16.
MinBufferSize
The minimum size of buffer to return (in bytes) - essentially, the size of the lowest band. Defaults to 1K.
ScalingFactor
The factor to use when moving from one size band to the next one up. For instance, with the default scaling factor of 2.0 and the default minimum size of 1K, the first size band is filled with 1K buffers, the second size band is filled with 2K buffers, then 4K etc. The scaling factor cannot be less than 1.25 (to preserve efficiency) and should not be set too high, as otherwise large buffers could be used to fill quite small requests - for instance, with a scaling factor of 16 and otherwise default parameters, a request for a 17K buffer would return one with a 256K array backing it. (There is no enforced upper bound for the scaling factor, however.)

Example usage

Here's a short program which demonstrates using a buffer manager to obtain buffers for copying data for streams. Only one buffer is ever created behind the scenes, to copy all the files.

using System.IO;
using MiscUtil;

/// <summary>
/// Simple program to concatenate the files specified on the command
/// line into a file "output.dat"
/// </summary>
class FileConcatenator
{
    const int BufferSize = 8*1024;
    
    static void Main(string[] args)
    {
        CachingBufferManager.Options options = new CachingBufferManager.Options();
        // We know we'll only use what we've copied into the buffer
        options.ClearAfterUse = false;
        
        IBufferManager manager = new CachingBufferManager(options);
        
        using (FileStream output = File.Create("output.dat"))
        {
            foreach (string inputName in args)
            {
                using (FileStream input = File.Open(inputName, FileMode.Open))
                {
                    CopyStream (input, output, manager);
                }
            }
        }
    }
    
    static void CopyStream (Stream input, Stream output, IBufferManager manager)
    {
        using (IBuffer buffer = manager.GetBuffer(BufferSize))
        {
            byte[] data = buffer.Bytes;
            
            int read;
            // Use the actual size of the byte array, even if it's
            // more than we asked for
            while ( (read=input.Read(data, 0, data.Length)) > 0)
            {
                output.Write(data, 0, read);
            }
        }        
    }
}

Back to the main MiscUtil page.