Adding comments to MemoryPool classes
This commit is contained in:
parent
ff0affe34d
commit
76b528e1d2
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue