diff --git a/src/System.Memory/ref/System.Memory.cs b/src/System.Memory/ref/System.Memory.cs index 07cf63f2e91a..62e7925ec5bb 100644 --- a/src/System.Memory/ref/System.Memory.cs +++ b/src/System.Memory/ref/System.Memory.cs @@ -209,6 +209,15 @@ public partial struct MemoryHandle : System.IDisposable public unsafe void* Pointer { get { throw null; } } public void Dispose() { } } + public abstract class MemoryPool : IDisposable + { + public static System.Buffers.MemoryPool Shared { get; } + public abstract System.Buffers.OwnedMemory Rent(int minBufferSize=-1); + public abstract int MaxBufferSize { get; } + protected MemoryPool() { throw null; } + public void Dispose() { throw null; } + protected abstract void Dispose(bool disposing); + } public enum OperationStatus { DestinationTooSmall = 1, diff --git a/src/System.Memory/src/System.Memory.csproj b/src/System.Memory/src/System.Memory.csproj index 666db9520544..c8cbdde51c6d 100644 --- a/src/System.Memory/src/System.Memory.csproj +++ b/src/System.Memory/src/System.Memory.csproj @@ -26,6 +26,9 @@ + + + @@ -123,6 +126,7 @@ + @@ -143,4 +147,4 @@ - \ No newline at end of file + diff --git a/src/System.Memory/src/System/Buffers/ArrayMemoryPool.ArrayMemoryPoolBuffer.cs b/src/System.Memory/src/System/Buffers/ArrayMemoryPool.ArrayMemoryPoolBuffer.cs new file mode 100644 index 000000000000..08015646e3f7 --- /dev/null +++ b/src/System.Memory/src/System/Buffers/ArrayMemoryPool.ArrayMemoryPoolBuffer.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +#if !netstandard +using Internal.Runtime.CompilerServices; +#else +using System.Runtime.CompilerServices; +#endif + +namespace System.Buffers +{ + internal sealed partial class ArrayMemoryPool : MemoryPool + { + private sealed class ArrayMemoryPoolBuffer : OwnedMemory + { + private T[] _array; + private int _refCount; + + public ArrayMemoryPoolBuffer(int size) + { + _array = ArrayPool.Shared.Rent(size); + } + + public sealed override int Length => _array.Length; + + public sealed override bool IsDisposed => _array == null; + + protected sealed override bool IsRetained => _refCount > 0; + + public sealed override Span Span + { + get + { + if (IsDisposed) + ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); + + return _array; + } + } + + protected sealed override void Dispose(bool disposing) + { + if (_array != null) + { + ArrayPool.Shared.Return(_array); + _array = null; + } + } + + protected +#if netstandard // TryGetArray is exposed as "protected internal". Normally, the rules of C# dictate we override it as "protected" because the base class is + // in a different assembly. Except in the netstandard config where the base class is in the same assembly. + internal +#endif + sealed override bool TryGetArray(out ArraySegment arraySegment) + { + if (IsDisposed) + ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); + + arraySegment = new ArraySegment(_array); + return true; + } + + public sealed override MemoryHandle Pin(int byteOffset = 0) + { + unsafe + { + Retain(); // this checks IsDisposed + + if (byteOffset != 0 && (((uint)byteOffset) - 1) / Unsafe.SizeOf() >= _array.Length) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.byteOffset); + + GCHandle handle = GCHandle.Alloc(_array, GCHandleType.Pinned); + return new MemoryHandle(this, ((byte*)handle.AddrOfPinnedObject()) + byteOffset, handle); + } + } + + public sealed override void Retain() + { + if (IsDisposed) + ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); + + _refCount++; + } + + public sealed override bool Release() + { + if (IsDisposed) + ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); + + int newRefCount = --_refCount; + if (newRefCount < 0) + ThrowHelper.ThrowInvalidOperationException(); + + return newRefCount != 0; + } + } + } +} diff --git a/src/System.Memory/src/System/Buffers/ArrayMemoryPool.cs b/src/System.Memory/src/System/Buffers/ArrayMemoryPool.cs new file mode 100644 index 000000000000..5b5472951eed --- /dev/null +++ b/src/System.Memory/src/System/Buffers/ArrayMemoryPool.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !netstandard +using Internal.Runtime.CompilerServices; +#else +using System.Runtime.CompilerServices; +#endif + +namespace System.Buffers +{ + internal sealed partial class ArrayMemoryPool : MemoryPool + { + private const int s_maxBufferSize = int.MaxValue; + public sealed override int MaxBufferSize => s_maxBufferSize; + + public sealed override OwnedMemory Rent(int minimumBufferSize = -1) + { + if (minimumBufferSize == -1) + minimumBufferSize = 1 + (4095 / Unsafe.SizeOf()); + else if (((uint)minimumBufferSize) > s_maxBufferSize) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); + + return new ArrayMemoryPoolBuffer(minimumBufferSize); + } + + protected sealed override void Dispose(bool disposing) {} // ArrayMemoryPool is a shared pool so Dispose() would be a nop even if there were native resources to dispose. + } +} diff --git a/src/System.Memory/src/System/Buffers/MemoryPool.cs b/src/System.Memory/src/System/Buffers/MemoryPool.cs new file mode 100644 index 000000000000..650b2ba4ac7c --- /dev/null +++ b/src/System.Memory/src/System/Buffers/MemoryPool.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers +{ + /// + /// Represents a pool of memory blocks. + /// + public abstract class MemoryPool : IDisposable + { + private static readonly MemoryPool s_shared = new ArrayMemoryPool(); + + /// + /// Returns a singleton instance of a MemoryPool based on arrays. + /// + public static MemoryPool Shared => s_shared; + + /// + /// Returns a memory block capable of holding at least elements of T. + /// + /// If -1 is passed, this is set to a default value for the pool. + public abstract OwnedMemory Rent(int minBufferSize = -1); + + /// + /// Returns the maximum buffer size supported by this pool. + /// + public abstract int MaxBufferSize { get; } + + /// + /// Constructs a new instance of a memory pool. + /// + protected MemoryPool() {} + + /// + /// Frees all resources used by the memory pool. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Frees all resources used by the memory pool. + /// + /// + protected abstract void Dispose(bool disposing); + } +} diff --git a/src/System.Memory/src/System/ThrowHelper.cs b/src/System.Memory/src/System/ThrowHelper.cs index 7618804fa6a6..459b436f5b17 100644 --- a/src/System.Memory/src/System/ThrowHelper.cs +++ b/src/System.Memory/src/System/ThrowHelper.cs @@ -60,10 +60,18 @@ internal static class ThrowHelper [MethodImpl(MethodImplOptions.NoInlining)] private static Exception CreateArgumentOutOfRangeException_SymbolDoesNotFit() { return new ArgumentOutOfRangeException("symbol", SR.Argument_BadFormatSpecifier); } + internal static void ThrowInvalidOperationException() { throw CreateInvalidOperationException(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static Exception CreateInvalidOperationException() { return new InvalidOperationException(); } + internal static void ThrowInvalidOperationException_OutstandingReferences() { throw CreateInvalidOperationException_OutstandingReferences(); } [MethodImpl(MethodImplOptions.NoInlining)] private static Exception CreateInvalidOperationException_OutstandingReferences() { return new InvalidOperationException(SR.OutstandingReferences); } + internal static void ThrowObjectDisposedException_ArrayMemoryPoolBuffer() { throw CreateObjectDisposedException_ArrayMemoryPoolBuffer(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private static Exception CreateObjectDisposedException_ArrayMemoryPoolBuffer() { return new ObjectDisposedException("ArrayMemoryPoolBuffer"); } + internal static void ThrowObjectDisposedException_MemoryDisposed() { throw CreateObjectDisposedException_MemoryDisposed(); } [MethodImpl(MethodImplOptions.NoInlining)] private static Exception CreateObjectDisposedException_MemoryDisposed() { return new ObjectDisposedException("OwnedMemory", SR.MemoryDisposed); } @@ -106,6 +114,8 @@ internal enum ExceptionArgument text, obj, ownedMemory, + minimumBufferSize, + byteOffset, pointer, comparable, comparer diff --git a/src/System.Memory/tests/MemoryPool/MemoryPool.cs b/src/System.Memory/tests/MemoryPool/MemoryPool.cs new file mode 100644 index 000000000000..5ef2e5a7d04e --- /dev/null +++ b/src/System.Memory/tests/MemoryPool/MemoryPool.cs @@ -0,0 +1,314 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.MemoryTests +{ + public static partial class MemoryPoolTests + { + [Fact] + public static void ThereIsOnlyOneSharedPool() + { + MemoryPool mp1 = MemoryPool.Shared; + MemoryPool mp2 = MemoryPool.Shared; + Assert.Same(mp1, mp2); + } + + [Fact] + public static void DisposingTheSharedPoolIsANop() + { + MemoryPool mp = MemoryPool.Shared; + mp.Dispose(); + mp.Dispose(); + using (OwnedMemory block = mp.Rent(10)) + { + Assert.True(block.Length >= 10); + } + } + + [Fact] + public static void RentWithTooLargeASize() + { + MemoryPool pool = MemoryPool.Shared; + Assert.Throws(() => pool.Rent(pool.MaxBufferSize + 1)); + } + + [Fact] + public static void MemoryPoolSpan() + { + MemoryPool pool = MemoryPool.Shared; + using (OwnedMemory block = pool.Rent(10)) + { + Span sp = block.Span; + Assert.Equal(block.Length, sp.Length); + using (MemoryHandle newMemoryHandle = block.Pin()) + { + unsafe + { + void* pSpan = Unsafe.AsPointer(ref MemoryMarshal.GetReference(sp)); + Assert.Equal((IntPtr)newMemoryHandle.Pointer, (IntPtr)pSpan); + } + } + } + } + + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(10 * sizeof(int))] + public static void MemoryPoolPin(int byteOffset) + { + MemoryPool pool = MemoryPool.Shared; + using (OwnedMemory block = pool.Rent(10)) + { + Span sp = block.Span; + Assert.Equal(block.Length, sp.Length); + using (MemoryHandle newMemoryHandle = block.Pin(byteOffset: byteOffset)) + { + unsafe + { + void* pSpan = Unsafe.AsPointer(ref MemoryMarshal.GetReference(sp)); + Assert.Equal((IntPtr)pSpan, ((IntPtr)newMemoryHandle.Pointer) - byteOffset); + } + } + } + } + + [Theory] + [InlineData(-1)] + [InlineData(int.MinValue)] + public static void MemoryPoolPinBadOffset(int byteOffset) + { + MemoryPool pool = MemoryPool.Shared; + OwnedMemory block = pool.Rent(10); + Span sp = block.Span; + Assert.Equal(block.Length, sp.Length); + Assert.Throws(() => block.Pin(byteOffset: byteOffset)); + } + + [Fact] + public static void MemoryPoolPinOffsetAtEnd() + { + MemoryPool pool = MemoryPool.Shared; + OwnedMemory block = pool.Rent(10); + Span sp = block.Span; + Assert.Equal(block.Length, sp.Length); + + int byteOffset = 0; + try + { + byteOffset = checked(block.Length * sizeof(int)); + } + catch (OverflowException) + { + return; // The pool gave us a very large block - too big to compute the byteOffset needed to carry out this test. Skip. + } + + using (MemoryHandle newMemoryHandle = block.Pin(byteOffset: byteOffset)) + { + unsafe + { + void* pSpan = Unsafe.AsPointer(ref MemoryMarshal.GetReference(sp)); + Assert.Equal((IntPtr)pSpan, ((IntPtr)newMemoryHandle.Pointer) - byteOffset); + } + } + } + + [Fact] + public static void MemoryPoolPinBadOffsetTooLarge() + { + MemoryPool pool = MemoryPool.Shared; + OwnedMemory block = pool.Rent(10); + Span sp = block.Span; + Assert.Equal(block.Length, sp.Length); + + int byteOffset = 0; + try + { + byteOffset = checked(block.Length * sizeof(int) + 1); + } + catch (OverflowException) + { + return; // The pool gave us a very large block - too big to compute the byteOffset needed to carry out this test. Skip. + } + + Assert.Throws(() => block.Pin(byteOffset: byteOffset)); + } + + [Fact] + public static void EachRentalIsUniqueUntilDisposed() + { + MemoryPool pool = MemoryPool.Shared; + List> priorBlocks = new List>(); + + Random r = new Random(42); + List testInputs = new List(); + for (int i = 0; i < 100; i++) + { + testInputs.Add((Math.Abs(r.Next() % 1000)) + 1); + } + + foreach (int minBufferSize in testInputs) + { + OwnedMemory newBlock = pool.Rent(minBufferSize); + Assert.True(newBlock.Length >= minBufferSize); + + foreach (OwnedMemory prior in priorBlocks) + { + using (MemoryHandle priorMemoryHandle = prior.Pin()) + { + using (MemoryHandle newMemoryHandle = newBlock.Pin()) + { + unsafe + { + Assert.NotEqual((IntPtr)priorMemoryHandle.Pointer, (IntPtr)newMemoryHandle.Pointer); + } + } + } + } + priorBlocks.Add(newBlock); + } + + foreach (OwnedMemory prior in priorBlocks) + { + prior.Dispose(); + } + } + + [Fact] + public static void RentWithDefaultSize() + { + using (OwnedMemory block = MemoryPool.Shared.Rent(minBufferSize: -1)) + { + Assert.True(block.Length >= 1); + } + } + + [Theory] + [MemberData(nameof(BadSizes))] + public static void RentBadSizes(int badSize) + { + MemoryPool pool = MemoryPool.Shared; + Assert.Throws(() => pool.Rent(minBufferSize: badSize)); + } + + public static IEnumerable BadSizes + { + get + { + yield return new object[] { -2 }; + yield return new object[] { int.MinValue }; + } + } + + [Fact] + public static void MemoryPoolTryGetArray() + { + using (OwnedMemory block = MemoryPool.Shared.Rent(42)) + { + Memory memory = block.Memory; + bool success = memory.TryGetArray(out ArraySegment arraySegment); + Assert.True(success); + Assert.Equal(block.Length, arraySegment.Count); + unsafe + { + void* pSpan = Unsafe.AsPointer(ref MemoryMarshal.GetReference(block.Span)); + fixed (int* pArray = arraySegment.Array) + { + Assert.Equal((IntPtr)pSpan, (IntPtr)pArray); + } + } + } + } + + [Fact] + public static void RefCounting() + { + using (OwnedMemory block = MemoryPool.Shared.Rent(42)) + { + block.Retain(); + block.Retain(); + block.Retain(); + + bool moreToGo; + moreToGo = block.Release(); + Assert.True(moreToGo); + + moreToGo = block.Release(); + Assert.True(moreToGo); + + moreToGo = block.Release(); + Assert.False(moreToGo); + + Assert.Throws(() => block.Release()); + } + } + + [Fact] + public static void IsDisposed() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + Assert.False(block.IsDisposed); + block.Dispose(); + Assert.True(block.IsDisposed); + block.Dispose(); + Assert.True(block.IsDisposed); + } + + [Fact] + public static void ExtraDisposesAreIgnored() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + block.Dispose(); + block.Dispose(); + } + + [Fact] + public static void NoSpanAfterDispose() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + block.Dispose(); + Assert.Throws(() => block.Span.DontBox()); + } + + [Fact] + public static void NoRetainAfterDispose() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + block.Dispose(); + Assert.Throws(() => block.Retain()); + } + + [Fact] + public static void NoRelease_AfterDispose() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + block.Dispose(); + Assert.Throws(() => block.Release()); + } + + [Fact] + public static void NoPinAfterDispose() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + block.Dispose(); + Assert.Throws(() => block.Pin()); + } + + [Fact] + public static void NoTryGetArrayAfterDispose() + { + OwnedMemory block = MemoryPool.Shared.Rent(42); + Memory memory = block.Memory; + block.Dispose(); + Assert.Throws(() => memory.TryGetArray(out ArraySegment arraySegment)); + } + } +} + diff --git a/src/System.Memory/tests/System.Memory.Tests.csproj b/src/System.Memory/tests/System.Memory.Tests.csproj index df4a91e18393..6e48a5c10e3c 100644 --- a/src/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/System.Memory/tests/System.Memory.Tests.csproj @@ -126,6 +126,9 @@ + + + @@ -183,4 +186,4 @@ - \ No newline at end of file +