diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs new file mode 100644 index 00000000000..7a09d1f9007 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -0,0 +1,134 @@ +// 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; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; +using Microsoft.Toolkit.HighPerformance.Extensions; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals +{ + /// + /// A custom that casts data from a array, to values. + /// + /// The source type of items to read. + /// The target type to cast the source items to. + internal sealed class ArrayMemoryManager : MemoryManager, IMemoryManager + where TFrom : unmanaged + where TTo : unmanaged + { + /// + /// The source array to read data from. + /// + private readonly TFrom[] array; + + /// + /// The starting offset within . + /// + private readonly int offset; + + /// + /// The original used length for . + /// + private readonly int length; + + /// + /// Initializes a new instance of the class. + /// + /// The source array to read data from. + /// The starting offset within . + /// The original used length for . + public ArrayMemoryManager(TFrom[] array, int offset, int length) + { + this.array = array; + this.offset = offset; + this.length = length; + } + + /// + public override Span GetSpan() + { +#if SPAN_RUNTIME_SUPPORT + ref TFrom r0 = ref this.array.DangerousGetReferenceAt(this.offset); + ref TTo r1 = ref Unsafe.As(ref r0); + int length = RuntimeHelpers.ConvertLength(this.length); + + return MemoryMarshal.CreateSpan(ref r1, length); +#else + Span span = this.array.AsSpan(this.offset, this.length); + + // We rely on MemoryMarshal.Cast here to deal with calculating the effective + // size of the new span to return. This will also make the behavior consistent + // for users that are both using this type as well as casting spans directly. + return MemoryMarshal.Cast(span); +#endif + } + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf())) + { + ThrowArgumentOutOfRangeExceptionForInvalidIndex(); + } + + int + bytePrefix = this.offset * Unsafe.SizeOf(), + byteSuffix = elementIndex * Unsafe.SizeOf(), + byteOffset = bytePrefix + byteSuffix; + + GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned); + + ref TFrom r0 = ref this.array.DangerousGetReference(); + ref byte r1 = ref Unsafe.As(ref r0); + ref byte r2 = ref Unsafe.Add(ref r1, byteOffset); + void* pi = Unsafe.AsPointer(ref r2); + + return new MemoryHandle(pi, handle); + } + + /// + public override void Unpin() + { + } + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public Memory GetMemory(int offset, int length) + where T : unmanaged + { + // We need to calculate the right offset and length of the new Memory. The local offset + // is the original offset into the wrapped TFrom[] array, while the input offset is the one + // with respect to TTo items in the Memory instance that is currently being cast. + int + absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset), + absoluteLength = RuntimeHelpers.ConvertLength(length); + + // We have a special handling in cases where the user is circling back to the original type + // of the wrapped array. In this case we can just return a memory wrapping that array directly, + // with offset and length being adjusted, without the memory manager indirection. + if (typeof(T) == typeof(TFrom)) + { + return (Memory)(object)this.array.AsMemory(absoluteOffset, absoluteLength); + } + + return new ArrayMemoryManager(this.array, absoluteOffset, absoluteLength).Memory; + } + + /// + /// Throws an when the target index for is invalid. + /// + private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex() + { + throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs new file mode 100644 index 00000000000..20a56c54b2b --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs @@ -0,0 +1,25 @@ +// 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; +using System.Buffers; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces +{ + /// + /// An interface for a instance that can reinterpret its underlying data. + /// + internal interface IMemoryManager + { + /// + /// Creates a new that reinterprets the underlying data for the current instance. + /// + /// The target type to cast the items to. + /// The starting offset within the data store. + /// The original used length for the data store. + /// A new instance of the specified type, reinterpreting the current items. + Memory GetMemory(int offset, int length) + where T : unmanaged; + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs new file mode 100644 index 00000000000..688450d6bd1 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -0,0 +1,134 @@ +// 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; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals +{ + /// + /// A custom that casts data from a of , to values. + /// + /// The source type of items to read. + /// The target type to cast the source items to. + internal sealed class ProxyMemoryManager : MemoryManager, IMemoryManager + where TFrom : unmanaged + where TTo : unmanaged + { + /// + /// The source to read data from. + /// + private readonly MemoryManager memoryManager; + + /// + /// The starting offset within . + /// + private readonly int offset; + + /// + /// The original used length for . + /// + private readonly int length; + + /// + /// Initializes a new instance of the class. + /// + /// The source to read data from. + /// The starting offset within . + /// The original used length for . + public ProxyMemoryManager(MemoryManager memoryManager, int offset, int length) + { + this.memoryManager = memoryManager; + this.offset = offset; + this.length = length; + } + + /// + public override Span GetSpan() + { + Span span = this.memoryManager.GetSpan().Slice(this.offset, this.length); + + return MemoryMarshal.Cast(span); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf())) + { + ThrowArgumentExceptionForInvalidIndex(); + } + + int + bytePrefix = this.offset * Unsafe.SizeOf(), + byteSuffix = elementIndex * Unsafe.SizeOf(), + byteOffset = bytePrefix + byteSuffix; + +#if NETSTANDARD1_4 + int + shiftedOffset = byteOffset / Unsafe.SizeOf(), + remainder = byteOffset - (shiftedOffset * Unsafe.SizeOf()); +#else + int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf(), out int remainder); +#endif + + if (remainder != 0) + { + ThrowArgumentExceptionForInvalidAlignment(); + } + + return this.memoryManager.Pin(shiftedOffset); + } + + /// + public override void Unpin() + { + this.memoryManager.Unpin(); + } + + /// + protected override void Dispose(bool disposing) + { + ((IDisposable)this.memoryManager).Dispose(); + } + + /// + public Memory GetMemory(int offset, int length) + where T : unmanaged + { + // Like in the other memory manager, calculate the absolute offset and length + int + absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset), + absoluteLength = RuntimeHelpers.ConvertLength(length); + + // Skip one indirection level and slice the original memory manager, if possible + if (typeof(T) == typeof(TFrom)) + { + return (Memory)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength); + } + + return new ProxyMemoryManager(this.memoryManager, absoluteOffset, absoluteLength).Memory; + } + + /// + /// Throws an when the target index for is invalid. + /// + private static void ThrowArgumentExceptionForInvalidIndex() + { + throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range"); + } + + /// + /// Throws an when receives an invalid target index. + /// + private static void ThrowArgumentExceptionForInvalidAlignment() + { + throw new ArgumentOutOfRangeException("elementIndex", "The input index doesn't result in an aligned item access"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs new file mode 100644 index 00000000000..3a16c0b4794 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs @@ -0,0 +1,126 @@ +// 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; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; +using Microsoft.Toolkit.HighPerformance.Extensions; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals +{ + /// + /// A custom that casts data from a to values. + /// + /// The target type to cast the source characters to. + internal sealed class StringMemoryManager : MemoryManager, IMemoryManager + where TTo : unmanaged + { + /// + /// The source to read data from. + /// + private readonly string text; + + /// + /// The starting offset within . + /// + private readonly int offset; + + /// + /// The original used length for . + /// + private readonly int length; + + /// + /// Initializes a new instance of the class. + /// + /// The source to read data from. + /// The starting offset within . + /// The original used length for . + public StringMemoryManager(string text, int offset, int length) + { + this.text = text; + this.offset = offset; + this.length = length; + } + + /// + public override Span GetSpan() + { +#if SPAN_RUNTIME_SUPPORT + ref char r0 = ref this.text.DangerousGetReferenceAt(this.offset); + ref TTo r1 = ref Unsafe.As(ref r0); + int length = RuntimeHelpers.ConvertLength(this.length); + + return MemoryMarshal.CreateSpan(ref r1, length); +#else + ReadOnlyMemory memory = this.text.AsMemory(this.offset, this.length); + Span span = MemoryMarshal.AsMemory(memory).Span; + + return MemoryMarshal.Cast(span); +#endif + } + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf())) + { + ThrowArgumentOutOfRangeExceptionForInvalidIndex(); + } + + int + bytePrefix = this.offset * Unsafe.SizeOf(), + byteSuffix = elementIndex * Unsafe.SizeOf(), + byteOffset = bytePrefix + byteSuffix; + + GCHandle handle = GCHandle.Alloc(this.text, GCHandleType.Pinned); + + ref char r0 = ref this.text.DangerousGetReference(); + ref byte r1 = ref Unsafe.As(ref r0); + ref byte r2 = ref Unsafe.Add(ref r1, byteOffset); + void* pi = Unsafe.AsPointer(ref r2); + + return new MemoryHandle(pi, handle); + } + + /// + public override void Unpin() + { + } + + /// + protected override void Dispose(bool disposing) + { + } + + /// + public Memory GetMemory(int offset, int length) + where T : unmanaged + { + int + absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset), + absoluteLength = RuntimeHelpers.ConvertLength(length); + + if (typeof(T) == typeof(char)) + { + ReadOnlyMemory memory = this.text.AsMemory(absoluteOffset, absoluteLength); + + return (Memory)(object)MemoryMarshal.AsMemory(memory); + } + + return new StringMemoryManager(this.text, absoluteOffset, absoluteLength).Memory; + } + + /// + /// Throws an when the target index for is invalid. + /// + private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex() + { + throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index e2b9d434cad..a175a02b577 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -6,6 +6,7 @@ using System.Diagnostics.Contracts; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; #if SPAN_RUNTIME_SUPPORT using Microsoft.Toolkit.HighPerformance.Memory; #endif @@ -64,6 +65,41 @@ public static Memory2D AsMemory2D(this Memory memory, int offset, int h } #endif + /// + /// Casts a of one primitive type to of bytes. + /// + /// The type if items in the source . + /// The source , of type . + /// A of bytes. + /// + /// Thrown if the property of the new would exceed . + /// + /// Thrown when the data store of is not supported. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory AsBytes(this Memory memory) + where T : unmanaged + { + return MemoryMarshal.AsMemory(((ReadOnlyMemory)memory).Cast()); + } + + /// + /// Casts a of one primitive type to another primitive type . + /// + /// The type of items in the source . + /// The type of items in the destination . + /// The source , of type . + /// A of type + /// Thrown when the data store of is not supported. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory Cast(this Memory memory) + where TFrom : unmanaged + where TTo : unmanaged + { + return MemoryMarshal.AsMemory(((ReadOnlyMemory)memory).Cast()); + } + /// /// Returns a wrapping the contents of the given of instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 0c1c6eb821f..d9d996717b0 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -3,9 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Diagnostics.Contracts; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; #if SPAN_RUNTIME_SUPPORT using Microsoft.Toolkit.HighPerformance.Memory; #endif @@ -64,6 +68,77 @@ public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, i } #endif + /// + /// Casts a of one primitive type to of bytes. + /// + /// The type if items in the source . + /// The source , of type . + /// A of bytes. + /// + /// Thrown if the property of the new would exceed . + /// + /// Thrown when the data store of is not supported. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory AsBytes(this ReadOnlyMemory memory) + where T : unmanaged + { + return Cast(memory); + } + + /// + /// Casts a of one primitive type to another primitive type . + /// + /// The type of items in the source . + /// The type of items in the destination . + /// The source , of type . + /// A of type + /// Thrown when the data store of is not supported. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory Cast(this ReadOnlyMemory memory) + where TFrom : unmanaged + where TTo : unmanaged + { + if (memory.IsEmpty) + { + return default; + } + + if (typeof(TFrom) == typeof(char) && + MemoryMarshal.TryGetString((ReadOnlyMemory)(object)memory, out string? text, out int start, out int length)) + { + return new StringMemoryManager(text!, start, length).Memory; + } + + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + return new ArrayMemoryManager(segment.Array!, segment.Offset, segment.Count).Memory; + } + + if (MemoryMarshal.TryGetMemoryManager>(memory, out var memoryManager, out start, out length)) + { + // If the memory manager is the one resulting from a previous cast, we can use it directly to retrieve + // a new manager for the target type that wraps the original data store, instead of creating one that + // wraps the current manager. This ensures that doing repeated casts always results in only up to one + // indirection level in the chain of memory managers needed to access the target data buffer to use. + if (memoryManager is IMemoryManager wrappingManager) + { + return wrappingManager.GetMemory(start, length); + } + + return new ProxyMemoryManager(memoryManager, start, length).Memory; + } + + // Throws when the memory instance has an unsupported backing store + static ReadOnlyMemory ThrowArgumentExceptionForUnsupportedMemory() + { + throw new ArgumentException("The input instance doesn't have a supported underlying data store."); + } + + return ThrowArgumentExceptionForUnsupportedMemory(); + } + /// /// Returns a wrapping the contents of the given of instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index b4264395133..e0aef51a3d4 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -259,14 +259,10 @@ public static int Count(this ReadOnlySpan span, T value) /// /// Casts a of one primitive type to of bytes. - /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety. /// /// The type if items in the source . /// The source slice, of type . /// A of bytes. - /// - /// Thrown when contains pointers. - /// /// /// Thrown if the property of the new would exceed . /// @@ -280,7 +276,6 @@ public static ReadOnlySpan AsBytes(this ReadOnlySpan span) /// /// Casts a of one primitive type to another primitive type . - /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety. /// /// The type of items in the source . /// The type of items in the destination . @@ -289,14 +284,11 @@ public static ReadOnlySpan AsBytes(this ReadOnlySpan span) /// /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. /// - /// - /// Thrown when or contains pointers. - /// [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan Cast(this ReadOnlySpan span) - where TFrom : struct - where TTo : struct + where TFrom : unmanaged + where TTo : unmanaged { return MemoryMarshal.Cast(span); } diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs index eecf0519360..d70c743f56b 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -117,14 +117,10 @@ public static Span2D AsSpan2D(this Span span, int offset, int height, i /// /// Casts a of one primitive type to of bytes. - /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety. /// /// The type if items in the source . /// The source slice, of type . /// A of bytes. - /// - /// Thrown when contains pointers. - /// /// /// Thrown if the property of the new would exceed . /// @@ -138,7 +134,6 @@ public static Span AsBytes(this Span span) /// /// Casts a of one primitive type to another primitive type . - /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety. /// /// The type of items in the source . /// The type of items in the destination . @@ -147,14 +142,11 @@ public static Span AsBytes(this Span span) /// /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. /// - /// - /// Thrown when or contains pointers. - /// [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span Cast(this Span span) - where TFrom : struct - where TTo : struct + where TFrom : unmanaged + where TTo : unmanaged { return MemoryMarshal.Cast(span); } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs index d14bc211193..5a22ef8aff8 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -18,10 +18,40 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals { /// - /// A helper class that act as polyfill for .NET Standard 2.0 and below. + /// A helper class that with utility methods for dealing with references, and other low-level details. + /// It also contains some APIs that act as polyfills for .NET Standard 2.0 and below. /// internal static class RuntimeHelpers { + /// + /// Converts a length of items from one size to another (rounding towards zero). + /// + /// The source type of items. + /// The target type of items. + /// The input length to convert. + /// The converted length for the specified argument and types. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int ConvertLength(int length) + where TFrom : unmanaged + where TTo : unmanaged + { + if (sizeof(TFrom) == sizeof(TTo)) + { + return length; + } + else if (sizeof(TFrom) == 1) + { + return length / sizeof(TTo); + } + else + { + ulong targetLength = (ulong)(uint)length * (uint)sizeof(TFrom) / (uint)sizeof(TTo); + + return checked((int)targetLength); + } + } + /// /// Gets the length of a given array as a native integer. /// diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 705a335735d..edbf5d0e961 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,12 +15,565 @@ namespace UnitTests.HighPerformance.Extensions [TestClass] public class Test_MemoryExtensions { + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_Cast_Empty() + { + // Casting an empty memory of any size should always be valid + // and result in another empty memory, regardless of types. + Memory m1 = default; + Memory mc1 = m1.Cast(); + + Assert.IsTrue(mc1.IsEmpty); + + Memory m2 = default; + Memory mc2 = m2.Cast(); + + Assert.IsTrue(mc2.IsEmpty); + + Memory m3 = default; + Memory mc3 = m3.Cast(); + + Assert.IsTrue(mc3.IsEmpty); + + // Same as above, but with a sliced memory (length 12, slide from 12, so length of 0) + Memory m4 = new byte[12].AsMemory(12); + Memory mc4 = m4.Cast(); + + Assert.IsTrue(mc4.IsEmpty); + + // Same as above, but slicing to 12 in two steps + Memory m5 = new byte[12].AsMemory().Slice(4).Slice(8); + Memory mc5 = m5.Cast(); + + Assert.IsTrue(mc5.IsEmpty); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_Cast_TooShort() + { + // One int is 4 bytes, so casting from 3 rounds down to 0 + Memory m1 = new byte[3]; + Memory mc1 = m1.Cast(); + + Assert.IsTrue(mc1.IsEmpty); + + // Same as above, 13 / sizeof(int) == 3 + Memory m2 = new byte[13]; + Memory mc2 = m2.Cast(); + + Assert.AreEqual(mc2.Length, 3); + + // 16 - 5 = 11 ---> 11 / sizeof(int) = 2 + Memory m3 = new byte[16].AsMemory(5); + Memory mc3 = m3.Cast(); + + Assert.AreEqual(mc3.Length, 2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastFromByte() + { + // Test for a standard cast from bytes with an evenly divisible length + Memory memoryOfBytes = new byte[128]; + Memory memoryOfFloats = memoryOfBytes.Cast(); + + Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float)); + + Span spanOfBytes = memoryOfBytes.Span; + Span spanOfFloats = memoryOfFloats.Span; + + // We also need to check that the Span returned from the caast memory + // actually has the initial reference pointing to the same location as + // the one to the same item in the span from the original memory. + Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfBytes[0], + ref Unsafe.As(ref spanOfFloats[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastToByte() + { + // Cast from float to bytes to verify casting works when the target type + // as a smaller byte size as well (so the resulting length will be larger). + Memory memoryOfFloats = new float[128]; + Memory memoryOfBytes = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfBytes = memoryOfBytes.Span; + + // Same as above, we need to verify that the resulting span has matching + // starting references with the one produced by the original memory. + Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfBytes[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastToShort() + { + // Similar test as above, just with different types to double check + Memory memoryOfFloats = new float[128]; + Memory memoryOfShorts = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfShorts = memoryOfShorts.Span; + + Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfShorts[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() + { + // Here we start from a byte array, get a memory, then cast to float and then + // back to byte. We want to verify that the final memory is both valid and + // consistent, as well that our internal optimization works and that the final + // memory correctly skipped the indirect memory managed and just wrapped the original + // array instead. This is documented in the custom array memory manager in the package. + var data = new byte[128]; + Memory memoryOfBytes = data; + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + // Here we get the array from the final memory and check that it does exist and + // the associated parameters match the ones we'd expect here (same length, offset of 0). + Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out var segment)); + Assert.AreSame(segment.Array!, data); + Assert.AreEqual(segment.Offset, 0); + Assert.AreEqual(segment.Count, data.Length); + + Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); + + Span span1 = memoryOfBytes.Span; + Span span2 = memoryBack.Span; + + // Also validate the initial and final spans for reference equality + Assert.IsTrue(span1 == span2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_Cast_TooShort_WithSlice() + { + // Like we did above, we have some more tests where we slice an initial memory and + // validate the length of the resulting, accounting for the expected rounding down. + Memory m1 = new byte[8].AsMemory().Slice(4, 3); + Memory mc1 = m1.Cast(); + + Assert.IsTrue(mc1.IsEmpty); + + Memory m2 = new byte[20].AsMemory().Slice(4, 13); + Memory mc2 = m2.Cast(); + + Assert.AreEqual(mc2.Length, 3); + + Memory m3 = new byte[16].AsMemory().Slice(5); + Memory mc3 = m3.Cast(); + + Assert.AreEqual(mc3.Length, 2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastFromByte_WithSlice() + { + // Same exact test as the cast from byte to float did above, but with a slice. This is done + // to ensure the cast still works correctly when the memory is internally storing an offset. + Memory memoryOfBytes = new byte[512].AsMemory().Slice(128, 128); + Memory memoryOfFloats = memoryOfBytes.Cast(); + + Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float)); + + Span spanOfBytes = memoryOfBytes.Span; + Span spanOfFloats = memoryOfFloats.Span; + + Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfBytes[0], + ref Unsafe.As(ref spanOfFloats[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastToByte_WithSlice() + { + // Same as above, just with inverted source and destination types + Memory memoryOfFloats = new float[512].AsMemory().Slice(128, 128); + Memory memoryOfBytes = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfBytes = memoryOfBytes.Span; + + Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfBytes[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastToShort_WithSlice() + { + // Once again the same test but with types both different in size than 1. We're mostly + // just testing the rounding logic in a number of different case to ensure it's correct. + Memory memoryOfFloats = new float[512].AsMemory().Slice(128, 128); + Memory memoryOfShorts = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfShorts = memoryOfShorts.Span; + + Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfShorts[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice() + { + // Just like the equivalent test above, but with a slice thrown in too + var data = new byte[512]; + Memory memoryOfBytes = data.AsMemory().Slice(128, 128); + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + // Here we now also have to validate the starting offset from the extracted array + Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out var segment)); + Assert.AreSame(segment.Array!, data); + Assert.AreEqual(segment.Offset, 128); + Assert.AreEqual(segment.Count, 128); + + Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); + + Span span1 = memoryOfBytes.Span; + Span span2 = memoryBack.Span; + + Assert.IsTrue(span1 == span2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastFromByte() + { + // This test is just like the ones above, but this time we're casting a memory + // that wraps a custom memory manager and not an array. This is done to ensure + // the casting logic works correctly in all cases, as it'll use a different + // memory manager internally (a memory can wrap a string, an array or a manager). + Memory memoryOfBytes = new ArrayMemoryManager(128); + Memory memoryOfFloats = memoryOfBytes.Cast(); + + Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float)); + + Span spanOfBytes = memoryOfBytes.Span; + Span spanOfFloats = memoryOfFloats.Span; + + Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfBytes[0], + ref Unsafe.As(ref spanOfFloats[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastToByte() + { + // Same as above, but with inverted types + Memory memoryOfFloats = new ArrayMemoryManager(128); + Memory memoryOfBytes = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfBytes = memoryOfBytes.Span; + + Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfBytes[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastToShort() + { + // Same as above, but with types different in size than 1, just in case + Memory memoryOfFloats = new ArrayMemoryManager(128); + Memory memoryOfShorts = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfShorts = memoryOfShorts.Span; + + Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfShorts[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack() + { + // Equivalent to the one with an array, but with a memory manager + var data = new ArrayMemoryManager(128); + Memory memoryOfBytes = data; + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + // Here we expect to get back the original memory manager, due to the same optimization we + // checked for when using an array. We need to check they're the same, and the other parameters. + Assert.IsTrue(MemoryMarshal.TryGetMemoryManager>(memoryBack, out var manager, out var start, out var length)); + Assert.AreSame(manager!, data); + Assert.AreEqual(start, 0); + Assert.AreEqual(length, 128); + + Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); + + Span span1 = memoryOfBytes.Span; + Span span2 = memoryBack.Span; + + Assert.IsTrue(span1 == span2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastFromByte_WithSlice() + { + // Same as the ones with an array, but with an extra slice + Memory memoryOfBytes = new ArrayMemoryManager(512).Memory.Slice(128, 128); + Memory memoryOfFloats = memoryOfBytes.Cast(); + + Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float)); + + Span spanOfBytes = memoryOfBytes.Span; + Span spanOfFloats = memoryOfFloats.Span; + + Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfBytes[0], + ref Unsafe.As(ref spanOfFloats[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastToByte_WithSlice() + { + // Same as above, but with inverted types + Memory memoryOfFloats = new ArrayMemoryManager(512).Memory.Slice(128, 128); + Memory memoryOfBytes = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfBytes = memoryOfBytes.Span; + + Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfBytes[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastToShort_WithSlice() + { + // Same as above but with different types + Memory memoryOfFloats = new ArrayMemoryManager(512).Memory.Slice(128, 128); + Memory memoryOfShorts = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfShorts = memoryOfShorts.Span; + + Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length); + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfShorts[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlice() + { + // Just like the one above, but with the slice + var data = new ArrayMemoryManager(512); + Memory memoryOfBytes = data.Memory.Slice(128, 128); + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + // Here we also need to validate that the offset was maintained + Assert.IsTrue(MemoryMarshal.TryGetMemoryManager>(memoryBack, out var manager, out var start, out var length)); + Assert.AreSame(manager!, data); + Assert.AreEqual(start, 128); + Assert.AreEqual(length, 128); + + Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); + + Span span1 = memoryOfBytes.Span; + Span span2 = memoryBack.Span; + + Assert.IsTrue(span1 == span2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + [DataRow(64, 0, 0)] + [DataRow(64, 4, 0)] + [DataRow(64, 0, 4)] + [DataRow(64, 4, 4)] + [DataRow(64, 4, 0)] + [DataRow(256, 16, 0)] + [DataRow(256, 4, 16)] + [DataRow(256, 64, 0)] + [DataRow(256, 64, 8)] + public unsafe void Test_MemoryExtensions_FromArray_CastFromByte_Pin(int size, int preOffset, int postOffset) + { + // Here we need to validate that pinning works correctly in a number of cases. First we allocate + // an array of the requested size, then get a memory after slicing to a target position, then cast + // and then slice again. We do so to ensure that pinning correctly tracks the correct index with + // respect to the original array through a number of internal offsets. As in, when pinning the + // final memory, our internal custom memory manager should be able to pin the item in the original + // array at offset preOffset + (postOffset * sizeof(float)), accounting for the cast as well. + var data = new byte[size]; + Memory memoryOfBytes = data.AsMemory(preOffset); + Memory memoryOfFloats = memoryOfBytes.Cast().Slice(postOffset); + + using var handle = memoryOfFloats.Pin(); + + void* p1 = handle.Pointer; + void* p2 = Unsafe.AsPointer(ref data[preOffset + (postOffset * sizeof(float))]); + + Assert.IsTrue(p1 == p2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + [DataRow(64, 0, 0)] + [DataRow(64, 4, 0)] + [DataRow(64, 0, 4)] + [DataRow(64, 4, 4)] + [DataRow(64, 4, 0)] + [DataRow(256, 16, 0)] + [DataRow(256, 4, 16)] + [DataRow(256, 64, 0)] + [DataRow(256, 64, 8)] + public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int size, int preOffset, int postOffset) + { + // Just like the test above, but this type the initial memory wraps a memory manager + var data = new ArrayMemoryManager(size); + Memory memoryOfBytes = data.Memory.Slice(preOffset); + Memory memoryOfFloats = memoryOfBytes.Cast().Slice(postOffset); + + using var handle = memoryOfFloats.Pin(); + + void* p1 = handle.Pointer; + void* p2 = Unsafe.AsPointer(ref data.GetSpan()[preOffset + (postOffset * sizeof(float))]); + + Assert.IsTrue(p1 == p2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromString_CastFromByteAndBack() + { + // This is the same as the tests above, but here we're testing the + // other remaining case, that is when a memory is wrapping a string. + var data = new string('a', 128); + Memory memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()); + Memory memoryOfFloats = memoryOfChars.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfChars.Length, memoryBack.Length); + + // Get the original string back (to validate the optimization too) and check the params + Assert.IsTrue(MemoryMarshal.TryGetString(memoryOfChars, out var text, out int start, out int length)); + Assert.AreSame(text!, data); + Assert.AreEqual(start, 0); + Assert.AreEqual(length, data.Length); + + Assert.IsTrue(memoryOfChars.Equals(memoryBack)); + + Span span1 = memoryOfChars.Span; + Span span2 = memoryBack.Span; + + Assert.IsTrue(span1 == span2); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + [DataRow(64, 0, 0)] + [DataRow(64, 4, 0)] + [DataRow(64, 0, 4)] + [DataRow(64, 4, 4)] + [DataRow(64, 4, 0)] + [DataRow(256, 16, 0)] + [DataRow(256, 4, 16)] + [DataRow(256, 64, 0)] + [DataRow(256, 64, 8)] + public unsafe void Test_MemoryExtensions_FromString_CastAndPin(int size, int preOffset, int postOffset) + { + // Same test as before to validate pinning, but starting from a string + var data = new string('a', size); + Memory memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()).Slice(preOffset); + Memory memoryOfBytes = memoryOfChars.Cast().Slice(postOffset); + + using (var handle1 = memoryOfBytes.Pin()) + { + void* p1 = handle1.Pointer; + void* p2 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(byte) / sizeof(char)))); + + Assert.IsTrue(p1 == p2); + } + + // Here we also add an extra test just like the one above, but casting to a type + // that is bigger in byte size than char. Just to double check the casting logic. + Memory memoryOfInts = memoryOfChars.Cast().Slice(postOffset); + + using (var handle2 = memoryOfInts.Pin()) + { + void* p3 = handle2.Pointer; + void* p4 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(int) / sizeof(char)))); + + Assert.IsTrue(p3 == p4); + } + } + [TestCategory("MemoryExtensions")] [TestMethod] public void Test_MemoryExtensions_EmptyMemoryStream() { Memory memory = default; + // Creating a stream from a default memory is valid, it's just empty Stream stream = memory.AsStream(); Assert.IsNotNull(stream); @@ -37,5 +593,43 @@ public void Test_MemoryExtensions_MemoryStream() Assert.AreEqual(stream.Length, memory.Length); Assert.IsTrue(stream.CanWrite); } + + private sealed class ArrayMemoryManager : MemoryManager + where T : unmanaged + { + private readonly T[] array; + + public ArrayMemoryManager(int size) + { + this.array = new T[size]; + } + + public override Span GetSpan() + { + return this.array; + } + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned); + ref T r0 = ref this.array[elementIndex]; + void* p = Unsafe.AsPointer(ref r0); + + return new MemoryHandle(p, handle); + } + + public override void Unpin() + { + } + + protected override void Dispose(bool disposing) + { + } + + public static implicit operator Memory(ArrayMemoryManager memoryManager) + { + return memoryManager.Memory; + } + } } }