// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; namespace System.Buffers { /// /// Used to allocate and distribute re-usable blocks of memory. /// internal class SlabMemoryPool : MemoryPool { /// /// The size of a block. 4096 is chosen because most operating systems use 4k pages. /// private const int _blockSize = 4096; /// /// 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. /// private const int _blockCount = 32; /// /// Max allocation block size for pooled blocks, /// larger values can be leased but they will be disposed after use rather than returned to the pool. /// public override int MaxBufferSize { get; } = _blockSize; /// /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab /// private static readonly int _slabLength = _blockSize * _blockCount; /// /// 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. /// private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); /// /// 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. /// private readonly ConcurrentStack _slabs = new ConcurrentStack(); /// /// This is part of implementing the IDisposable pattern. /// private bool _isDisposed; // To detect redundant calls private int _totalAllocatedBlocks; private readonly object _disposeSync = new object(); /// /// This default value passed in to Rent to use the default value for the pool. /// private const int AnySize = -1; public override IMemoryOwner Rent(int size = AnySize) { if (size > _blockSize) { MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockSize); } var block = Lease(); return block; } /// /// Called to take a block from the pool. /// /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. private MemoryPoolBlock Lease() { if (_isDisposed) { MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); } if (_blocks.TryDequeue(out MemoryPoolBlock block)) { // block successfully taken from the stack - return it block.Lease(); return block; } // no blocks available - grow the pool block = AllocateSlab(); block.Lease(); return block; } /// /// 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. /// private MemoryPoolBlock AllocateSlab() { var slab = MemoryPoolSlab.Create(_slabLength); _slabs.Push(slab); var basePtr = slab.NativePointer; // Page align the blocks var offset = (int)((((ulong)basePtr + (uint)_blockSize - 1) & ~((uint)_blockSize - 1)) - (ulong)basePtr); // Ensure page aligned Debug.Assert(((ulong)basePtr + (uint)offset) % _blockSize == 0); var blockCount = (_slabLength - offset) / _blockSize; Interlocked.Add(ref _totalAllocatedBlocks, blockCount); MemoryPoolBlock block = null; for (int i = 0; i < blockCount; i++) { block = new MemoryPoolBlock(this, slab, offset, _blockSize); if (i != blockCount - 1) // last block { #if BLOCK_LEASE_TRACKING block.IsLeased = true; #endif Return(block); } offset += _blockSize; } return block; } /// /// 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. /// /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. internal void Return(MemoryPoolBlock block) { #if BLOCK_LEASE_TRACKING Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); block.IsLeased = false; #endif if (!_isDisposed) { _blocks.Enqueue(block); } else { GC.SuppressFinalize(block); } } protected override void Dispose(bool disposing) { if (_isDisposed) { return; } lock (_disposeSync) { _isDisposed = true; if (disposing) { while (_slabs.TryPop(out MemoryPoolSlab slab)) { // dispose managed state (managed objects). slab.Dispose(); } } // Discard blocks in pool while (_blocks.TryDequeue(out MemoryPoolBlock block)) { GC.SuppressFinalize(block); } } } } }