From 6c7ffe7c2d68384537e384f53018a95a9194c655 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 13:38:41 +0200 Subject: [PATCH 01/18] Added initial version of Memory.Cast --- .../Extensions/MemoryExtensions.cs | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index 8c1e053a186..47d8c810eb1 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -3,9 +3,11 @@ // 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 MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -15,6 +17,56 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class MemoryExtensions { + /// + /// 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 . + /// The source slice, of type . + /// A of type + /// + /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory Cast(this Memory memory) + where TFrom : unmanaged + where TTo : unmanaged + { + if (memory.IsEmpty) + { + return default; + } + + 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 int start, out int 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.Cast(start, length).Memory; + } + + return new ProxyMemoryManager(memoryManager, start, length).Memory; + } + + // Throws when the memory instance has an unsupported backing store (eg. a string) + static Memory 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. /// @@ -32,5 +84,205 @@ public static Stream AsStream(this Memory memory) { return new MemoryStream(memory); } + + /// + /// 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. + MemoryManager Cast(int offset, int length) + where T : unmanaged; + } + + /// + /// 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); + + Span span = MemoryMarshal.CreateSpan(ref r0, this.length); +#else + Span span = this.array.AsSpan(this.offset, this.length); +#endif + + // 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); + } + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + 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 MemoryManager Cast(int offset, int length) + where T : unmanaged + { + return new ArrayMemoryManager(this.array, this.offset + offset, length); + } + } + + /// + /// 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) + { + int byteOffset = elementIndex * Unsafe.SizeOf(); + +#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(this.length + shiftedOffset); + } + + /// + public override void Unpin() + { + this.memoryManager.Unpin(); + } + + /// + protected override void Dispose(bool disposing) + { + ((IDisposable)this.memoryManager).Dispose(); + } + + /// + public MemoryManager Cast(int offset, int length) + where T : unmanaged + { + return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length); + } + + /// + /// 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"); + } + } } } From 87c16a80a2bf0f82893ed5e637d39aa777618186 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 13:45:16 +0200 Subject: [PATCH 02/18] Code refactoring --- .../ArrayMemoryManager{TFrom,TTo}.cs | 99 +++++++++ .../Internals/Interfaces/IMemoryManager.cs | 20 ++ .../ProxyMemoryManager{TFrom,TTo}.cs | 102 +++++++++ .../Extensions/MemoryExtensions.cs | 202 +----------------- 4 files changed, 223 insertions(+), 200 deletions(-) create mode 100644 Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs create mode 100644 Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs create mode 100644 Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs 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..db77e313dab --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -0,0 +1,99 @@ +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; + +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); + + Span span = MemoryMarshal.CreateSpan(ref r0, this.length); +#else + Span span = this.array.AsSpan(this.offset, this.length); +#endif + + // 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); + } + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + 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 MemoryManager Cast(int offset, int length) + where T : unmanaged + { + return new ArrayMemoryManager(this.array, this.offset + offset, length); + } + } +} 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..282cfab3627 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs @@ -0,0 +1,20 @@ +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. + MemoryManager Cast(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..b4c0ba16c0c --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -0,0 +1,102 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; + +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) + { + int byteOffset = elementIndex * Unsafe.SizeOf(); + +#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(this.length + shiftedOffset); + } + + /// + public override void Unpin() + { + this.memoryManager.Unpin(); + } + + /// + protected override void Dispose(bool disposing) + { + ((IDisposable)this.memoryManager).Dispose(); + } + + /// + public MemoryManager Cast(int offset, int length) + where T : unmanaged + { + return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length); + } + + /// + /// 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/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index 47d8c810eb1..22e6291642a 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -8,6 +8,8 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -84,205 +86,5 @@ public static Stream AsStream(this Memory memory) { return new MemoryStream(memory); } - - /// - /// 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. - MemoryManager Cast(int offset, int length) - where T : unmanaged; - } - - /// - /// 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); - - Span span = MemoryMarshal.CreateSpan(ref r0, this.length); -#else - Span span = this.array.AsSpan(this.offset, this.length); -#endif - - // 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); - } - - /// - public override unsafe MemoryHandle Pin(int elementIndex = 0) - { - 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 MemoryManager Cast(int offset, int length) - where T : unmanaged - { - return new ArrayMemoryManager(this.array, this.offset + offset, length); - } - } - - /// - /// 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) - { - int byteOffset = elementIndex * Unsafe.SizeOf(); - -#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(this.length + shiftedOffset); - } - - /// - public override void Unpin() - { - this.memoryManager.Unpin(); - } - - /// - protected override void Dispose(bool disposing) - { - ((IDisposable)this.memoryManager).Dispose(); - } - - /// - public MemoryManager Cast(int offset, int length) - where T : unmanaged - { - return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length); - } - - /// - /// 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"); - } - } } } From ccab26b01114d2b0cb460af29069e0aad1380bdd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 13:49:54 +0200 Subject: [PATCH 03/18] Added the extension for ReadOnlyMemory --- .../Extensions/MemoryExtensions.cs | 50 +++--------------- .../Extensions/ReadOnlyMemoryExtensions.cs | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index 22e6291642a..457837d7e47 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -3,13 +3,10 @@ // 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; using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -20,53 +17,20 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions public static class MemoryExtensions { /// - /// 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. + /// 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 slice, of type . - /// A of type - /// - /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. - /// + /// 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 (eg. when it is a ). [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory Cast(this Memory memory) where TFrom : unmanaged where TTo : unmanaged { - if (memory.IsEmpty) - { - return default; - } - - 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 int start, out int 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.Cast(start, length).Memory; - } - - return new ProxyMemoryManager(memoryManager, start, length).Memory; - } - - // Throws when the memory instance has an unsupported backing store (eg. a string) - static Memory ThrowArgumentExceptionForUnsupportedMemory() - { - throw new ArgumentException("The input instance doesn't have a supported underlying data store."); - } - - return ThrowArgumentExceptionForUnsupportedMemory(); + return MemoryMarshal.AsMemory(((ReadOnlyMemory)memory).Cast()); } /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 46101e3db89..5877eb4ef0a 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; using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -15,6 +19,53 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class ReadOnlyMemoryExtensions { + /// + /// 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 (eg. when it is a ). + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory Cast(this ReadOnlyMemory memory) + where TFrom : unmanaged + where TTo : unmanaged + { + if (memory.IsEmpty) + { + return default; + } + + 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 int start, out int 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.Cast(start, length).Memory; + } + + return new ProxyMemoryManager(memoryManager, start, length).Memory; + } + + // Throws when the memory instance has an unsupported backing store (eg. a string) + 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. /// From caca3b1232bfe8b939f729fedf909629479776af Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 13:50:02 +0200 Subject: [PATCH 04/18] Added missing file headers --- .../Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs | 6 +++++- .../Buffers/Internals/Interfaces/IMemoryManager.cs | 6 +++++- .../Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs index db77e313dab..1d3d0643b30 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -1,4 +1,8 @@ -using System; +// 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; diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs index 282cfab3627..06dcfab30cc 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs @@ -1,4 +1,8 @@ -using System.Buffers; +// 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; namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces { diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs index b4c0ba16c0c..2b0c943e228 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -1,4 +1,8 @@ -using System; +// 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; From 77f203f5f7acdd69644fe49e54c58a2d265e08b2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 13:58:50 +0200 Subject: [PATCH 05/18] Added validation for Pin methods --- .../Internals/ArrayMemoryManager{TFrom,TTo}.cs | 13 +++++++++++++ .../Internals/ProxyMemoryManager{TFrom,TTo}.cs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs index 1d3d0643b30..89cd2360756 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -68,6 +68,11 @@ public override Span GetSpan() /// public override unsafe 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(), @@ -99,5 +104,13 @@ public MemoryManager Cast(int offset, int length) { return new ArrayMemoryManager(this.array, this.offset + offset, length); } + + /// + /// 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"); + } } } diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs index 2b0c943e228..f993adec660 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -58,6 +58,11 @@ public override Span GetSpan() /// public override MemoryHandle Pin(int elementIndex = 0) { + if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf())) + { + ThrowArgumentExceptionForInvalidIndex(); + } + int byteOffset = elementIndex * Unsafe.SizeOf(); #if NETSTANDARD1_4 @@ -95,6 +100,14 @@ public MemoryManager Cast(int offset, int length) return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length); } + /// + /// 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. /// From 79665cde34241d350f0696a9c7ab3e7359178797 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 15:13:54 +0200 Subject: [PATCH 06/18] Fixed type constraints for [ReadOnly]Span.Cast --- .../Extensions/ReadOnlySpanExtensions.cs | 8 ++------ .../Extensions/SpanExtensions.cs | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index b2a07767c01..72c0ef5746b 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -197,7 +197,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 . @@ -206,14 +205,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 8ce1cca75a0..888b732eaad 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -71,7 +71,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 . @@ -80,14 +79,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); } From db31e9804f098397d651b3eaff14316e03a9f6dc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 15:17:43 +0200 Subject: [PATCH 07/18] Added [ReadOnly]Memory.AsBytes extensions --- .../Extensions/MemoryExtensions.cs | 17 +++++++++++++++++ .../Extensions/ReadOnlyMemoryExtensions.cs | 17 +++++++++++++++++ .../Extensions/ReadOnlySpanExtensions.cs | 4 ---- .../Extensions/SpanExtensions.cs | 4 ---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index 457837d7e47..6f5e62233be 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -16,6 +16,23 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class MemoryExtensions { + /// + /// 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 . + /// + [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 . /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 5877eb4ef0a..670d91aa94d 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -19,6 +19,23 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class ReadOnlyMemoryExtensions { + /// + /// 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 . + /// + [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 . /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index 72c0ef5746b..f0c4f3aac6f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -176,14 +176,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 . /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs index 888b732eaad..22a14d6258f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -50,14 +50,10 @@ public static ref T DangerousGetReferenceAt(this Span span, int 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 . /// From ba22d22c1da6a0c6977e3f0bb982569e152db4bd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 15:56:53 +0200 Subject: [PATCH 08/18] Optimized [ReadOnly]Memory cast to source T --- .../Internals/ArrayMemoryManager{TFrom,TTo}.cs | 12 ++++++++++-- .../Buffers/Internals/Interfaces/IMemoryManager.cs | 7 ++++--- .../Internals/ProxyMemoryManager{TFrom,TTo}.cs | 11 +++++++++-- .../Extensions/ReadOnlyMemoryExtensions.cs | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs index 89cd2360756..7e19404a3b7 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -99,10 +99,18 @@ protected override void Dispose(bool disposing) } /// - public MemoryManager Cast(int offset, int length) + public Memory GetMemory(int offset, int length) where T : unmanaged { - return new ArrayMemoryManager(this.array, this.offset + offset, 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(this.offset + offset, length); + } + + return new ArrayMemoryManager(this.array, this.offset + offset, length).Memory; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs index 06dcfab30cc..20a56c54b2b 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs @@ -2,6 +2,7 @@ // 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 @@ -12,13 +13,13 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces internal interface IMemoryManager { /// - /// Creates a new that reinterprets the underlying data for the current instance. + /// 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. - MemoryManager Cast(int offset, int length) + /// 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 index f993adec660..125427c0d9c 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -94,10 +94,17 @@ protected override void Dispose(bool disposing) } /// - public MemoryManager Cast(int offset, int length) + public Memory GetMemory(int offset, int length) where T : unmanaged { - return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length); + // Like in the other memory manager, we can skip one level of indirection in cases + // where the user is just going back to the original memory type, reusing the manager. + if (typeof(T) == typeof(TFrom)) + { + return (Memory)(object)this.memoryManager.Memory.Slice(offset, length); + } + + return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length).Memory; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 670d91aa94d..d5303663aa8 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -68,7 +68,7 @@ public static ReadOnlyMemory Cast(this ReadOnlyMemory me // indirection level in the chain of memory managers needed to access the target data buffer to use. if (memoryManager is IMemoryManager wrappingManager) { - return wrappingManager.Cast(start, length).Memory; + return wrappingManager.GetMemory(start, length); } return new ProxyMemoryManager(memoryManager, start, length).Memory; From 53cb75f4d961343c43083b35e01161907e5d6e67 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 18:50:23 +0200 Subject: [PATCH 09/18] Fixed offset/length computation with double cast --- .../ArrayMemoryManager{TFrom,TTo}.cs | 18 +++++-- .../ProxyMemoryManager{TFrom,TTo}.cs | 13 +++-- .../Helpers/HashCode{T}.cs | 1 + .../Helpers/Internals/RuntimeHelpers.cs | 54 +++++++++++++++++++ 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs index 7e19404a3b7..b33f4240653 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -8,6 +8,7 @@ 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 { @@ -53,16 +54,18 @@ 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); - Span span = MemoryMarshal.CreateSpan(ref r0, this.length); + return MemoryMarshal.CreateSpan(ref r1, length); #else Span span = this.array.AsSpan(this.offset, this.length); -#endif // 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 } /// @@ -102,15 +105,22 @@ 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(this.offset + offset, length); + return (Memory)(object)this.array.AsMemory(absoluteOffset, absoluteLength); } - return new ArrayMemoryManager(this.array, this.offset + offset, length).Memory; + return new ArrayMemoryManager(this.array, absoluteOffset, absoluteLength).Memory; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs index 125427c0d9c..98efecb0ebb 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -7,6 +7,7 @@ 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 { @@ -97,14 +98,18 @@ protected override void Dispose(bool disposing) public Memory GetMemory(int offset, int length) where T : unmanaged { - // Like in the other memory manager, we can skip one level of indirection in cases - // where the user is just going back to the original memory type, reusing the manager. + // 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(offset, length); + return (Memory)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength); } - return new ProxyMemoryManager(this.memoryManager, this.offset + offset, length).Memory; + return new ProxyMemoryManager(this.memoryManager, absoluteOffset, absoluteLength).Memory; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs index c2f96561722..32ee67f2da8 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +using RuntimeHelpers = System.Runtime.CompilerServices.RuntimeHelpers; namespace Microsoft.Toolkit.HighPerformance.Helpers { diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs new file mode 100644 index 00000000000..13c1bee7dbc --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -0,0 +1,54 @@ +// 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.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals +{ + /// + /// A helper class that with utility methods for dealing with references, and other low-level details. + /// + internal static class RuntimeHelpers + { + /// + /// Converts a length of items from one size to another. This method exposes the logic from + /// , just for the length conversion. + /// + /// 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 int ConvertLength(int length) + where TFrom : unmanaged + where TTo : unmanaged + { + uint fromSize = (uint)Unsafe.SizeOf(); + uint toSize = (uint)Unsafe.SizeOf(); + uint fromLength = (uint)length; + int toLength; + + if (fromSize == toSize) + { + toLength = (int)fromLength; + } + else if (fromSize == 1) + { + toLength = (int)(fromLength / toSize); + } + else + { + ulong toLengthUInt64 = (ulong)fromLength * fromSize / toSize; + + toLength = checked((int)toLengthUInt64); + } + + return toLength; + } + } +} \ No newline at end of file From b9047906dfe476e80f8fdeef65073f0b5d8cbeaa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 18:50:35 +0200 Subject: [PATCH 10/18] Added some unit tests --- .../Extensions/Test_MemoryExtensions.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 705a335735d..49870749fe7 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -4,6 +4,8 @@ using System; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,6 +14,109 @@ namespace UnitTests.HighPerformance.Extensions [TestClass] public class Test_MemoryExtensions { + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_Cast_Empty() + { + 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); + + Memory m4 = new byte[12].AsMemory(12); + Memory mc4 = m4.Cast(); + + Assert.IsTrue(mc4.IsEmpty); + + 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() + { + Memory m1 = new byte[3]; + Memory mc1 = m1.Cast(); + + Assert.IsTrue(mc1.IsEmpty); + + Memory m2 = new byte[13]; + Memory mc2 = m2.Cast(); + + Assert.AreEqual(mc2.Length, 3); + + 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() + { + Memory memoryOfBytes = new byte[128]; + Memory memoryOfFloats = memoryOfBytes.Cast(); + + Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float)); + + Span spanOfBytes = memoryOfBytes.Span; + Span spanOfFloats = memoryOfFloats.Span; + + Assert.IsTrue(Unsafe.AreSame( + ref spanOfBytes[0], + ref Unsafe.As(ref spanOfFloats[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastToByte() + { + Memory memoryOfFloats = new float[128]; + Memory memoryOfBytes = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float)); + + Span spanOfFloats = memoryOfFloats.Span; + Span spanOfBytes = memoryOfBytes.Span; + + Assert.IsTrue(Unsafe.AreSame( + ref spanOfFloats[0], + ref Unsafe.As(ref spanOfBytes[0]))); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() + { + var data = new byte[128]; + Memory memoryOfBytes = data; + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + 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)); + } + [TestCategory("MemoryExtensions")] [TestMethod] public void Test_MemoryExtensions_EmptyMemoryStream() From ba514b08184cab1205b8b0d1a0456bef310a1af8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 18:58:58 +0200 Subject: [PATCH 11/18] Added unit test with types != byte --- .../Extensions/Test_MemoryExtensions.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 49870749fe7..da4e9721d52 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -76,6 +76,7 @@ public void Test_MemoryExtensions_FromArray_CastFromByte() 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]))); @@ -93,11 +94,30 @@ public void Test_MemoryExtensions_FromArray_CastToByte() 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() + { + 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() @@ -115,6 +135,11 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() Assert.AreEqual(segment.Count, data.Length); Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); + + Span span1 = memoryOfBytes.Span; + Span span2 = memoryBack.Span; + + Assert.IsTrue(span1 == span2); } [TestCategory("MemoryExtensions")] From c52374224bb4688e61cc653253205245378e1af1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Oct 2020 20:32:51 +0200 Subject: [PATCH 12/18] Added unit tests for slices and MemoryManager --- .../Extensions/Test_MemoryExtensions.cs | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index da4e9721d52..10a66251a27 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -3,6 +3,7 @@ // 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; @@ -142,6 +143,260 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() Assert.IsTrue(span1 == span2); } + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_Cast_TooShort_WithSlice() + { + 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() + { + 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() + { + 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() + { + 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() + { + 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); + + 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() + { + 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() + { + 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() + { + 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() + { + var data = new ArrayMemoryManager(128); + Memory memoryOfBytes = data; + Memory memoryOfFloats = memoryOfBytes.Cast(); + Memory memoryBack = memoryOfFloats.Cast(); + + Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); + + 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() + { + 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() + { + 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() + { + 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() + { + 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); + + 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] public void Test_MemoryExtensions_EmptyMemoryStream() @@ -167,5 +422,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; + } + } } } From ae2844ae4132d1b1b847fe6b298af2efaaf263ed Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 00:52:42 +0200 Subject: [PATCH 13/18] Bug fixed in Pin methods, added unit tests --- .../ArrayMemoryManager{TFrom,TTo}.cs | 6 +-- .../ProxyMemoryManager{TFrom,TTo}.cs | 7 ++- .../Extensions/Test_MemoryExtensions.cs | 50 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs index b33f4240653..7a09d1f9007 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs @@ -73,11 +73,11 @@ public override unsafe MemoryHandle Pin(int elementIndex = 0) { if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf())) { - ThrowArgumentExceptionForInvalidIndex(); + ThrowArgumentOutOfRangeExceptionForInvalidIndex(); } int - bytePrefix = this.offset + Unsafe.SizeOf(), + bytePrefix = this.offset * Unsafe.SizeOf(), byteSuffix = elementIndex * Unsafe.SizeOf(), byteOffset = bytePrefix + byteSuffix; @@ -126,7 +126,7 @@ public Memory GetMemory(int offset, int length) /// /// Throws an when the target index for is invalid. /// - private static void ThrowArgumentExceptionForInvalidIndex() + 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/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs index 98efecb0ebb..688450d6bd1 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs @@ -64,7 +64,10 @@ public override MemoryHandle Pin(int elementIndex = 0) ThrowArgumentExceptionForInvalidIndex(); } - int byteOffset = elementIndex * Unsafe.SizeOf(); + int + bytePrefix = this.offset * Unsafe.SizeOf(), + byteSuffix = elementIndex * Unsafe.SizeOf(), + byteOffset = bytePrefix + byteSuffix; #if NETSTANDARD1_4 int @@ -79,7 +82,7 @@ public override MemoryHandle Pin(int elementIndex = 0) ThrowArgumentExceptionForInvalidAlignment(); } - return this.memoryManager.Pin(this.length + shiftedOffset); + return this.memoryManager.Pin(shiftedOffset); } /// diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 10a66251a27..549a22b8cdd 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -397,6 +397,56 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlic 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) + { + 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) + { + 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_EmptyMemoryStream() From 62d1a2a0680cbdd302a2539c192dfea3d08b92fd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 02:06:00 +0200 Subject: [PATCH 14/18] Added support for casting Memory from string --- .../Internals/StringMemoryManager{TTo}.cs | 126 ++++++++++++++++++ .../Extensions/MemoryExtensions.cs | 3 +- .../Extensions/ReadOnlyMemoryExtensions.cs | 11 +- 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs 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 6f5e62233be..87ace56fbfd 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -25,6 +25,7 @@ public static class MemoryExtensions /// /// 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) @@ -40,7 +41,7 @@ public static Memory AsBytes(this Memory memory) /// The type of items in the destination . /// The source , of type . /// A of type - /// Thrown when the data store of is not supported (eg. when it is a ). + /// Thrown when the data store of is not supported. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory Cast(this Memory memory) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index d5303663aa8..51dc8903192 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -28,6 +28,7 @@ public static class ReadOnlyMemoryExtensions /// /// 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) @@ -43,7 +44,7 @@ public static ReadOnlyMemory AsBytes(this ReadOnlyMemory memory) /// The type of items in the destination . /// The source , of type . /// A of type - /// Thrown when the data store of is not supported (eg. when it is a ). + /// Thrown when the data store of is not supported. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory Cast(this ReadOnlyMemory memory) @@ -55,12 +56,18 @@ public static ReadOnlyMemory Cast(this ReadOnlyMemory me 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 int start, out int length)) + 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 From aa895a4cfa8222bc1cfdace01bda22a03f518be1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 11:40:42 +0200 Subject: [PATCH 15/18] Added unit tests for StringMemoryManager --- .../Extensions/ReadOnlyMemoryExtensions.cs | 2 +- .../Extensions/Test_MemoryExtensions.cs | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 51dc8903192..2cbd5454e1d 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -81,7 +81,7 @@ public static ReadOnlyMemory Cast(this ReadOnlyMemory me return new ProxyMemoryManager(memoryManager, start, length).Memory; } - // Throws when the memory instance has an unsupported backing store (eg. a string) + // 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."); diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 549a22b8cdd..189243efcb3 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -447,6 +447,66 @@ public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int Assert.IsTrue(p1 == p2); } + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_FromString_CastFromByteAndBack() + { + 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); + + 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) + { + 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); + } + + 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() From 7184fe21387587f7f38f30803e87abf163b1c5f0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 30 Oct 2020 17:53:57 +0100 Subject: [PATCH 16/18] Added comments to unit tests --- .../Extensions/Test_MemoryExtensions.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs index 189243efcb3..edbf5d0e961 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -19,6 +19,8 @@ public class Test_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(); @@ -34,11 +36,13 @@ public void Test_MemoryExtensions_Cast_Empty() 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(); @@ -49,16 +53,19 @@ public void Test_MemoryExtensions_Cast_Empty() [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(); @@ -69,6 +76,7 @@ public void Test_MemoryExtensions_Cast_TooShort() [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(); @@ -77,6 +85,9 @@ public void Test_MemoryExtensions_FromArray_CastFromByte() 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], @@ -87,6 +98,8 @@ public void Test_MemoryExtensions_FromArray_CastFromByte() [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(); @@ -95,6 +108,8 @@ public void Test_MemoryExtensions_FromArray_CastToByte() 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], @@ -105,6 +120,7 @@ public void Test_MemoryExtensions_FromArray_CastToByte() [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(); @@ -123,6 +139,11 @@ public void Test_MemoryExtensions_FromArray_CastToShort() [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(); @@ -130,6 +151,8 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() 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); @@ -140,6 +163,7 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() Span span1 = memoryOfBytes.Span; Span span2 = memoryBack.Span; + // Also validate the initial and final spans for reference equality Assert.IsTrue(span1 == span2); } @@ -147,6 +171,8 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack() [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(); @@ -167,6 +193,8 @@ public void Test_MemoryExtensions_Cast_TooShort_WithSlice() [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(); @@ -185,6 +213,7 @@ public void Test_MemoryExtensions_FromArray_CastFromByte_WithSlice() [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(); @@ -203,6 +232,8 @@ public void Test_MemoryExtensions_FromArray_CastToByte_WithSlice() [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(); @@ -221,6 +252,7 @@ public void Test_MemoryExtensions_FromArray_CastToShort_WithSlice() [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(); @@ -228,6 +260,7 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice() 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); @@ -245,6 +278,10 @@ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice() [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(); @@ -263,6 +300,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByte() [TestMethod] public void Test_MemoryExtensions_FromMemoryManager_CastToByte() { + // Same as above, but with inverted types Memory memoryOfFloats = new ArrayMemoryManager(128); Memory memoryOfBytes = memoryOfFloats.Cast(); @@ -281,6 +319,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastToByte() [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(); @@ -299,6 +338,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastToShort() [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(); @@ -306,6 +346,8 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack() 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); @@ -323,6 +365,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack() [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(); @@ -341,6 +384,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByte_WithSlice() [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(); @@ -359,6 +403,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastToByte_WithSlice() [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(); @@ -377,6 +422,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastToShort_WithSlice() [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(); @@ -384,6 +430,7 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlic 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); @@ -410,6 +457,12 @@ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlic [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); @@ -435,6 +488,7 @@ public unsafe void Test_MemoryExtensions_FromArray_CastFromByte_Pin(int size, in [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); @@ -451,6 +505,8 @@ public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int [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(); @@ -458,6 +514,7 @@ public void Test_MemoryExtensions_FromString_CastFromByteAndBack() 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); @@ -484,6 +541,7 @@ public void Test_MemoryExtensions_FromString_CastFromByteAndBack() [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); @@ -496,6 +554,8 @@ public unsafe void Test_MemoryExtensions_FromString_CastAndPin(int size, int pre 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()) @@ -513,6 +573,7 @@ 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); From cad806ca230cbf76f5f65baff920a201f855e71b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Nov 2020 21:36:52 +0100 Subject: [PATCH 17/18] Fixed an issue with an XML comment --- .../Helpers/Internals/RuntimeHelpers.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs index ccf0cef0898..2b077eada2d 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -24,8 +24,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals internal static class RuntimeHelpers { /// - /// Converts a length of items from one size to another. This method exposes the logic from - /// , just for the length conversion. + /// Converts a length of items from one size to another (rounding towards zero). /// /// The source type of items. /// The target type of items. From 259418b35aecffe062e3448ba75a17632083ad6d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Nov 2020 21:44:59 +0100 Subject: [PATCH 18/18] Minor codegen improvements --- .../Helpers/Internals/RuntimeHelpers.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs index 2b077eada2d..5a22ef8aff8 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -32,31 +32,24 @@ internal static class RuntimeHelpers /// The converted length for the specified argument and types. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ConvertLength(int length) + public static unsafe int ConvertLength(int length) where TFrom : unmanaged where TTo : unmanaged { - uint fromSize = (uint)Unsafe.SizeOf(); - uint toSize = (uint)Unsafe.SizeOf(); - uint fromLength = (uint)length; - int toLength; - - if (fromSize == toSize) + if (sizeof(TFrom) == sizeof(TTo)) { - toLength = (int)fromLength; + return length; } - else if (fromSize == 1) + else if (sizeof(TFrom) == 1) { - toLength = (int)(fromLength / toSize); + return length / sizeof(TTo); } else { - ulong toLengthUInt64 = (ulong)fromLength * fromSize / toSize; + ulong targetLength = (ulong)(uint)length * (uint)sizeof(TFrom) / (uint)sizeof(TTo); - toLength = checked((int)toLengthUInt64); + return checked((int)targetLength); } - - return toLength; } ///