Adding comments to MemoryPool classes

This commit is contained in:
Louis DeJardin 2015-09-18 16:29:29 -07:00
parent ff0affe34d
commit 76b528e1d2
3 changed files with 177 additions and 3 deletions

View File

@ -3,22 +3,70 @@ using System.Collections.Concurrent;
namespace Microsoft.AspNet.Server.Kestrel.Http
{
/// <summary>
/// Used to allocate and distribute re-usable blocks of memory.
/// </summary>
public class MemoryPool2 : IDisposable
{
/// <summary>
/// The gap between blocks' starting address. 4096 is chosen because most operating systems are 4k pages in size and alignment.
/// </summary>
private const int blockStride = 4096;
/// <summary>
/// The last 64 bytes of a block are unused to prevent CPU from pre-fetching the next 64 byte into it's memory cache.
/// See https://github.com/aspnet/KestrelHttpServer/issues/117 and https://www.youtube.com/watch?v=L7zSU9HI-6I
/// </summary>
private const int blockUnused = 64;
/// <summary>
/// Allocating 32 contiguous blocks per slab makes the slab size 128k. This is larger than the 85k size which will place the memory
/// in the large object heap. This means the GC will not try to relocate this array, so the fact it remains pinned does not negatively
/// affect memory management's compactification.
/// </summary>
private const int blockCount = 32;
/// <summary>
/// 4096 - 64 gives you a blockLength of 4032 usable bytes per block.
/// </summary>
private const int blockLength = blockStride - blockUnused;
/// <summary>
/// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab
/// </summary>
private const int slabLength = blockStride * blockCount;
private ConcurrentStack<MemoryPoolBlock2> _blocks = new ConcurrentStack<MemoryPoolBlock2>();
private ConcurrentStack<MemoryPoolSlab2> _slabs = new ConcurrentStack<MemoryPoolSlab2>();
/// <summary>
/// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects
/// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added.
/// </summary>
private readonly ConcurrentStack<MemoryPoolBlock2> _blocks = new ConcurrentStack<MemoryPoolBlock2>();
/// <summary>
/// Thread-safe collection of slabs which have been allocated by this pool. As long as a slab is in this collection and slab.IsActive,
/// the blocks will be added to _blocks when returned.
/// </summary>
private readonly ConcurrentStack<MemoryPoolSlab2> _slabs = new ConcurrentStack<MemoryPoolSlab2>();
/// <summary>
/// This is part of implementing the IDisposable pattern.
/// </summary>
private bool disposedValue = false; // To detect redundant calls
/// <summary>
/// Called to take a block from the pool.
/// </summary>
/// <param name="minimumSize">The block returned must be at least this size. It may be larger than this minimum size, and if so,
/// the caller may write to the block's entire size rather than being limited to the minumumSize requested.</param>
/// <returns>The block that is reserved for the called. It must be passed to Return when it is no longer being used.</returns>
public MemoryPoolBlock2 Lease(int minimumSize)
{
if (minimumSize > blockLength)
{
// The requested minimumSize is actually larger then the usable memory of a single block.
// Because this is the degenerate case, a one-time-use byte[] array and tracking object are allocated.
// When this block tracking object is returned it is not added to the pool - instead it will be
// allowed to be garbace collected normally.
return MemoryPoolBlock2.Create(
new ArraySegment<byte>(new byte[minimumSize]),
dataPtr: IntPtr.Zero,
@ -31,12 +79,18 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
MemoryPoolBlock2 block;
if (_blocks.TryPop(out block))
{
// block successfully taken from the stack - return it
return block;
}
// no blocks available - grow the pool and try again
AllocateSlab();
}
}
/// <summary>
/// Internal method called when a block is requested and the pool is empty. It allocates one additional slab, creates all of the
/// block tracking objects, and adds them all to the pool.
/// </summary>
private void AllocateSlab()
{
var slab = MemoryPoolSlab2.Create(slabLength);
@ -58,6 +112,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
}
}
/// <summary>
/// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and
/// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the
/// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return
/// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid
/// leaving "dead zones" in the slab due to lost block tracking objects.
/// </summary>
/// <param name="block">The block to return. It must have been acquired by calling Lease on the same memory pool instance.</param>
public void Return(MemoryPoolBlock2 block)
{
block.Reset();

View File

@ -8,33 +8,93 @@ using System.Text;
namespace Microsoft.AspNet.Server.Kestrel.Http
{
/// <summary>
/// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The
/// individual blocks are then treated as independant array segments.
/// </summary>
public class MemoryPoolBlock2
{
private static Vector<byte> _dotIndex = new Vector<byte>(Enumerable.Range(0, Vector<byte>.Count).Select(x => (byte)-x).ToArray());
/// <summary>
/// Array of "minus one" bytes of the length of SIMD operations on the current hardware. Used as an argument in the
/// vector dot product that counts matching character occurence.
/// </summary>
private static Vector<byte> _dotCount = new Vector<byte>(Byte.MaxValue);
/// <summary>
/// Array of negative numbers starting at 0 and continuing for the length of SIMD operations on the current hardware.
/// Used as an argument in the vector dot product that determines matching character index.
/// </summary>
private static Vector<byte> _dotIndex = new Vector<byte>(Enumerable.Range(0, Vector<byte>.Count).Select(x => (byte)-x).ToArray());
/// <summary>
/// If this block represents a one-time-use memory object, this GCHandle will hold that memory object at a fixed address
/// so it can be used in native operations.
/// </summary>
private GCHandle _pinHandle;
/// <summary>
/// Native address of the first byte of this block's Data memory. It is null for one-time-use memory, or copied from
/// the Slab's ArrayPtr for a slab-block segment. The byte it points to corresponds to Data.Array[0], and in practice you will always
/// use the _dataArrayPtr + Start or _dataArrayPtr + End, which point to the start of "active" bytes, or point to just after the "active" bytes.
/// </summary>
private IntPtr _dataArrayPtr;
/// <summary>
/// The array segment describing the range of memory this block is tracking. The caller which has leased this block may only read and
/// modify the memory in this range.
/// </summary>
public ArraySegment<byte> Data;
/// <summary>
/// This object cannot be instantiated outside of the static Create method
/// </summary>
protected MemoryPoolBlock2()
{
}
/// <summary>
/// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool.
/// </summary>
public MemoryPool2 Pool { get; private set; }
/// <summary>
/// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory.
/// </summary>
public MemoryPoolSlab2 Slab { get; private set; }
/// <summary>
/// Convenience accessor
/// </summary>
public byte[] Array => Data.Array;
/// <summary>
/// The Start represents the offset into Array where the range of "active" bytes begins. At the point when the block is leased
/// the Start is guaranteed to be equal to Array.Offset. The value of Start may be assigned anywhere between Data.Offset and
/// Data.Offset + Data.Count, and must be equal to or less than End.
/// </summary>
public int Start { get; set; }
/// <summary>
/// The End represents the offset into Array where the range of "active" bytes ends. At the point when the block is leased
/// the End is guaranteed to be equal to Array.Offset. The value of Start may be assigned anywhere between Data.Offset and
/// Data.Offset + Data.Count, and must be equal to or less than End.
/// </summary>
public int End { get; set; }
/// <summary>
/// Reference to the next block of data when the overall "active" bytes spans multiple blocks. At the point when the block is
/// leased Next is guaranteed to be null. Start, End, and Next are used together in order to create a linked-list of discontiguous
/// working memory. The "active" memory is grown when bytes are copied in, End is increased, and Next is assigned. The "active"
/// memory is shrunk when bytes are consumed, Start is increased, and blocks are returned to the pool.
/// </summary>
public MemoryPoolBlock2 Next { get; set; }
~MemoryPoolBlock2()
{
if (_pinHandle.IsAllocated)
{
// if this is a one-time-use block, ensure that the GCHandle does not leak
_pinHandle.Free();
}
@ -50,16 +110,23 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
}
}
/// <summary>
/// Called to ensure that a block is pinned, and return the pointer to native memory just after
/// the range of "active" bytes. This is where arriving data is read into.
/// </summary>
/// <returns></returns>
public IntPtr Pin()
{
Debug.Assert(!_pinHandle.IsAllocated);
if (_dataArrayPtr != IntPtr.Zero)
{
// this is a slab managed block - use the native address of the slab which is always locked
return _dataArrayPtr + End;
}
else
{
// this is one-time-use memory - lock the managed memory until Unpin is called
_pinHandle = GCHandle.Alloc(Data.Array, GCHandleType.Pinned);
return _pinHandle.AddrOfPinnedObject() + End;
}
@ -69,6 +136,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
if (_dataArrayPtr == IntPtr.Zero)
{
// this is one-time-use memory - unlock the managed memory
Debug.Assert(_pinHandle.IsAllocated);
_pinHandle.Free();
}
@ -100,6 +168,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
};
}
/// <summary>
/// called when the block is returned to the pool. mutable values are re-assigned to their guaranteed initialized state.
/// </summary>
public void Reset()
{
Next = null;
@ -107,11 +178,19 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
End = Data.Offset;
}
/// <summary>
/// ToString overridden for debugger convenience. This displays the "active" byte information in this block as ASCII characters.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Encoding.ASCII.GetString(Array, Start, End - Start);
}
/// <summary>
/// acquires a cursor pointing into this block at the Start of "active" byte information
/// </summary>
/// <returns></returns>
public Iterator GetIterator()
{
return new Iterator(this);

View File

@ -5,18 +5,51 @@ using System.Runtime.InteropServices;
namespace Microsoft.AspNet.Server.Kestrel.Http
{
/// <summary>
/// Slab tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The
/// individual blocks are then treated as independant array segments.
/// </summary>
public class MemoryPoolSlab2 : IDisposable
{
/// <summary>
/// This handle pins the managed array in memory until the slab is disposed. This prevents it from being
/// relocated and enables any subsections of the array to be used as native memory pointers to P/Invoked API calls.
/// </summary>
private GCHandle _gcHandle;
/// <summary>
/// The managed memory allocated in the large object heap.
/// </summary>
public byte[] Array;
/// <summary>
/// The native memory pointer of the pinned Array. All block native addresses are pointers into the memory
/// ranging from ArrayPtr to ArrayPtr + Array.Length
/// </summary>
public IntPtr ArrayPtr;
/// <summary>
/// True as long as the blocks from this slab are to be considered returnable to the pool. In order to shrink the
/// memory pool size an entire slab must be removed. That is done by (1) setting IsActive to false and removing the
/// slab from the pool's _slabs collection, (2) as each block currently in use is Return()ed to the pool it will
/// be allowed to be garbage collected rather than re-pooled, and (3) when all block tracking objects are garbage
/// collected and the slab is no longer references the slab will be garbage collected and the memory unpinned will
/// be unpinned by the slab's Dispose.
/// </summary>
public bool IsActive;
/// <summary>
/// Part of the IDisposable implementation
/// </summary>
private bool disposedValue = false; // To detect redundant calls
public static MemoryPoolSlab2 Create(int length)
{
// allocate and pin requested memory length
var array = new byte[length];
var gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
// allocate and return slab tracking object
return new MemoryPoolSlab2
{
Array = array,