diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs index e17c0c48a18..9e1c032b1f2 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs @@ -24,7 +24,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers /// the arrays in use are rented from the shared instance, /// and that is also available on .NET Standard 2.0. /// - [DebuggerTypeProxy(typeof(ArrayPoolBufferWriterDebugView<>))] + [DebuggerTypeProxy(typeof(MemoryDebugView<>))] [DebuggerDisplay("{ToString(),raw}")] public sealed class ArrayPoolBufferWriter : IBuffer, IMemoryOwner { diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/RawObjectMemoryManager{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/RawObjectMemoryManager{T}.cs new file mode 100644 index 00000000000..89ce81ea208 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/RawObjectMemoryManager{T}.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if SPAN_RUNTIME_SUPPORT + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Extensions; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals +{ + /// + /// A custom that can wrap arbitrary instances. + /// + /// The type of elements in the target memory area. + internal sealed class RawObjectMemoryManager : MemoryManager + { + /// + /// The target instance. + /// + private readonly object instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The length of the target memory area. + /// + private readonly int length; + + /// + /// Initializes a new instance of the class. + /// + /// The target instance. + /// The starting offset within . + /// The usable length within . + public RawObjectMemoryManager(object instance, IntPtr offset, int length) + { + this.instance = instance; + this.offset = offset; + this.length = length; + } + + /// + public override Span GetSpan() + { + ref T r0 = ref this.instance.DangerousGetObjectDataReferenceAt(this.offset); + + return MemoryMarshal.CreateSpan(ref r0, this.length); + } + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if ((uint)elementIndex >= (uint)this.length) + { + ThrowArgumentOutOfRangeExceptionForInvalidElementIndex(); + } + + // Allocating a pinned handle for the array with fail and throw an exception + // if the array contains non blittable data. This is the expected behavior and + // the same happens when trying to pin a Memory instance obtained through + // traditional means (eg. via the implicit T[] array conversion), if T is a + // reference type or a type containing some references. + GCHandle handle = GCHandle.Alloc(this.instance, GCHandleType.Pinned); + ref T r0 = ref this.instance.DangerousGetObjectDataReferenceAt(this.offset); + ref T r1 = ref Unsafe.Add(ref r0, (nint)(uint)elementIndex); + void* p = Unsafe.AsPointer(ref r1); + + return new MemoryHandle(p, handle); + } + + /// + public override void Unpin() + { + } + + /// + protected override void Dispose(bool disposing) + { + } + + /// + /// Throws an when the input index for is not valid. + /// + private static void ThrowArgumentOutOfRangeExceptionForInvalidElementIndex() + { + throw new ArgumentOutOfRangeException("elementIndex", "The input element index was not in the valid range"); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/MemoryBufferWriter{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryBufferWriter{T}.cs index 9203eebc182..2dca3d29c03 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/MemoryBufferWriter{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryBufferWriter{T}.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Views; namespace Microsoft.Toolkit.HighPerformance.Buffers { @@ -20,7 +21,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers /// instances (or objects that can be converted to a ), to ensure the data is written directly /// to the intended buffer, with no possibility of doing additional allocations or expanding the available capacity. /// - [DebuggerTypeProxy(typeof(MemoryBufferWriter<>))] + [DebuggerTypeProxy(typeof(MemoryDebugView<>))] [DebuggerDisplay("{ToString(),raw}")] public sealed class MemoryBufferWriter : IBuffer { diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs index b243a5fdb5d..7f6391a967a 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs @@ -16,7 +16,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers /// An implementation with an embedded length and a fast accessor. /// /// The type of items to store in the current instance. - [DebuggerTypeProxy(typeof(MemoryOwnerDebugView<>))] + [DebuggerTypeProxy(typeof(MemoryDebugView<>))] [DebuggerDisplay("{ToString(),raw}")] public sealed class MemoryOwner : IMemoryOwner { diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs index 481f60f8195..6db21491cc5 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs @@ -31,7 +31,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers /// Not doing so will cause the underlying buffer not to be returned to the shared pool. /// /// The type of items to store in the current instance. - [DebuggerTypeProxy(typeof(SpanOwnerDebugView<>))] + [DebuggerTypeProxy(typeof(MemoryDebugView<>))] [DebuggerDisplay("{ToString(),raw}")] public readonly ref struct SpanOwner { diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/StringPool.cs b/Microsoft.Toolkit.HighPerformance/Buffers/StringPool.cs index d2a98ae8c94..b2f75bbf0d3 100644 --- a/Microsoft.Toolkit.HighPerformance/Buffers/StringPool.cs +++ b/Microsoft.Toolkit.HighPerformance/Buffers/StringPool.cs @@ -536,7 +536,7 @@ private unsafe ref string TryGet(ReadOnlySpan span, int hashcode) (uint)i < (uint)length; i = entry.NextIndex) { - entry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)i); + entry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)i); if (entry.HashCode == hashcode && entry.Value!.AsSpan().SequenceEqual(span)) @@ -556,7 +556,7 @@ private unsafe ref string TryGet(ReadOnlySpan span, int hashcode) /// The new instance to store. /// The precomputed hashcode for . [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe void Insert(string value, int hashcode) + private void Insert(string value, int hashcode) { ref int bucketsRef = ref this.buckets.DangerousGetReference(); ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference(); @@ -571,7 +571,7 @@ private unsafe void Insert(string value, int hashcode) entryIndex = heapEntriesRef.MapIndex; heapIndex = 0; - ref MapEntry removedEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex); + ref MapEntry removedEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex); // The removal logic can be extremely optimized in this case, as we // can retrieve the precomputed hashcode for the target entry by doing @@ -588,9 +588,9 @@ private unsafe void Insert(string value, int hashcode) } int bucketIndex = hashcode & (this.buckets.Length - 1); - ref int targetBucket = ref Unsafe.Add(ref bucketsRef, (IntPtr)(void*)(uint)bucketIndex); - ref MapEntry targetMapEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex); - ref HeapEntry targetHeapEntry = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)heapIndex); + ref int targetBucket = ref Unsafe.Add(ref bucketsRef, (nint)(uint)bucketIndex); + ref MapEntry targetMapEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex); + ref HeapEntry targetHeapEntry = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)heapIndex); // Assign the values in the new map entry targetMapEntry.HashCode = hashcode; @@ -616,7 +616,7 @@ private unsafe void Insert(string value, int hashcode) /// The index of the target map node to remove. /// The input instance needs to already exist in the map. [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe void Remove(int hashcode, int mapIndex) + private void Remove(int hashcode, int mapIndex) { ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference(); int @@ -628,7 +628,7 @@ private unsafe void Remove(int hashcode, int mapIndex) // value we're looking for is guaranteed to be present while (true) { - ref MapEntry candidate = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex); + ref MapEntry candidate = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex); // Check the current value for a match if (entryIndex == mapIndex) @@ -636,7 +636,7 @@ private unsafe void Remove(int hashcode, int mapIndex) // If this was not the first list node, update the parent as well if (lastIndex != EndOfList) { - ref MapEntry lastEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)lastIndex); + ref MapEntry lastEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)lastIndex); lastEntry.NextIndex = candidate.NextIndex; } @@ -662,14 +662,14 @@ private unsafe void Remove(int hashcode, int mapIndex) /// /// The index of the target heap node to update. [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe void UpdateTimestamp(ref int heapIndex) + private void UpdateTimestamp(ref int heapIndex) { int currentIndex = heapIndex, count = this.count; ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference(); ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference(); - ref HeapEntry root = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)currentIndex); + ref HeapEntry root = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)currentIndex); uint timestamp = this.timestamp; // Check if incrementing the current timestamp for the heap node to update @@ -721,7 +721,7 @@ private unsafe void UpdateTimestamp(ref int heapIndex) // Check and update the left child, if necessary if (left < count) { - ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)left); + ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)left); if (child.Timestamp < minimum.Timestamp) { @@ -733,7 +733,7 @@ private unsafe void UpdateTimestamp(ref int heapIndex) // Same check as above for the right child if (right < count) { - ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)right); + ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)right); if (child.Timestamp < minimum.Timestamp) { @@ -752,8 +752,8 @@ private unsafe void UpdateTimestamp(ref int heapIndex) } // Update the indices in the respective map entries (accounting for the swap) - Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)root.MapIndex).HeapIndex = targetIndex; - Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)minimum.MapIndex).HeapIndex = currentIndex; + Unsafe.Add(ref mapEntriesRef, (nint)(uint)root.MapIndex).HeapIndex = targetIndex; + Unsafe.Add(ref mapEntriesRef, (nint)(uint)minimum.MapIndex).HeapIndex = currentIndex; currentIndex = targetIndex; @@ -764,7 +764,7 @@ private unsafe void UpdateTimestamp(ref int heapIndex) minimum = temp; // Update the reference to the root node - root = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)currentIndex); + root = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)currentIndex); } Fallback: @@ -787,14 +787,14 @@ private unsafe void UpdateTimestamp(ref int heapIndex) /// a given number of nodes, those are all contiguous from the start of the array. /// [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe void UpdateAllTimestamps() + private void UpdateAllTimestamps() { int count = this.count; ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference(); for (int i = 0; i < count; i++) { - Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)i).Timestamp = (uint)i; + Unsafe.Add(ref heapEntriesRef, (nint)(uint)i).Timestamp = (uint)i; } } } diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs deleted file mode 100644 index 216ec3d5939..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Diagnostics; - -namespace Microsoft.Toolkit.HighPerformance.Buffers.Views -{ - /// - /// A debug proxy used to display items for the type. - /// - /// The type of items stored in the input instances. - internal sealed class ArrayPoolBufferWriterDebugView - { - /// - /// Initializes a new instance of the class with the specified parameters. - /// - /// The input instance with the items to display. - public ArrayPoolBufferWriterDebugView(ArrayPoolBufferWriter? arrayPoolBufferWriter) - { - this.Items = arrayPoolBufferWriter?.WrittenSpan.ToArray(); - } - - /// - /// Gets the items to display for the current instance - /// - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[]? Items { get; } - } -} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryBufferWriterDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryBufferWriterDebugView{T}.cs deleted file mode 100644 index b034647d86d..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryBufferWriterDebugView{T}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Diagnostics; - -namespace Microsoft.Toolkit.HighPerformance.Buffers.Views -{ - /// - /// A debug proxy used to display items for the type. - /// - /// The type of items stored in the input instances. - internal sealed class MemoryBufferWriterDebugView - { - /// - /// Initializes a new instance of the class with the specified parameters. - /// - /// The input instance with the items to display. - public MemoryBufferWriterDebugView(MemoryBufferWriter? memoryBufferWriter) - { - this.Items = memoryBufferWriter?.WrittenSpan.ToArray(); - } - - /// - /// Gets the items to display for the current instance - /// - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[]? Items { get; } - } -} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryDebugView{T}.cs new file mode 100644 index 00000000000..8dcfd81a19d --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryDebugView{T}.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics; + +namespace Microsoft.Toolkit.HighPerformance.Buffers.Views +{ + /// + /// A debug proxy used to display items in a 1D layout. + /// + /// The type of items to display. + internal sealed class MemoryDebugView + { + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(ArrayPoolBufferWriter? arrayPoolBufferWriter) + { + this.Items = arrayPoolBufferWriter?.WrittenSpan.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(MemoryBufferWriter? memoryBufferWriter) + { + this.Items = memoryBufferWriter?.WrittenSpan.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(MemoryOwner? memoryOwner) + { + this.Items = memoryOwner?.Span.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView(SpanOwner spanOwner) + { + this.Items = spanOwner.Span.ToArray(); + } + + /// + /// Gets the items to display for the current instance + /// + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public T[]? Items { get; } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryOwnerDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryOwnerDebugView{T}.cs deleted file mode 100644 index 980115fa426..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryOwnerDebugView{T}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Diagnostics; - -namespace Microsoft.Toolkit.HighPerformance.Buffers.Views -{ - /// - /// A debug proxy used to display items for the type. - /// - /// The type of items stored in the input instances. - internal sealed class MemoryOwnerDebugView - { - /// - /// Initializes a new instance of the class with the specified parameters. - /// - /// The input instance with the items to display. - public MemoryOwnerDebugView(MemoryOwner? memoryOwner) - { - this.Items = memoryOwner?.Span.ToArray(); - } - - /// - /// Gets the items to display for the current instance - /// - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[]? Items { get; } - } -} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/SpanOwnerDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/SpanOwnerDebugView{T}.cs deleted file mode 100644 index 84802700b97..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Buffers/Views/SpanOwnerDebugView{T}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Diagnostics; - -namespace Microsoft.Toolkit.HighPerformance.Buffers.Views -{ - /// - /// A debug proxy used to display items for the type. - /// - /// The type of items stored in the input instances. - internal sealed class SpanOwnerDebugView - { - /// - /// Initializes a new instance of the class with the specified parameters. - /// - /// The input instance with the items to display. - public SpanOwnerDebugView(SpanOwner spanOwner) - { - this.Items = spanOwner.Span.ToArray(); - } - - /// - /// Gets the items to display for the current instance - /// - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[]? Items { get; } - } -} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DColumnEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DColumnEnumerable{T}.cs deleted file mode 100644 index 629d5f903f8..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DColumnEnumerable{T}.cs +++ /dev/null @@ -1,213 +0,0 @@ -// 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.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; -using Microsoft.Toolkit.HighPerformance.Extensions; - -namespace Microsoft.Toolkit.HighPerformance.Enumerables -{ - /// - /// A that iterates a column in a given 2D array instance. - /// - /// The type of items to enumerate. - [EditorBrowsable(EditorBrowsableState.Never)] - public readonly ref struct Array2DColumnEnumerable - { - /// - /// The source 2D array instance. - /// - private readonly T[,] array; - - /// - /// The target column to iterate within . - /// - private readonly int column; - - /// - /// Initializes a new instance of the struct. - /// - /// The source 2D array instance. - /// The target column to iterate within . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Array2DColumnEnumerable(T[,] array, int column) - { - this.array = array; - this.column = column; - } - - /// - /// Implements the duck-typed method. - /// - /// An instance targeting the current 2D array instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() => new Enumerator(this.array, this.column); - - /// - /// Returns a array with the values in the target column. - /// - /// A array with the values in the target column. - /// - /// This method will allocate a new array, so only - /// use it if you really need to copy the target items in a new memory location. - /// - [Pure] - public T[] ToArray() - { - if ((uint)column >= (uint)this.array.GetLength(1)) - { - ThrowArgumentOutOfRangeExceptionForInvalidColumn(); - } - - int height = this.array.GetLength(0); - - T[] array = new T[height]; - - ref T r0 = ref array.DangerousGetReference(); - int i = 0; - - // Leverage the enumerator to traverse the column - foreach (T item in this) - { - Unsafe.Add(ref r0, i++) = item; - } - - return array; - } - - /// - /// An enumerator for a source 2D array instance. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ref struct Enumerator - { -#if SPAN_RUNTIME_SUPPORT - /// - /// The instance mapping the target 2D array. - /// - /// - /// In runtimes where we have support for the type, we can - /// create one from the input 2D array and use that to traverse the target column. - /// This reduces the number of operations to perform for the offsetting to the right - /// column element (we simply need to add to the offset at each - /// iteration to move down by one row), and allows us to use the fast - /// accessor instead of the slower indexer for 2D arrays, as we can then access each - /// individual item linearly, since we know the absolute offset from the base location. - /// - private readonly Span span; - - /// - /// The width of the target 2D array. - /// - private readonly int width; - - /// - /// The current absolute offset within . - /// - private int offset; -#else - /// - /// The source 2D array instance. - /// - private readonly T[,] array; - - /// - /// The target column to iterate within . - /// - private readonly int column; - - /// - /// The height of a column in . - /// - private readonly int height; - - /// - /// The current row. - /// - private int row; -#endif - - /// - /// Initializes a new instance of the struct. - /// - /// The source 2D array instance. - /// The target column to iterate within . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator(T[,] array, int column) - { - if ((uint)column >= (uint)array.GetLength(1)) - { - ThrowArgumentOutOfRangeExceptionForInvalidColumn(); - } - -#if SPAN_RUNTIME_SUPPORT - this.span = array.AsSpan(); - this.width = array.GetLength(1); - this.offset = column - this.width; -#else - this.array = array; - this.column = column; - this.height = array.GetLength(0); - this.row = -1; -#endif - } - - /// - /// Implements the duck-typed method. - /// - /// whether a new element is available, otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { -#if SPAN_RUNTIME_SUPPORT - int offset = this.offset + this.width; - - if ((uint)offset < (uint)this.span.Length) - { - this.offset = offset; - - return true; - } -#else - int row = this.row + 1; - - if (row < this.height) - { - this.row = row; - - return true; - } -#endif - return false; - } - - /// - /// Gets the duck-typed property. - /// - public ref T Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if SPAN_RUNTIME_SUPPORT - return ref this.span.DangerousGetReferenceAt(this.offset); -#else - return ref this.array[this.row, this.column]; -#endif - } - } - } - - /// - /// Throws an when the is invalid. - /// - private static void ThrowArgumentOutOfRangeExceptionForInvalidColumn() - { - throw new ArgumentOutOfRangeException(nameof(column), "The target column parameter was not valid"); - } - } -} \ No newline at end of file diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.cs deleted file mode 100644 index 5c146d5a5e8..00000000000 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if !SPAN_RUNTIME_SUPPORT - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; -using Microsoft.Toolkit.HighPerformance.Extensions; - -namespace Microsoft.Toolkit.HighPerformance.Enumerables -{ - /// - /// A that iterates a row in a given 2D array instance. - /// - /// The type of items to enumerate. - [EditorBrowsable(EditorBrowsableState.Never)] - public readonly ref struct Array2DRowEnumerable - { - /// - /// The source 2D array instance. - /// - private readonly T[,] array; - - /// - /// The target row to iterate within . - /// - private readonly int row; - - /// - /// Initializes a new instance of the struct. - /// - /// The source 2D array instance. - /// The target row to iterate within . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Array2DRowEnumerable(T[,] array, int row) - { - this.array = array; - this.row = row; - } - - /// - /// Implements the duck-typed method. - /// - /// An instance targeting the current 2D array instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() => new Enumerator(this.array, this.row); - - /// - /// Returns a array with the values in the target row. - /// - /// A array with the values in the target row. - /// - /// This method will allocate a new array, so only - /// use it if you really need to copy the target items in a new memory location. - /// - [Pure] - public T[] ToArray() - { - if ((uint)row >= (uint)this.array.GetLength(0)) - { - ThrowArgumentOutOfRangeExceptionForInvalidRow(); - } - - int width = this.array.GetLength(1); - - T[] array = new T[width]; - - for (int i = 0; i < width; i++) - { - array.DangerousGetReferenceAt(i) = this.array.DangerousGetReferenceAt(this.row, i); - } - - return array; - } - - /// - /// An enumerator for a source 2D array instance. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ref struct Enumerator - { - /// - /// The source 2D array instance. - /// - private readonly T[,] array; - - /// - /// The target row to iterate within . - /// - private readonly int row; - - /// - /// The width of a row in . - /// - private readonly int width; - - /// - /// The current column. - /// - private int column; - - /// - /// Initializes a new instance of the struct. - /// - /// The source 2D array instance. - /// The target row to iterate within . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator(T[,] array, int row) - { - if ((uint)row >= (uint)array.GetLength(0)) - { - ThrowArgumentOutOfRangeExceptionForInvalidRow(); - } - - this.array = array; - this.row = row; - this.width = array.GetLength(1); - this.column = -1; - } - - /// - /// Implements the duck-typed method. - /// - /// whether a new element is available, otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - int column = this.column + 1; - - if (column < this.width) - { - this.column = column; - - return true; - } - - return false; - } - - /// - /// Gets the duck-typed property. - /// - public ref T Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - // This type is never used on .NET Core runtimes, where - // the fast indexer is available. Therefore, we can just - // use the built-in indexer for 2D arrays to access the value. - return ref this.array[this.row, this.column]; - } - } - } - - /// - /// Throws an when the is invalid. - /// - private static void ThrowArgumentOutOfRangeExceptionForInvalidRow() - { - throw new ArgumentOutOfRangeException(nameof(row), "The target row parameter was not valid"); - } - } -} - -#endif diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable{T}.cs new file mode 100644 index 00000000000..2eff0897662 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable{T}.cs @@ -0,0 +1,371 @@ +// 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; +#if SPAN_RUNTIME_SUPPORT +using System.Runtime.InteropServices; +#endif +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +#if !SPAN_RUNTIME_SUPPORT +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that iterates readonly items from arbitrary memory locations. + /// + /// The type of items to enumerate. + public readonly ref struct ReadOnlyRefEnumerable + { +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + /// The field maps to the total available length. + private readonly ReadOnlySpan span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The total available length for the sequence. + /// + private readonly int length; +#endif + + /// + /// The distance between items in the sequence to enumerate. + /// + /// The distance refers to items, not byte offset. + private readonly int step; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The instance pointing to the first item in the target memory area. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlyRefEnumerable(ReadOnlySpan span, int step) + { + this.span = span; + this.step = step; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A reference to the first item of the sequence. + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlyRefEnumerable(in T reference, int length, int step) + { + this.span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(reference), length); + this.step = step; + } +#else + /// + /// Initializes a new instance of the struct. + /// + /// The target instance. + /// The initial offset within . + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlyRefEnumerable(object? instance, IntPtr offset, int length, int step) + { + this.instance = instance; + this.offset = offset; + this.length = length; + this.step = step; + } +#endif + + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() + { +#if SPAN_RUNTIME_SUPPORT + return new Enumerator(this.span, this.step); +#else + return new Enumerator(this.instance, this.offset, this.length, this.step); +#endif + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(RefEnumerable destination) + { +#if SPAN_RUNTIME_SUPPORT + if (this.step == 1) + { + destination.CopyFrom(this.span); + + return; + } + + if (destination.Step == 1) + { + CopyTo(destination.Span); + + return; + } + + ref T sourceRef = ref this.span.DangerousGetReference(); + ref T destinationRef = ref destination.Span.DangerousGetReference(); + int + sourceLength = this.span.Length, + destinationLength = destination.Span.Length; +#else + ref T sourceRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); + ref T destinationRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(destination.Instance, destination.Offset); + int + sourceLength = this.length, + destinationLength = destination.Length; +#endif + + if ((uint)destinationLength < (uint)sourceLength) + { + ThrowArgumentExceptionForDestinationTooShort(); + } + + RefEnumerableHelper.CopyTo(ref sourceRef, ref destinationRef, (nint)(uint)sourceLength, (nint)(uint)this.step, (nint)(uint)destination.Step); + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(RefEnumerable destination) + { +#if SPAN_RUNTIME_SUPPORT + int + sourceLength = this.span.Length, + destinationLength = destination.Span.Length; +#else + int + sourceLength = this.length, + destinationLength = destination.Length; +#endif + + if (destinationLength >= sourceLength) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { +#if SPAN_RUNTIME_SUPPORT + if (this.step == 1) + { + this.span.CopyTo(destination); + + return; + } + + ref T sourceRef = ref this.span.DangerousGetReference(); + int length = this.span.Length; +#else + ref T sourceRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); + int length = this.length; +#endif + if ((uint)destination.Length < (uint)length) + { + ThrowArgumentExceptionForDestinationTooShort(); + } + + ref T destinationRef = ref destination.DangerousGetReference(); + + RefEnumerableHelper.CopyTo(ref sourceRef, ref destinationRef, (nint)(uint)length, (nint)(uint)this.step); + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span destination) + { +#if SPAN_RUNTIME_SUPPORT + int length = this.span.Length; +#else + int length = this.length; +#endif + + if (destination.Length >= length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + [Pure] + public T[] ToArray() + { +#if SPAN_RUNTIME_SUPPORT + int length = this.span.Length; +#else + int length = this.length; +#endif + + // Empty array if no data is mapped + if (length == 0) + { + return Array.Empty(); + } + + T[] array = new T[length]; + + CopyTo(array); + + return array; + } + + /// + /// Implicitly converts a instance into a one. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlyRefEnumerable(RefEnumerable enumerable) + { +#if SPAN_RUNTIME_SUPPORT + return new ReadOnlyRefEnumerable(enumerable.Span, enumerable.Step); +#else + return new ReadOnlyRefEnumerable(enumerable.Instance, enumerable.Offset, enumerable.Length, enumerable.Step); +#endif + } + + /// + /// A custom enumerator type to traverse items within a instance. + /// + public ref struct Enumerator + { +#if SPAN_RUNTIME_SUPPORT + /// + private readonly ReadOnlySpan span; +#else + /// + private readonly object? instance; + + /// + private readonly IntPtr offset; + + /// + private readonly int length; +#endif + + /// + private readonly int step; + + /// + /// The current position in the sequence. + /// + private int position; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The instance with the info on the items to traverse. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(ReadOnlySpan span, int step) + { + this.span = span; + this.step = step; + this.position = -1; + } +#else + /// + /// Initializes a new instance of the struct. + /// + /// The target instance. + /// The initial offset within . + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(object? instance, IntPtr offset, int length, int step) + { + this.instance = instance; + this.offset = offset; + this.length = length; + this.step = step; + this.position = -1; + } +#endif + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { +#if SPAN_RUNTIME_SUPPORT + return ++this.position < this.span.Length; +#else + return ++this.position < this.length; +#endif + } + + /// + public readonly ref readonly T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref this.span.DangerousGetReference(); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint offset = (nint)(uint)this.position * (nint)(uint)this.step; + ref T ri = ref Unsafe.Add(ref r0, offset); + + return ref ri; + } + } + } + + /// + /// Throws an when the target span is too short. + /// + private static void ThrowArgumentExceptionForDestinationTooShort() + { + throw new ArgumentException("The target span is too short to copy all the current items to"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs index c732861d670..6a0724ed01e 100644 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs @@ -54,16 +54,7 @@ public ReadOnlySpanEnumerable(ReadOnlySpan span) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - int newIndex = this.index + 1; - - if (newIndex < this.span.Length) - { - this.index = newIndex; - - return true; - } - - return false; + return ++this.index < this.span.Length; } /// @@ -76,7 +67,7 @@ public readonly Item Current { #if SPAN_RUNTIME_SUPPORT ref T r0 = ref MemoryMarshal.GetReference(this.span); - ref T ri = ref Unsafe.Add(ref r0, this.index); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); // See comment in SpanEnumerable about this return new Item(ref ri, this.index); @@ -139,7 +130,7 @@ public ref readonly T Value return ref MemoryMarshal.GetReference(this.span); #else ref T r0 = ref MemoryMarshal.GetReference(this.span); - ref T ri = ref Unsafe.Add(ref r0, this.index); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); return ref ri; #endif diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs index 37bc6168f10..a4a4dd7c4aa 100644 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs @@ -43,6 +43,7 @@ public ref struct ReadOnlySpanTokenizer /// /// The source instance. /// The separator item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpanTokenizer(ReadOnlySpan span, T separator) { this.span = span; diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/RefEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/RefEnumerable{T}.cs new file mode 100644 index 00000000000..fae37dc76fc --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/RefEnumerable{T}.cs @@ -0,0 +1,464 @@ +// 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; +#if SPAN_RUNTIME_SUPPORT +using System.Runtime.InteropServices; +#endif +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +#if !SPAN_RUNTIME_SUPPORT +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that iterates items from arbitrary memory locations. + /// + /// The type of items to enumerate. + public readonly ref struct RefEnumerable + { +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + /// The field maps to the total available length. + internal readonly Span Span; +#else + /// + /// The target instance, if present. + /// + internal readonly object? Instance; + + /// + /// The initial offset within . + /// + internal readonly IntPtr Offset; + + /// + /// The total available length for the sequence. + /// + internal readonly int Length; +#endif + + /// + /// The distance between items in the sequence to enumerate. + /// + /// The distance refers to items, not byte offset. + internal readonly int Step; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// A reference to the first item of the sequence. + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal RefEnumerable(ref T reference, int length, int step) + { + Span = MemoryMarshal.CreateSpan(ref reference, length); + Step = step; + } +#else + /// + /// Initializes a new instance of the struct. + /// + /// The target instance. + /// The initial offset within . + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal RefEnumerable(object? instance, IntPtr offset, int length, int step) + { + Instance = instance; + Offset = offset; + Length = length; + Step = step; + } +#endif + + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() + { +#if SPAN_RUNTIME_SUPPORT + return new Enumerator(this.Span, this.Step); +#else + return new Enumerator(this.Instance, this.Offset, this.Length, this.Step); +#endif + } + + /// + /// Clears the contents of the current instance. + /// + public void Clear() + { +#if SPAN_RUNTIME_SUPPORT + // Fast path for contiguous items + if (this.Step == 1) + { + this.Span.Clear(); + + return; + } + + ref T r0 = ref this.Span.DangerousGetReference(); + int length = this.Span.Length; +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); + int length = this.Length; +#endif + + RefEnumerableHelper.Clear(ref r0, (nint)(uint)length, (nint)(uint)this.Step); + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(RefEnumerable destination) + { +#if SPAN_RUNTIME_SUPPORT + if (this.Step == 1) + { + destination.CopyFrom(this.Span); + + return; + } + + if (destination.Step == 1) + { + CopyTo(destination.Span); + + return; + } + + ref T sourceRef = ref this.Span.DangerousGetReference(); + ref T destinationRef = ref destination.Span.DangerousGetReference(); + int + sourceLength = this.Span.Length, + destinationLength = destination.Span.Length; +#else + ref T sourceRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); + ref T destinationRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(destination.Instance, destination.Offset); + int + sourceLength = this.Length, + destinationLength = destination.Length; +#endif + + if ((uint)destinationLength < (uint)sourceLength) + { + ThrowArgumentExceptionForDestinationTooShort(); + } + + RefEnumerableHelper.CopyTo(ref sourceRef, ref destinationRef, (nint)(uint)sourceLength, (nint)(uint)this.Step, (nint)(uint)destination.Step); + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(RefEnumerable destination) + { +#if SPAN_RUNTIME_SUPPORT + int + sourceLength = this.Span.Length, + destinationLength = destination.Span.Length; +#else + int + sourceLength = this.Length, + destinationLength = destination.Length; +#endif + + if (destinationLength >= sourceLength) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { +#if SPAN_RUNTIME_SUPPORT + if (this.Step == 1) + { + this.Span.CopyTo(destination); + + return; + } + + ref T sourceRef = ref this.Span.DangerousGetReference(); + int length = this.Span.Length; +#else + ref T sourceRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); + int length = this.Length; +#endif + if ((uint)destination.Length < (uint)length) + { + ThrowArgumentExceptionForDestinationTooShort(); + } + + ref T destinationRef = ref destination.DangerousGetReference(); + + RefEnumerableHelper.CopyTo(ref sourceRef, ref destinationRef, (nint)(uint)length, (nint)(uint)this.Step); + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span destination) + { +#if SPAN_RUNTIME_SUPPORT + int length = this.Span.Length; +#else + int length = this.Length; +#endif + + if (destination.Length >= length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Copies the contents of a source into the current instance. + /// + /// The source instance. + /// + /// Thrown when the current is shorter than the source instance. + /// + internal void CopyFrom(ReadOnlySpan source) + { +#if SPAN_RUNTIME_SUPPORT + if (this.Step == 1) + { + source.CopyTo(this.Span); + + return; + } + + ref T destinationRef = ref this.Span.DangerousGetReference(); + int destinationLength = this.Span.Length; +#else + ref T destinationRef = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); + int destinationLength = this.Length; +#endif + ref T sourceRef = ref source.DangerousGetReference(); + int sourceLength = source.Length; + + if ((uint)destinationLength < (uint)sourceLength) + { + ThrowArgumentExceptionForDestinationTooShort(); + } + + RefEnumerableHelper.CopyFrom(ref sourceRef, ref destinationRef, (nint)(uint)sourceLength, (nint)(uint)this.Step); + } + + /// + /// Attempts to copy the source into the current instance. + /// + /// The source instance. + /// Whether or not the operation was successful. + public bool TryCopyFrom(ReadOnlySpan source) + { +#if SPAN_RUNTIME_SUPPORT + int length = this.Span.Length; +#else + int length = this.Length; +#endif + + if (length >= source.Length) + { + CopyFrom(source); + + return true; + } + + return false; + } + + /// + /// Fills the elements of this with a specified value. + /// + /// The value to assign to each element of the instance. + public void Fill(T value) + { +#if SPAN_RUNTIME_SUPPORT + if (this.Step == 1) + { + this.Span.Fill(value); + + return; + } + + ref T r0 = ref this.Span.DangerousGetReference(); + int length = this.Span.Length; +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); + int length = this.Length; +#endif + + RefEnumerableHelper.Fill(ref r0, (nint)(uint)length, (nint)(uint)this.Step, value); + } + + /// + /// Returns a array with the values in the target row. + /// + /// A array with the values in the target row. + /// + /// This method will allocate a new array, so only + /// use it if you really need to copy the target items in a new memory location. + /// + [Pure] + public T[] ToArray() + { +#if SPAN_RUNTIME_SUPPORT + int length = this.Span.Length; +#else + int length = this.Length; +#endif + + // Empty array if no data is mapped + if (length == 0) + { + return Array.Empty(); + } + + T[] array = new T[length]; + + CopyTo(array); + + return array; + } + + /// + /// A custom enumerator type to traverse items within a instance. + /// + public ref struct Enumerator + { +#if SPAN_RUNTIME_SUPPORT + /// + private readonly Span span; +#else + /// + private readonly object? instance; + + /// + private readonly IntPtr offset; + + /// + private readonly int length; +#endif + + /// + private readonly int step; + + /// + /// The current position in the sequence. + /// + private int position; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The instance with the info on the items to traverse. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(Span span, int step) + { + this.span = span; + this.step = step; + this.position = -1; + } +#else + /// + /// Initializes a new instance of the struct. + /// + /// The target instance. + /// The initial offset within . + /// The number of items in the sequence. + /// The distance between items in the sequence to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(object? instance, IntPtr offset, int length, int step) + { + this.instance = instance; + this.offset = offset; + this.length = length; + this.step = step; + this.position = -1; + } +#endif + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { +#if SPAN_RUNTIME_SUPPORT + return ++this.position < this.span.Length; +#else + return ++this.position < this.length; +#endif + } + + /// + public readonly ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref this.span.DangerousGetReference(); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + + // Here we just offset by shifting down as if we were traversing a 2D array with a + // a single column, with the width of each row represented by the step, the height + // represented by the current position, and with only the first element of each row + // being inspected. We can perform all the indexing operations in this type as nint, + // as the maximum offset is guaranteed never to exceed the maximum value, since on + // 32 bit architectures it's not possible to allocate that much memory anyway. + nint offset = (nint)(uint)this.position * (nint)(uint)this.step; + ref T ri = ref Unsafe.Add(ref r0, offset); + + return ref ri; + } + } + } + + /// + /// Throws an when the target span is too short. + /// + private static void ThrowArgumentExceptionForDestinationTooShort() + { + throw new ArgumentException("The target span is too short to copy all the current items to"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs index 23cbffac668..618f1419ace 100644 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs @@ -54,16 +54,7 @@ public SpanEnumerable(Span span) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - int newIndex = this.index + 1; - - if (newIndex < this.span.Length) - { - this.index = newIndex; - - return true; - } - - return false; + return ++this.index < this.span.Length; } /// @@ -76,7 +67,7 @@ public readonly Item Current { #if SPAN_RUNTIME_SUPPORT ref T r0 = ref MemoryMarshal.GetReference(this.span); - ref T ri = ref Unsafe.Add(ref r0, this.index); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); // On .NET Standard 2.1 and .NET Core (or on any target that offers runtime // support for the Span types), we can save 4 bytes by piggybacking the @@ -144,7 +135,7 @@ public ref T Value return ref MemoryMarshal.GetReference(this.span); #else ref T r0 = ref MemoryMarshal.GetReference(this.span); - ref T ri = ref Unsafe.Add(ref r0, this.index); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)this.index); return ref ri; #endif diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs index da8a9dce040..b5673f2e1c4 100644 --- a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs @@ -43,6 +43,7 @@ public ref struct SpanTokenizer /// /// The source instance. /// The separator item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SpanTokenizer(Span span, T separator) { this.span = span; diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.1D.cs similarity index 81% rename from Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs rename to Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.1D.cs index 73e3b8ed73f..887cdd0f989 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.1D.cs @@ -10,6 +10,7 @@ #endif using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -35,20 +36,9 @@ public static ref T DangerousGetReference(this T[] array) return ref r0; #else -#pragma warning disable SA1131 // Inverted comparison to remove JIT bounds check - // Checking the length of the array like so allows the JIT - // to skip its own bounds check, which results in the element - // access below to be executed without branches. - if (0u < (uint)array.Length) - { - return ref array[0]; - } + IntPtr offset = RuntimeHelpers.GetArrayDataByteOffset(); - unsafe - { - return ref Unsafe.AsRef(null); - } -#pragma warning restore SA1131 + return ref array.DangerousGetObjectDataReferenceAt(offset); #endif } @@ -62,21 +52,20 @@ public static ref T DangerousGetReference(this T[] array) /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref T DangerousGetReferenceAt(this T[] array, int i) + public static ref T DangerousGetReferenceAt(this T[] array, int i) { #if NETCORE_RUNTIME var arrayData = Unsafe.As(array); ref T r0 = ref Unsafe.As(ref arrayData.Data); - ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); return ref ri; #else - if ((uint)i < (uint)array.Length) - { - return ref array[i]; - } + IntPtr offset = RuntimeHelpers.GetArrayDataByteOffset(); + ref T r0 = ref array.DangerousGetObjectDataReferenceAt(offset); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); - return ref Unsafe.AsRef(null); + return ref ri; #endif } @@ -111,13 +100,20 @@ private sealed class RawArrayData /// The number of occurrences of in . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Count(this T[] array, T value) + public static int Count(this T[] array, T value) where T : IEquatable { ref T r0 = ref array.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)array.Length; + nint + length = RuntimeHelpers.GetArrayNativeLength(array), + count = SpanHelper.Count(ref r0, length, value); + + if ((nuint)count > int.MaxValue) + { + ThrowOverflowException(); + } - return SpanHelper.Count(ref r0, length, value); + return (int)count; } /// @@ -182,13 +178,34 @@ public static SpanTokenizer Tokenize(this T[] array, T separator) /// The Djb2 hash is fully deterministic and with no random components. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int GetDjb2HashCode(this T[] array) + public static int GetDjb2HashCode(this T[] array) where T : notnull { ref T r0 = ref array.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)array.Length; + nint length = RuntimeHelpers.GetArrayNativeLength(array); return SpanHelper.GetDjb2HashCode(ref r0, length); } + + /// + /// Checks whether or not a given array is covariant. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// Whether or not is covariant. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCovariant(this T[] array) + { + return default(T) is null && array.GetType() != typeof(T[]); + } + + /// + /// Throws an when the "column" parameter is invalid. + /// + public static void ThrowOverflowException() + { + throw new OverflowException(); + } } } diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs index a7a4baa075d..8807c558b33 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs @@ -4,13 +4,15 @@ using System; using System.Diagnostics.Contracts; -using System.Drawing; using System.Runtime.CompilerServices; #if SPAN_RUNTIME_SUPPORT using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; #endif using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +using Microsoft.Toolkit.HighPerformance.Memory; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -36,17 +38,9 @@ public static ref T DangerousGetReference(this T[,] array) return ref r0; #else -#pragma warning disable SA1131 // Inverted comparison to remove JIT bounds check - if (0u < (uint)array.Length) - { - return ref array[0, 0]; - } + IntPtr offset = RuntimeHelpers.GetArray2DDataByteOffset(); - unsafe - { - return ref Unsafe.AsRef(null); - } -#pragma warning restore SA1131 + return ref array.DangerousGetObjectDataReferenceAt(offset); #endif } @@ -66,23 +60,23 @@ public static ref T DangerousGetReference(this T[,] array) /// [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref T DangerousGetReferenceAt(this T[,] array, int i, int j) + public static ref T DangerousGetReferenceAt(this T[,] array, int i, int j) { #if NETCORE_RUNTIME var arrayData = Unsafe.As(array); - int offset = (i * arrayData.Width) + j; + nint offset = ((nint)(uint)i * (nint)(uint)arrayData.Width) + (nint)(uint)j; ref T r0 = ref Unsafe.As(ref arrayData.Data); - ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)offset); + ref T ri = ref Unsafe.Add(ref r0, offset); return ref ri; #else - if ((uint)i < (uint)array.GetLength(0) && - (uint)j < (uint)array.GetLength(1)) - { - return ref array[i, j]; - } + int width = array.GetLength(1); + nint index = ((nint)(uint)i * (nint)(uint)width) + (nint)(uint)j; + IntPtr offset = RuntimeHelpers.GetArray2DDataByteOffset(); + ref T r0 = ref array.DangerousGetObjectDataReferenceAt(offset); + ref T ri = ref Unsafe.Add(ref r0, index); - return ref Unsafe.AsRef(null); + return ref ri; #endif } @@ -112,95 +106,46 @@ private sealed class RawArray2DData #endif /// - /// Fills an area in a given 2D array instance with a specified value. - /// This API will try to fill as many items as possible, ignoring positions outside the bounds of the array. - /// If invalid coordinates are given, they will simply be ignored and no exception will be thrown. - /// - /// The type of elements in the input 2D array instance. - /// The input array instance. - /// The value to fill the target area with. - /// The row to start on (inclusive, 0-based index). - /// The column to start on (inclusive, 0-based index). - /// The positive width of area to fill. - /// The positive height of area to fill. - public static void Fill(this T[,] array, T value, int row, int column, int width, int height) - { - Rectangle bounds = new Rectangle(0, 0, array.GetLength(1), array.GetLength(0)); - - // Precompute bounds to skip branching in main loop - bounds.Intersect(new Rectangle(column, row, width, height)); - - for (int i = bounds.Top; i < bounds.Bottom; i++) - { -#if SPAN_RUNTIME_SUPPORT -#if NETCORE_RUNTIME - ref T r0 = ref array.DangerousGetReferenceAt(i, bounds.Left); -#else - ref T r0 = ref array[i, bounds.Left]; -#endif - - // Span.Fill will use vectorized instructions when possible - MemoryMarshal.CreateSpan(ref r0, bounds.Width).Fill(value); -#else - ref T r0 = ref array[i, bounds.Left]; - - for (int j = 0; j < bounds.Width; j++) - { - // Storing the initial reference and only incrementing - // that one in each iteration saves one additional indirect - // dereference for every loop iteration compared to using - // the DangerousGetReferenceAt extension on the array. - Unsafe.Add(ref r0, j) = value; - } -#endif - } - } - - /// - /// Returns a over a row in a given 2D array instance. + /// Returns a over a row in a given 2D array instance. /// /// The type of elements in the input 2D array instance. /// The input array instance. /// The target row to retrieve (0-based index). - /// A with the items from the target row within . + /// A with the items from the target row within . + /// The returned value shouldn't be used directly: use this extension in a loop. + /// Thrown when one of the input parameters is out of range. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static -#if SPAN_RUNTIME_SUPPORT - Span -#else - // .NET Standard 2.0 lacks MemoryMarshal.CreateSpan(ref T, int), - // which is necessary to create arbitrary Span-s over a 2D array. - // To work around this, we use a custom ref struct enumerator, - // which makes the lack of that API completely transparent to the user. - // If a user then moves from .NET Standard 2.0 to 2.1, all the previous - // features will be perfectly supported, and in addition to that it will - // also gain the ability to use the Span value elsewhere. - // The only case where this would be a breaking change for a user upgrading - // the target framework is when the returned enumerator type is used directly, - // but since that's specifically discouraged from the docs, we don't - // need to worry about that scenario in particular, as users doing that - // would be willingly go against the recommended usage of this API. - Array2DRowEnumerable -#endif - GetRow(this T[,] array, int row) + public static RefEnumerable GetRow(this T[,] array, int row) { - if ((uint)row >= (uint)array.GetLength(0)) + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + int height = array.GetLength(0); + + if ((uint)row >= (uint)height) { - throw new ArgumentOutOfRangeException(nameof(row)); + ThrowArgumentOutOfRangeExceptionForRow(); } + int width = array.GetLength(1); + #if SPAN_RUNTIME_SUPPORT ref T r0 = ref array.DangerousGetReferenceAt(row, 0); - return MemoryMarshal.CreateSpan(ref r0, array.GetLength(1)); + return new RefEnumerable(ref r0, width, 1); #else - return new Array2DRowEnumerable(array, row); + ref T r0 = ref array.DangerousGetReferenceAt(row, 0); + IntPtr offset = array.DangerousGetObjectDataByteOffset(ref r0); + + return new RefEnumerable(array, offset, width, 1); #endif } /// - /// Returns an enumerable that returns the items from a given column in a given 2D array instance. + /// Returns a that returns the items from a given column in a given 2D array instance. /// This extension should be used directly within a loop: /// /// int[,] matrix = @@ -221,15 +166,196 @@ public static /// The input array instance. /// The target column to retrieve (0-based index). /// A wrapper type that will handle the column enumeration for . - /// The returned value shouldn't be used directly: use this extension in a loop. + /// The returned value shouldn't be used directly: use this extension in a loop. + /// Thrown when one of the input parameters is out of range. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RefEnumerable GetColumn(this T[,] array, int column) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + int width = array.GetLength(1); + + if ((uint)column >= (uint)width) + { + ThrowArgumentOutOfRangeExceptionForColumn(); + } + + int height = array.GetLength(0); + +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref array.DangerousGetReferenceAt(0, column); + + return new RefEnumerable(ref r0, height, width); +#else + ref T r0 = ref array.DangerousGetReferenceAt(0, column); + IntPtr offset = array.DangerousGetObjectDataByteOffset(ref r0); + + return new RefEnumerable(array, offset, height, width); +#endif + } + + /// + /// Creates a new over an input 2D array. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span2D AsSpan2D(this T[,]? array) + { + return new Span2D(array); + } + + /// + /// Creates a new over an input 2D array. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span2D AsSpan2D(this T[,]? array, int row, int column, int height, int width) + { + return new Span2D(array, row, column, height, width); + } + + /// + /// Creates a new over an input 2D array. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// A instance with the values of . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Array2DColumnEnumerable GetColumn(this T[,] array, int column) + public static Memory2D AsMemory2D(this T[,]? array) { - return new Array2DColumnEnumerable(array, column); + return new Memory2D(array); + } + + /// + /// Creates a new over an input 2D array. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this T[,]? array, int row, int column, int height, int width) + { + return new Memory2D(array, row, column, height, width); } #if SPAN_RUNTIME_SUPPORT + /// + /// Returns a over a row in a given 2D array instance. + /// + /// The type of elements in the input 2D array instance. + /// The input array instance. + /// The target row to retrieve (0-based index). + /// A with the items from the target row within . + /// Thrown when doesn't match . + /// Thrown when is invalid. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetRowSpan(this T[,] array, int row) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)row >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(row, 0); + + return MemoryMarshal.CreateSpan(ref r0, array.GetLength(1)); + } + + /// + /// Returns a over a row in a given 2D array instance. + /// + /// The type of elements in the input 2D array instance. + /// The input array instance. + /// The target row to retrieve (0-based index). + /// A with the items from the target row within . + /// Thrown when doesn't match . + /// Thrown when is invalid. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory GetRowMemory(this T[,] array, int row) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)row >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(row, 0); + IntPtr offset = array.DangerousGetObjectDataByteOffset(ref r0); + + return new RawObjectMemoryManager(array, offset, array.GetLength(1)).Memory; + } + + /// + /// Creates a new over an input 2D array. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory AsMemory(this T[,]? array) + { + if (array is null) + { + return default; + } + + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + IntPtr offset = RuntimeHelpers.GetArray2DDataByteOffset(); + int length = array.Length; + + return new RawObjectMemoryManager(array, offset, length).Memory; + } + /// /// Creates a new over an input 2D array. /// @@ -238,26 +364,21 @@ public static Array2DColumnEnumerable GetColumn(this T[,] array, int colum /// A instance with the values of . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Span AsSpan(this T[,] array) + public static Span AsSpan(this T[,]? array) { -#if NETCORE_RUNTIME - var arrayData = Unsafe.As(array); + if (array is null) + { + return default; + } - // On x64, the length is padded to x64, but it is represented in memory - // as two sequential uint fields (one of which is padding). - // So we can just reinterpret a reference to the IntPtr as one of type - // uint, to access the first 4 bytes of that field, regardless of whether - // we're running in a 32 or 64 bit process. This will work when on little - // endian systems as well, as the memory layout for fields is the same, - // the only difference is the order of bytes within each field of a given type. - // We use checked here to follow suit with the CoreCLR source, where an - // invalid value here should fail to perform the cast and throw an exception. - int length = checked((int)Unsafe.As(ref arrayData.Length)); - ref T r0 = ref Unsafe.As(ref arrayData.Data); -#else + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + ref T r0 = ref array.DangerousGetReference(); int length = array.Length; - ref T r0 = ref array[0, 0]; -#endif + return MemoryMarshal.CreateSpan(ref r0, length); } #endif @@ -275,9 +396,16 @@ public static unsafe int Count(this T[,] array, T value) where T : IEquatable { ref T r0 = ref array.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)array.Length; + nint + length = RuntimeHelpers.GetArrayNativeLength(array), + count = SpanHelper.Count(ref r0, length, value); + + if ((nuint)count > int.MaxValue) + { + ThrowOverflowException(); + } - return SpanHelper.Count(ref r0, length, value); + return (int)count; } /// @@ -294,9 +422,46 @@ public static unsafe int GetDjb2HashCode(this T[,] array) where T : notnull { ref T r0 = ref array.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)array.Length; + nint length = RuntimeHelpers.GetArrayNativeLength(array); return SpanHelper.GetDjb2HashCode(ref r0, length); } + + /// + /// Checks whether or not a given array is covariant. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// Whether or not is covariant. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCovariant(this T[,] array) + { + return default(T) is null && array.GetType() != typeof(T[,]); + } + + /// + /// Throws an when using an array of an invalid type. + /// + private static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException("The given array doesn't match the specified type T"); + } + + /// + /// Throws an when the "row" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForRow() + { + throw new ArgumentOutOfRangeException("row"); + } + + /// + /// Throws an when the "column" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForColumn() + { + throw new ArgumentOutOfRangeException("column"); + } } } diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs new file mode 100644 index 00000000000..846e3c5eadb --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs @@ -0,0 +1,324 @@ +// 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; +#if SPAN_RUNTIME_SUPPORT +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; +#endif +using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +using Microsoft.Toolkit.HighPerformance.Memory; +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static partial class ArrayExtensions + { + /// + /// Returns a reference to the first element within a given 3D array, with no bounds checks. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// A reference to the first element within , or the location it would have used, if is empty. + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to perform checks in case the returned value is dereferenced. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReference(this T[,,] array) + { +#if NETCORE_RUNTIME + var arrayData = Unsafe.As(array); + ref T r0 = ref Unsafe.As(ref arrayData.Data); + + return ref r0; +#else + IntPtr offset = RuntimeHelpers.GetArray3DDataByteOffset(); + + return ref array.DangerousGetObjectDataReferenceAt(offset); +#endif + } + + /// + /// Returns a reference to an element at a specified coordinate within a given 3D array, with no bounds checks. + /// + /// The type of elements in the input 3D array instance. + /// The input 2D array instance. + /// The depth index of the element to retrieve within . + /// The vertical index of the element to retrieve within . + /// The horizontal index of the element to retrieve within . + /// A reference to the element within at the coordinate specified by and . + /// + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the + /// and parameters are valid. Furthermore, this extension will ignore the lower bounds for the input + /// array, and will just assume that the input index is 0-based. It is responsability of the caller to adjust the input + /// indices to account for the actual lower bounds, if the input array has either axis not starting at 0. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReferenceAt(this T[,,] array, int i, int j, int k) + { +#if NETCORE_RUNTIME + var arrayData = Unsafe.As(array); + nint offset = + ((nint)(uint)i * (nint)(uint)arrayData.Height * (nint)(uint)arrayData.Width) + + ((nint)(uint)j * (nint)(uint)arrayData.Width) + (nint)(uint)k; + ref T r0 = ref Unsafe.As(ref arrayData.Data); + ref T ri = ref Unsafe.Add(ref r0, offset); + + return ref ri; +#else + int + height = array.GetLength(1), + width = array.GetLength(2); + nint index = + ((nint)(uint)i * (nint)(uint)height * (nint)(uint)width) + + ((nint)(uint)j * (nint)(uint)width) + (nint)(uint)k; + IntPtr offset = RuntimeHelpers.GetArray3DDataByteOffset(); + ref T r0 = ref array.DangerousGetObjectDataReferenceAt(offset); + ref T ri = ref Unsafe.Add(ref r0, index); + + return ref ri; +#endif + } + +#if NETCORE_RUNTIME + // See description for this in the 2D partial file. + // Using the CHW naming scheme here (like with RGB images). + [StructLayout(LayoutKind.Sequential)] + private sealed class RawArray3DData + { +#pragma warning disable CS0649 // Unassigned fields +#pragma warning disable SA1401 // Fields should be private + public IntPtr Length; + public int Channel; + public int Height; + public int Width; + public int ChannelLowerBound; + public int HeightLowerBound; + public int WidthLowerBound; + public byte Data; +#pragma warning restore CS0649 +#pragma warning restore SA1401 + } +#endif + +#if SPAN_RUNTIME_SUPPORT + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory AsMemory(this T[,,]? array) + { + if (array is null) + { + return default; + } + + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + IntPtr offset = RuntimeHelpers.GetArray3DDataByteOffset(); + int length = array.Length; + + return new RawObjectMemoryManager(array, offset, length).Memory; + } + + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// A instance with the values of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this T[,,]? array) + { + if (array is null) + { + return default; + } + + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + ref T r0 = ref array.DangerousGetReference(); + int length = array.Length; + + return MemoryMarshal.CreateSpan(ref r0, length); + } + + /// + /// Creates a new instance of the struct wrapping a layer in a 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The given 3D array to wrap. + /// The target layer to map within . + /// Thrown when doesn't match . + /// Thrown when is invalid. + /// A instance wrapping the target layer within . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this T[,,] array, int depth) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForDepth(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(depth, 0, 0); + int length = checked(array.GetLength(1) * array.GetLength(2)); + + return MemoryMarshal.CreateSpan(ref r0, length); + } + + /// + /// Creates a new instance of the struct wrapping a layer in a 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The given 3D array to wrap. + /// The target layer to map within . + /// Thrown when doesn't match . + /// Thrown when is invalid. + /// A instance wrapping the target layer within . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory AsMemory(this T[,,] array, int depth) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForDepth(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(depth, 0, 0); + IntPtr offset = array.DangerousGetObjectDataByteOffset(ref r0); + int length = checked(array.GetLength(1) * array.GetLength(2)); + + return new RawObjectMemoryManager(array, offset, length).Memory; + } +#endif + + /// + /// Creates a new instance of the struct wrapping a layer in a 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The given 3D array to wrap. + /// The target layer to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when either is invalid. + /// A instance wrapping the target layer within . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span2D AsSpan2D(this T[,,] array, int depth) + { + return new Span2D(array, depth); + } + + /// + /// Creates a new instance of the struct wrapping a layer in a 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The given 3D array to wrap. + /// The target layer to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when either is invalid. + /// A instance wrapping the target layer within . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this T[,,] array, int depth) + { + return new Memory2D(array, depth); + } + + /// + /// Counts the number of occurrences of a given value into a target 3D array instance. + /// + /// The type of items in the input 3D array instance. + /// The input 3D array instance. + /// The value to look for. + /// The number of occurrences of in . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Count(this T[,,] array, T value) + where T : IEquatable + { + ref T r0 = ref array.DangerousGetReference(); + nint + length = RuntimeHelpers.GetArrayNativeLength(array), + count = SpanHelper.Count(ref r0, length, value); + + if ((nuint)count > int.MaxValue) + { + ThrowOverflowException(); + } + + return (int)count; + } + + /// + /// Gets a content hash from the input 3D array instance using the Djb2 algorithm. + /// For more info, see the documentation for . + /// + /// The type of items in the input 3D array instance. + /// The input 3D array instance. + /// The Djb2 value for the input 3D array instance. + /// The Djb2 hash is fully deterministic and with no random components. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetDjb2HashCode(this T[,,] array) + where T : notnull + { + ref T r0 = ref array.DangerousGetReference(); + nint length = RuntimeHelpers.GetArrayNativeLength(array); + + return SpanHelper.GetDjb2HashCode(ref r0, length); + } + + /// + /// Checks whether or not a given array is covariant. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// Whether or not is covariant. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCovariant(this T[,,] array) + { + return default(T) is null && array.GetType() != typeof(T[,,]); + } + + /// + /// Throws an when the "depth" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForDepth() + { + throw new ArgumentOutOfRangeException("depth"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs index 19e5c258619..95040b5689c 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.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.Diagnostics.Contracts; using System.Runtime.CompilerServices; @@ -12,6 +13,19 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class BoolExtensions { + /// + /// Converts the given value into a . + /// + /// The input value to convert. + /// 1 if is , 0 otherwise. + /// This method does not contain branching instructions. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ToByte(this bool flag) + { + return Unsafe.As(ref flag); + } + /// /// Converts the given value into an . /// @@ -20,6 +34,7 @@ public static class BoolExtensions /// This method does not contain branching instructions. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use ToByte instead.")] public static int ToInt(this bool flag) { return Unsafe.As(ref flag); diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/HashCodeExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/HashCodeExtensions.cs index c4c468162fb..2dfab655d8d 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/HashCodeExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/HashCodeExtensions.cs @@ -23,12 +23,7 @@ public static class HashCodeExtensions /// The input instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Add(ref this HashCode hashCode, ReadOnlySpan span) -#if SPAN_RUNTIME_SUPPORT where T : notnull -#else - // Same type constraints as HashCode, see comments there - where T : unmanaged -#endif { int hash = HashCode.CombineValues(span); diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index 794339fbbed..e2b9d434cad 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -6,6 +6,9 @@ using System.Diagnostics.Contracts; using System.IO; using System.Runtime.CompilerServices; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Memory; +#endif using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -15,6 +18,52 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class MemoryExtensions { +#if SPAN_RUNTIME_SUPPORT + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this Memory memory, int height, int width) + { + return new Memory2D(memory, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this Memory memory, int offset, int height, int width, int pitch) + { + return new Memory2D(memory, offset, height, width, pitch); + } +#endif + /// /// Returns a wrapping the contents of the given of instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index eab50592776..0c1c6eb821f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -5,6 +5,10 @@ using System; using System.Diagnostics.Contracts; using System.IO; +using System.Runtime.CompilerServices; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Memory; +#endif using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions @@ -14,6 +18,52 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class ReadOnlyMemoryExtensions { +#if SPAN_RUNTIME_SUPPORT + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, int height, int width) + { + return new ReadOnlyMemory2D(memory, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, int offset, int height, int width, int pitch) + { + return new ReadOnlyMemory2D(memory, offset, height, width, pitch); + } +#endif + /// /// Returns a wrapping the contents of the given of instance. /// @@ -27,6 +77,7 @@ public static class ReadOnlyMemoryExtensions /// /// Thrown when has an invalid data store. [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Stream AsStream(this ReadOnlyMemory memory) { return MemoryStream.Create(memory, true); diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index 47428db40a2..b4264395133 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -8,6 +8,9 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Memory; +#endif namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -40,10 +43,10 @@ public static ref T DangerousGetReference(this ReadOnlySpan span) /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref T DangerousGetReferenceAt(this ReadOnlySpan span, int i) + public static ref T DangerousGetReferenceAt(this ReadOnlySpan span, int i) { - // Here we assume the input index will never be negative, so we do an unsafe cast to - // force the JIT to skip the sign extension when going from int to native int. + // Here we assume the input index will never be negative, so we do a (nint)(uint) cast + // to force the JIT to skip the sign extension when going from int to native int. // On .NET Core 3.1, if we only use Unsafe.Add(ref r0, i), we get the following: // ============================= // L0000: mov rax, [rcx] @@ -54,7 +57,7 @@ public static unsafe ref T DangerousGetReferenceAt(this ReadOnlySpan span, // Note the movsxd (move with sign extension) to expand the index passed in edx to // the whole rdx register. This is unnecessary and more expensive than just a mov, // which when done to a large register size automatically zeroes the upper bits. - // With the (IntPtr)(void*)(uint) cast, we get the following codegen instead: + // With the (nint)(uint) cast, we get the following codegen instead: // ============================= // L0000: mov rax, [rcx] // L0003: mov edx, edx @@ -67,13 +70,31 @@ public static unsafe ref T DangerousGetReferenceAt(this ReadOnlySpan span, // bit architectures, producing optimal code in both cases (they are either completely // elided on 32 bit systems, or result in the correct register expansion when on 64 bit). // We first do an unchecked conversion to uint (which is just a reinterpret-cast). We - // then cast to void*, which lets the following IntPtr cast avoid the range check on 32 bit - // (since uint could be out of range there if the original index was negative). The final - // result is a clean mov as shown above. This will eventually be natively supported by the - // JIT compiler (see https://github.com/dotnet/runtime/issues/38794), but doing this here + // then cast to nint, so that we can obtain an IntPtr value without the range check (since + // uint could be out of range there if the original index was negative). The final result + // is a clean mov as shown above. This will eventually be natively supported by the JIT + // compiler (see https://github.com/dotnet/runtime/issues/38794), but doing this here // still ensures the optimal codegen even on existing runtimes (eg. .NET Core 2.1 and 3.1). ref T r0 = ref MemoryMarshal.GetReference(span); - ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); + + return ref ri; + } + + /// + /// Returns a reference to an element at a specified index within a given , with no bounds checks. + /// + /// The type of elements in the input instance. + /// The input instance. + /// The index of the element to retrieve within . + /// A reference to the element within at the index specified by . + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReferenceAt(this ReadOnlySpan span, nint i) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref T ri = ref Unsafe.Add(ref r0, i); return ref ri; } @@ -117,7 +138,7 @@ public static unsafe ref T DangerousGetReferenceAt(this ReadOnlySpan span, /// [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref readonly T DangerousGetLookupReferenceAt(this ReadOnlySpan span, int i) + public static ref readonly T DangerousGetLookupReferenceAt(this ReadOnlySpan span, int i) { // Check whether the input is in range by first casting both // operands to uint and then comparing them, as this allows @@ -141,11 +162,57 @@ public static unsafe ref readonly T DangerousGetLookupReferenceAt(this ReadOn mask = ~negativeFlag, offset = (uint)i & mask; ref T r0 = ref MemoryMarshal.GetReference(span); - ref T r1 = ref Unsafe.Add(ref r0, (IntPtr)(void*)offset); + ref T r1 = ref Unsafe.Add(ref r0, (nint)offset); return ref r1; } +#if SPAN_RUNTIME_SUPPORT + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int height, int width) + { + return new ReadOnlySpan2D(span, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int offset, int height, int width, int pitch) + { + return new ReadOnlySpan2D(span, offset, height, width, pitch); + } +#endif + /// /// Gets the index of an element of a given from its reference. /// @@ -156,34 +223,20 @@ public static unsafe ref readonly T DangerousGetLookupReferenceAt(this ReadOn /// Thrown if does not belong to . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this ReadOnlySpan span, in T value) + public static int IndexOf(this ReadOnlySpan span, in T value) { ref T r0 = ref MemoryMarshal.GetReference(span); ref T r1 = ref Unsafe.AsRef(value); IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref r1); - if (sizeof(IntPtr) == sizeof(long)) - { - long elementOffset = (long)byteOffset / Unsafe.SizeOf(); - - if ((ulong)elementOffset >= (ulong)span.Length) - { - SpanExtensions.ThrowArgumentOutOfRangeExceptionForInvalidReference(); - } + nint elementOffset = byteOffset / (nint)(uint)Unsafe.SizeOf(); - return unchecked((int)elementOffset); - } - else + if ((nuint)elementOffset >= (uint)span.Length) { - int elementOffset = (int)byteOffset / Unsafe.SizeOf(); - - if ((uint)elementOffset >= (uint)span.Length) - { - SpanExtensions.ThrowArgumentOutOfRangeExceptionForInvalidReference(); - } - - return elementOffset; + SpanExtensions.ThrowArgumentOutOfRangeExceptionForInvalidReference(); } + + return (int)elementOffset; } /// @@ -195,13 +248,13 @@ public static unsafe int IndexOf(this ReadOnlySpan span, in T value) /// The number of occurrences of in . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Count(this ReadOnlySpan span, T value) + public static int Count(this ReadOnlySpan span, T value) where T : IEquatable { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr length = (IntPtr)(void*)(uint)span.Length; + nint length = (nint)(uint)span.Length; - return SpanHelper.Count(ref r0, length, value); + return (int)SpanHelper.Count(ref r0, length, value); } /// @@ -321,13 +374,41 @@ public static ReadOnlySpanTokenizer Tokenize(this ReadOnlySpan span, T /// The Djb2 hash is fully deterministic and with no random components. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int GetDjb2HashCode(this ReadOnlySpan span) + public static int GetDjb2HashCode(this ReadOnlySpan span) where T : notnull { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr length = (IntPtr)(void*)(uint)span.Length; + nint length = (nint)(uint)span.Length; return SpanHelper.GetDjb2HashCode(ref r0, length); } + + /// + /// Copies the contents of a given into destination instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The instance to copy items into. + /// + /// Thrown when the destination is shorter than the source . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this ReadOnlySpan span, RefEnumerable destination) + { + destination.CopyFrom(span); + } + + /// + /// Attempts to copy the contents of a given into destination instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The instance to copy items into. + /// Whether or not the operation was successful. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryCopyTo(this ReadOnlySpan span, RefEnumerable destination) + { + return destination.TryCopyFrom(span); + } } } diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs index 52dac209876..eecf0519360 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -8,6 +8,9 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Memory; +#endif namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -40,14 +43,78 @@ public static ref T DangerousGetReference(this Span span) /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref T DangerousGetReferenceAt(this Span span, int i) + public static ref T DangerousGetReferenceAt(this Span span, int i) { ref T r0 = ref MemoryMarshal.GetReference(span); - ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i); + ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i); return ref ri; } + /// + /// Returns a reference to an element at a specified index within a given , with no bounds checks. + /// + /// The type of elements in the input instance. + /// The input instance. + /// The index of the element to retrieve within . + /// A reference to the element within at the index specified by . + /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousGetReferenceAt(this Span span, nint i) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref T ri = ref Unsafe.Add(ref r0, i); + + return ref ri; + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span2D AsSpan2D(this Span span, int height, int width) + { + return new Span2D(span, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span2D AsSpan2D(this Span span, int offset, int height, int width, int pitch) + { + return new Span2D(span, offset, height, width, pitch); + } +#endif + /// /// 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. @@ -102,33 +169,19 @@ public static Span Cast(this Span span) /// Thrown if does not belong to . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this Span span, ref T value) + public static int IndexOf(this Span span, ref T value) { ref T r0 = ref MemoryMarshal.GetReference(span); IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref value); - if (sizeof(IntPtr) == sizeof(long)) - { - long elementOffset = (long)byteOffset / Unsafe.SizeOf(); - - if ((ulong)elementOffset >= (ulong)span.Length) - { - ThrowArgumentOutOfRangeExceptionForInvalidReference(); - } + nint elementOffset = byteOffset / (nint)(uint)Unsafe.SizeOf(); - return unchecked((int)elementOffset); - } - else + if ((nuint)elementOffset >= (uint)span.Length) { - int elementOffset = (int)byteOffset / Unsafe.SizeOf(); - - if ((uint)elementOffset >= (uint)span.Length) - { - ThrowArgumentOutOfRangeExceptionForInvalidReference(); - } - - return elementOffset; + ThrowArgumentOutOfRangeExceptionForInvalidReference(); } + + return (int)elementOffset; } /// @@ -140,13 +193,13 @@ public static unsafe int IndexOf(this Span span, ref T value) /// The number of occurrences of in . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Count(this Span span, T value) + public static int Count(this Span span, T value) where T : IEquatable { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr length = (IntPtr)(void*)(uint)span.Length; + nint length = (nint)(uint)span.Length; - return SpanHelper.Count(ref r0, length, value); + return (int)SpanHelper.Count(ref r0, length, value); } /// @@ -211,15 +264,43 @@ public static SpanTokenizer Tokenize(this Span span, T separator) /// The Djb2 hash is fully deterministic and with no random components. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int GetDjb2HashCode(this Span span) + public static int GetDjb2HashCode(this Span span) where T : notnull { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr length = (IntPtr)(void*)(uint)span.Length; + nint length = (nint)(uint)span.Length; return SpanHelper.GetDjb2HashCode(ref r0, length); } + /// + /// Copies the contents of a given into destination instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The instance to copy items into. + /// + /// Thrown when the destination is shorter than the source . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this Span span, RefEnumerable destination) + { + destination.CopyFrom(span); + } + + /// + /// Attempts to copy the contents of a given into destination instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The instance to copy items into. + /// Whether or not the operation was successful. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryCopyTo(this Span span, RefEnumerable destination) + { + return destination.TryCopyFrom(span); + } + /// /// Throws an when the given reference is out of range. /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs index c9cdc114c36..223480d5b06 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs @@ -48,7 +48,7 @@ public static ref char DangerousGetReference(this string text) /// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the parameter is valid. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe ref char DangerousGetReferenceAt(this string text, int i) + public static ref char DangerousGetReferenceAt(this string text, int i) { #if NETCOREAPP3_1 ref char r0 = ref Unsafe.AsRef(text.GetPinnableReference()); @@ -57,7 +57,7 @@ public static unsafe ref char DangerousGetReferenceAt(this string text, int i) #else ref char r0 = ref MemoryMarshal.GetReference(text.AsSpan()); #endif - ref char ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i); + ref char ri = ref Unsafe.Add(ref r0, (nint)(uint)i); return ref ri; } @@ -91,12 +91,12 @@ private sealed class RawStringData /// The number of occurrences of in . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int Count(this string text, char c) + public static int Count(this string text, char c) { ref char r0 = ref text.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)text.Length; + nint length = (nint)(uint)text.Length; - return SpanHelper.Count(ref r0, length, c); + return (int)SpanHelper.Count(ref r0, length, c); } /// @@ -160,7 +160,7 @@ public static ReadOnlySpanTokenizer Tokenize(this string text, char separa public static unsafe int GetDjb2HashCode(this string text) { ref char r0 = ref text.DangerousGetReference(); - IntPtr length = (IntPtr)(void*)(uint)text.Length; + nint length = (nint)(uint)text.Length; return SpanHelper.GetDjb2HashCode(ref r0, length); } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs index 3c2a43c32a4..c4c651782bb 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs @@ -9,6 +9,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; +#if SPAN_RUNTIME_SUPPORT +using RuntimeHelpers = System.Runtime.CompilerServices.RuntimeHelpers; +#else +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif namespace Microsoft.Toolkit.HighPerformance.Helpers { @@ -25,14 +30,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers /// For more info, see . /// public struct HashCode -#if SPAN_RUNTIME_SUPPORT where T : notnull -#else - // If we lack the RuntimeHelpers.IsReferenceOrContainsReferences API, - // we need to constraint the generic type parameter to unmanaged, as we - // wouldn't otherwise be able to properly validate it at runtime. - where T : unmanaged -#endif { /// /// Gets a content hash from the input instance using the xxHash32 algorithm. @@ -57,19 +55,17 @@ public static int Combine(ReadOnlySpan span) /// The returned hash code is not processed through APIs. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe int CombineValues(ReadOnlySpan span) + internal static int CombineValues(ReadOnlySpan span) { ref T r0 = ref MemoryMarshal.GetReference(span); -#if SPAN_RUNTIME_SUPPORT // If typeof(T) is not unmanaged, iterate over all the items one by one. // This check is always known in advance either by the JITter or by the AOT // compiler, so this branch will never actually be executed by the code. if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - return SpanHelper.GetDjb2HashCode(ref r0, (IntPtr)(void*)(uint)span.Length); + return SpanHelper.GetDjb2HashCode(ref r0, (nint)(uint)span.Length); } -#endif // Get the info for the target memory area to process. // The line below is computing the total byte size for the span, @@ -79,7 +75,7 @@ internal static unsafe int CombineValues(ReadOnlySpan span) // process. In that case it will just compute the byte size as a 32 bit // multiplication with overflow, which is guaranteed never to happen anyway. ref byte rb = ref Unsafe.As(ref r0); - IntPtr length = (IntPtr)(void*)((uint)span.Length * (uint)Unsafe.SizeOf()); + nint length = (nint)((uint)span.Length * (uint)Unsafe.SizeOf()); return SpanHelper.GetDjb2LikeByteHash(ref rb, length); } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RefEnumerableHelper.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RefEnumerableHelper.cs new file mode 100644 index 00000000000..2844a34fa8b --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RefEnumerableHelper.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals +{ + /// + /// Helpers to process sequences of values by reference with a given step. + /// + internal static class RefEnumerableHelper + { + /// + /// Clears a target memory area. + /// + /// The type of values to clear. + /// A reference to the start of the memory area. + /// The number of items in the memory area. + /// The number of items between each consecutive target value. + public static void Clear(ref T r0, nint length, nint step) + { + nint offset = 0; + + // Main loop with 8 unrolled iterations + while (length >= 8) + { + Unsafe.Add(ref r0, offset) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + + length -= 8; + offset += step; + } + + if (length >= 4) + { + Unsafe.Add(ref r0, offset) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + Unsafe.Add(ref r0, offset += step) = default!; + + length -= 4; + offset += step; + } + + // Clear the remaining values + while (length > 0) + { + Unsafe.Add(ref r0, offset) = default!; + + length -= 1; + offset += step; + } + } + + /// + /// Copies a sequence of discontiguous items from one memory area to another. + /// + /// The type of items to copy. + /// The source reference to copy from. + /// The target reference to copy to. + /// The total number of items to copy. + /// The step between consecutive items in the memory area pointed to by . + public static void CopyTo(ref T sourceRef, ref T destinationRef, nint length, nint sourceStep) + { + nint + sourceOffset = 0, + destinationOffset = 0; + + while (length >= 8) + { + Unsafe.Add(ref destinationRef, destinationOffset + 0) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset + 1) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 2) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 3) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 4) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 5) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 6) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 7) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + + length -= 8; + sourceOffset += sourceStep; + destinationOffset += 8; + } + + if (length >= 4) + { + Unsafe.Add(ref destinationRef, destinationOffset + 0) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset + 1) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 2) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset + 3) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + + length -= 4; + sourceOffset += sourceStep; + destinationOffset += 4; + } + + while (length > 0) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + + length -= 1; + sourceOffset += sourceStep; + destinationOffset += 1; + } + } + + /// + /// Copies a sequence of discontiguous items from one memory area to another. + /// + /// The type of items to copy. + /// The source reference to copy from. + /// The target reference to copy to. + /// The total number of items to copy. + /// The step between consecutive items in the memory area pointed to by . + /// The step between consecutive items in the memory area pointed to by . + public static void CopyTo(ref T sourceRef, ref T destinationRef, nint length, nint sourceStep, nint destinationStep) + { + nint + sourceOffset = 0, + destinationOffset = 0; + + while (length >= 8) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + + length -= 8; + sourceOffset += sourceStep; + destinationOffset += destinationStep; + } + + if (length >= 4) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + Unsafe.Add(ref destinationRef, destinationOffset += destinationStep) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep); + + length -= 4; + sourceOffset += sourceStep; + destinationOffset += destinationStep; + } + + while (length > 0) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + + length -= 1; + sourceOffset += sourceStep; + destinationOffset += destinationStep; + } + } + + /// + /// Copies a sequence of discontiguous items from one memory area to another. This mirrors + /// , but refers to instead. + /// + /// The type of items to copy. + /// The source reference to copy from. + /// The target reference to copy to. + /// The total number of items to copy. + /// The step between consecutive items in the memory area pointed to by . + public static void CopyFrom(ref T sourceRef, ref T destinationRef, nint length, nint sourceStep) + { + nint + sourceOffset = 0, + destinationOffset = 0; + + while (length >= 8) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 1); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 2); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 3); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 4); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 5); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 6); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 7); + + length -= 8; + sourceOffset += 8; + destinationOffset += sourceStep; + } + + if (length >= 4) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 1); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 2); + Unsafe.Add(ref destinationRef, destinationOffset += sourceStep) = Unsafe.Add(ref sourceRef, sourceOffset + 3); + + length -= 4; + sourceOffset += 4; + destinationOffset += sourceStep; + } + + while (length > 0) + { + Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset); + + length -= 1; + sourceOffset += 1; + destinationOffset += sourceStep; + } + } + + /// + /// Fills a target memory area. + /// + /// The type of values to fill. + /// A reference to the start of the memory area. + /// The number of items in the memory area. + /// The number of items between each consecutive target value. + /// The value to assign to every item in the target memory area. + public static void Fill(ref T r0, nint length, nint step, T value) + { + nint offset = 0; + + while (length >= 8) + { + Unsafe.Add(ref r0, offset) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + + length -= 8; + offset += step; + } + + if (length >= 4) + { + Unsafe.Add(ref r0, offset) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + Unsafe.Add(ref r0, offset += step) = value; + + length -= 4; + offset += step; + } + + while (length > 0) + { + Unsafe.Add(ref r0, offset) = value; + + length -= 1; + offset += step; + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs new file mode 100644 index 00000000000..d14bc211193 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs @@ -0,0 +1,275 @@ +// 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. + +#pragma warning disable SA1512 + +// The portable implementation in this type is originally from CoreFX. +// See https://github.com/dotnet/corefx/blob/release/2.1/src/System.Memory/src/System/SpanHelpers.cs. + +using System; +using System.Diagnostics.Contracts; +#if !SPAN_RUNTIME_SUPPORT +using System.Reflection; +#endif +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; + +namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals +{ + /// + /// A helper class that act as polyfill for .NET Standard 2.0 and below. + /// + internal static class RuntimeHelpers + { + /// + /// Gets the length of a given array as a native integer. + /// + /// The type of values in the array. + /// The input instance. + /// The total length of as a native integer. + /// + /// This method is needed because this expression is not inlined correctly if the target array + /// is only visible as a non-generic instance, because the C# compiler will + /// not be able to emit the opcode instead of calling the right method. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GetArrayNativeLength(T[] array) + { +#if NETSTANDARD1_4 + // .NET Standard 1.4 doesn't include the API to get the long length, so + // we just cast the length and throw in case the array is larger than + // int.MaxValue. There's not much we can do in this specific case. + return (nint)(uint)array.Length; +#else + return (nint)array.LongLength; +#endif + } + + /// + /// Gets the length of a given array as a native integer. + /// + /// The input instance. + /// The total length of as a native integer. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GetArrayNativeLength(Array array) + { +#if NETSTANDARD1_4 + return (nint)(uint)array.Length; +#else + return (nint)array.LongLength; +#endif + } + + /// + /// Gets the byte offset to the first element in a SZ array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a SZ array. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArrayDataByteOffset() + { + return TypeInfo.ArrayDataByteOffset; + } + + /// + /// Gets the byte offset to the first element in a 2D array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a 2D array. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArray2DDataByteOffset() + { + return TypeInfo.Array2DDataByteOffset; + } + + /// + /// Gets the byte offset to the first element in a 3D array. + /// + /// The type of values in the array. + /// The byte offset to the first element in a 3D array. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetArray3DDataByteOffset() + { + return TypeInfo.Array3DDataByteOffset; + } + +#if !SPAN_RUNTIME_SUPPORT + /// + /// Gets a byte offset describing a portable pinnable reference. This can either be an + /// interior pointer into some object data (described with a valid reference + /// and a reference to some of its data), or a raw pointer (described with a + /// reference to an , and a reference that is assumed to refer to pinned data). + /// + /// The type of field being referenced. + /// The input hosting the target field. + /// A reference to a target field of type within . + /// + /// The value representing the offset to the target field from the start of the object data + /// for the parameter , or the value of the raw pointer passed as a tracked reference. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe IntPtr GetObjectDataOrReferenceByteOffset(object? obj, ref T data) + { + if (obj is null) + { + return (IntPtr)Unsafe.AsPointer(ref data); + } + + return obj.DangerousGetObjectDataByteOffset(ref data); + } + + /// + /// Gets a reference from data describing a portable pinnable reference. This can either be an + /// interior pointer into some object data (described with a valid reference + /// and a byte offset into its data), or a raw pointer (described with a + /// reference to an , and a byte offset representing the value of the raw pointer). + /// + /// The type of reference to retrieve. + /// The input hosting the target field. + /// The input byte offset for the reference to retrieve. + /// A reference matching the given parameters. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ref T GetObjectDataAtOffsetOrPointerReference(object? obj, IntPtr offset) + { + if (obj is null) + { + return ref Unsafe.AsRef((void*)offset); + } + + return ref obj.DangerousGetObjectDataReferenceAt(offset); + } + + /// + /// Checks whether or not a given type is a reference type or contains references. + /// + /// The type to check. + /// Whether or not respects the constraint. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsReferenceOrContainsReferences() + { + return TypeInfo.IsReferenceOrContainsReferences; + } + + /// + /// Implements the logic for . + /// + /// The current type to check. + /// Whether or not is a reference type or contains references. + [Pure] + private static bool IsReferenceOrContainsReferences(Type type) + { + // Common case, for primitive types + if (type.GetTypeInfo().IsPrimitive) + { + return false; + } + + if (!type.GetTypeInfo().IsValueType) + { + return true; + } + + // Check if the type is Nullable + if (Nullable.GetUnderlyingType(type) is Type nullableType) + { + type = nullableType; + } + + if (type.GetTypeInfo().IsEnum) + { + return false; + } + + // Complex struct, recursively inspect all fields + foreach (FieldInfo field in type.GetTypeInfo().DeclaredFields) + { + if (field.IsStatic) + { + continue; + } + + if (IsReferenceOrContainsReferences(field.FieldType)) + { + return true; + } + } + + return false; + } +#endif + + /// + /// A private generic class to preload type info for arbitrary runtime types. + /// + /// The type to load info for. + private static class TypeInfo + { + /// + /// The byte offset to the first element in a SZ array. + /// + public static readonly IntPtr ArrayDataByteOffset = MeasureArrayDataByteOffset(); + + /// + /// The byte offset to the first element in a 2D array. + /// + public static readonly IntPtr Array2DDataByteOffset = MeasureArray2DDataByteOffset(); + + /// + /// The byte offset to the first element in a 3D array. + /// + public static readonly IntPtr Array3DDataByteOffset = MeasureArray3DDataByteOffset(); + +#if !SPAN_RUNTIME_SUPPORT + /// + /// Indicates whether does not respect the constraint. + /// + public static readonly bool IsReferenceOrContainsReferences = IsReferenceOrContainsReferences(typeof(T)); +#endif + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + [Pure] + private static IntPtr MeasureArrayDataByteOffset() + { + var array = new T[1]; + + return array.DangerousGetObjectDataByteOffset(ref array[0]); + } + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + [Pure] + private static IntPtr MeasureArray2DDataByteOffset() + { + var array = new T[1, 1]; + + return array.DangerousGetObjectDataByteOffset(ref array[0, 0]); + } + + /// + /// Computes the value for . + /// + /// The value of for the current runtime. + [Pure] + private static IntPtr MeasureArray3DDataByteOffset() + { + var array = new T[1, 1, 1]; + + return array.DangerousGetObjectDataByteOffset(ref array[0, 0, 0]); + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Count.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Count.cs index 8e1288bda9e..408b3073cf1 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Count.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Count.cs @@ -25,7 +25,7 @@ internal static partial class SpanHelper /// The number of occurrences of in the search space [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Count(ref T r0, IntPtr length, T value) + public static nint Count(ref T r0, nint length, T value) where T : IEquatable { if (!Vector.IsHardwareAccelerated) @@ -41,7 +41,7 @@ public static int Count(ref T r0, IntPtr length, T value) ref sbyte r1 = ref Unsafe.As(ref r0); sbyte target = Unsafe.As(ref value); - return CountSimd(ref r1, length, target, (IntPtr)sbyte.MaxValue); + return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(char) || @@ -51,7 +51,7 @@ public static int Count(ref T r0, IntPtr length, T value) ref short r1 = ref Unsafe.As(ref r0); short target = Unsafe.As(ref value); - return CountSimd(ref r1, length, target, (IntPtr)short.MaxValue); + return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(int) || @@ -60,7 +60,7 @@ public static int Count(ref T r0, IntPtr length, T value) ref int r1 = ref Unsafe.As(ref r0); int target = Unsafe.As(ref value); - return CountSimd(ref r1, length, target, (IntPtr)int.MaxValue); + return CountSimd(ref r1, length, target); } if (typeof(T) == typeof(long) || @@ -69,7 +69,7 @@ public static int Count(ref T r0, IntPtr length, T value) ref long r1 = ref Unsafe.As(ref r0); long target = Unsafe.As(ref value); - return CountSimd(ref r1, length, target, (IntPtr)int.MaxValue); + return CountSimd(ref r1, length, target); } return CountSequential(ref r0, length, value); @@ -82,44 +82,44 @@ public static int Count(ref T r0, IntPtr length, T value) #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - private static unsafe int CountSequential(ref T r0, IntPtr length, T value) + private static nint CountSequential(ref T r0, nint length, T value) where T : IEquatable { - int result = 0; - - IntPtr offset = default; + nint + result = 0, + offset = 0; // Main loop with 8 unrolled iterations - while ((byte*)length >= (byte*)8) + while (length >= 8) { - result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); length -= 8; offset += 8; } - if ((byte*)length >= (byte*)4) + if (length >= 4) { - result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); length -= 4; offset += 4; } // Iterate over the remaining values and count those that match - while ((byte*)length > (byte*)0) + while (length > 0) { - result += Unsafe.Add(ref r0, offset).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); length -= 1; offset += 1; @@ -135,15 +135,15 @@ private static unsafe int CountSequential(ref T r0, IntPtr length, T value) #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - private static unsafe int CountSimd(ref T r0, IntPtr length, T value, IntPtr max) + private static nint CountSimd(ref T r0, nint length, T value) where T : unmanaged, IEquatable { - int result = 0; - - IntPtr offset = default; + nint + result = 0, + offset = 0; // Skip the initialization overhead if there are not enough items - if ((byte*)length >= (byte*)Vector.Count) + if (length >= Vector.Count) { var vc = new Vector(value); @@ -154,13 +154,14 @@ private static unsafe int CountSimd(ref T r0, IntPtr length, T value, IntPtr // to sum the partial results. We also backup the current offset to // be able to track how many items have been processed, which lets // us avoid updating a third counter (length) in the loop body. - IntPtr - chunkLength = Min(length, max), + nint + max = GetUpperBound(), + chunkLength = length <= max ? length : max, initialOffset = offset; var partials = Vector.Zero; - while ((byte*)chunkLength >= (byte*)Vector.Count) + while (chunkLength >= Vector.Count) { ref T ri = ref Unsafe.Add(ref r0, offset); @@ -181,27 +182,26 @@ private static unsafe int CountSimd(ref T r0, IntPtr length, T value, IntPtr offset += Vector.Count; } - result += CastToInt(Vector.Dot(partials, Vector.One)); - - length = Subtract(length, Subtract(offset, initialOffset)); + result += CastToNativeInt(Vector.Dot(partials, Vector.One)); + length -= offset - initialOffset; } - while ((byte*)length >= (byte*)Vector.Count); + while (length >= Vector.Count); } // Optional 8 unrolled iterations. This is only done when a single SIMD // register can contain over 8 values of the current type, as otherwise // there could never be enough items left after the vectorized path if (Vector.Count > 8 && - (byte*)length >= (byte*)8) + length >= 8) { - result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 4).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 5).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 6).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 7).Equals(value).ToByte(); length -= 8; offset += 8; @@ -209,21 +209,21 @@ private static unsafe int CountSimd(ref T r0, IntPtr length, T value, IntPtr // Optional 4 unrolled iterations if (Vector.Count > 4 && - (byte*)length >= (byte*)4) + length >= 4) { - result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToInt(); - result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset + 0).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 1).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 2).Equals(value).ToByte(); + result += Unsafe.Add(ref r0, offset + 3).Equals(value).ToByte(); length -= 4; offset += 4; } // Iterate over the remaining values and count those that match - while ((byte*)length > (byte*)0) + while (length > 0) { - result += Unsafe.Add(ref r0, offset).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, offset).Equals(value).ToByte(); length -= 1; offset += 1; @@ -233,73 +233,88 @@ private static unsafe int CountSimd(ref T r0, IntPtr length, T value, IntPtr } /// - /// Returns the minimum between two values. + /// Gets the upper bound for partial sums with a given parameter. /// - /// The first value. - /// The second value - /// The minimum between and . + /// The type argument currently in use. + /// The native value representing the upper bound. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe IntPtr Min(IntPtr a, IntPtr b) + private static unsafe nint GetUpperBound() + where T : unmanaged { - if (sizeof(IntPtr) == 4) + if (typeof(T) == typeof(byte) || + typeof(T) == typeof(sbyte) || + typeof(T) == typeof(bool)) { - return (IntPtr)Math.Min((int)a, (int)b); + return sbyte.MaxValue; } - return (IntPtr)Math.Min((long)a, (long)b); - } + if (typeof(T) == typeof(char) || + typeof(T) == typeof(ushort) || + typeof(T) == typeof(short)) + { + return short.MaxValue; + } - /// - /// Returns the difference between two values. - /// - /// The first value. - /// The second value - /// The difference between and . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe IntPtr Subtract(IntPtr a, IntPtr b) - { - if (sizeof(IntPtr) == 4) + if (typeof(T) == typeof(int) || + typeof(T) == typeof(uint)) + { + return int.MaxValue; + } + + if (typeof(T) == typeof(long) || + typeof(T) == typeof(ulong)) { - return (IntPtr)((int)a - (int)b); + if (sizeof(nint) == sizeof(int)) + { + return int.MaxValue; + } + + // If we are on a 64 bit architecture and we are counting with a SIMD vector of 64 + // bit values, we can use long.MaxValue as the upper bound, as a native integer will + // be able to contain such a value with no overflows. This will allow the count tight + // loop to process all the items in the target area in a single pass (except the mod). + // The (void*) cast is necessary to ensure the right constant is produced on runtimes + // before .NET 5 that don't natively support C# 9. For instance, removing that (void*) + // cast results in the value 0xFFFFFFFFFFFFFFFF (-1) instead of 0x7FFFFFFFFFFFFFFFF. + return (nint)(void*)long.MaxValue; } - return (IntPtr)((long)a - (long)b); + throw null!; } /// - /// Casts a value of a given type to . + /// Casts a value of a given type to a native . /// /// The input type to cast. - /// The input value to cast to . - /// The cast of . + /// The input value to cast to native . + /// The native cast of . [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int CastToInt(T value) + private static nint CastToNativeInt(T value) where T : unmanaged { if (typeof(T) == typeof(sbyte)) { - return Unsafe.As(ref value); + return (byte)(sbyte)(object)value; } if (typeof(T) == typeof(short)) { - return Unsafe.As(ref value); + return (ushort)(short)(object)value; } if (typeof(T) == typeof(int)) { - return Unsafe.As(ref value); + return (nint)(uint)(int)(object)value; } if (typeof(T) == typeof(long)) { - return (int)Unsafe.As(ref value); + return (nint)(ulong)(long)(object)value; } - throw new NotSupportedException($"Invalid input type {typeof(T)}"); + throw null!; } } } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Hash.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Hash.cs index 9ad86f923e0..a7920734683 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Hash.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/SpanHelper.Hash.cs @@ -2,7 +2,6 @@ // 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.Numerics; using System.Runtime.CompilerServices; @@ -25,14 +24,13 @@ internal static partial class SpanHelper #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - public static unsafe int GetDjb2HashCode(ref T r0, IntPtr length) + public static int GetDjb2HashCode(ref T r0, nint length) where T : notnull { int hash = 5381; + nint offset = 0; - IntPtr offset = default; - - while ((byte*)length >= (byte*)8) + while (length >= 8) { // Doing a left shift by 5 and adding is equivalent to multiplying by 33. // This is preferred for performance reasons, as when working with integer @@ -52,7 +50,7 @@ public static unsafe int GetDjb2HashCode(ref T r0, IntPtr length) offset += 8; } - if ((byte*)length >= (byte*)4) + if (length >= 4) { hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 0).GetHashCode()); hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset + 1).GetHashCode()); @@ -63,7 +61,7 @@ public static unsafe int GetDjb2HashCode(ref T r0, IntPtr length) offset += 4; } - while ((byte*)length > (byte*)0) + while (length > 0) { hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset).GetHashCode()); @@ -92,11 +90,10 @@ public static unsafe int GetDjb2HashCode(ref T r0, IntPtr length) #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) + public static unsafe int GetDjb2LikeByteHash(ref byte r0, nint length) { int hash = 5381; - - IntPtr offset = default; + nint offset = 0; // Check whether SIMD instructions are supported, and also check // whether we have enough data to perform at least one unrolled @@ -107,7 +104,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) // any preprocessing to try to get memory aligned, as that would cause // the hash codes to potentially be different for the same data. if (Vector.IsHardwareAccelerated && - (byte*)length >= (byte*)(Vector.Count << 3)) + length >= (Vector.Count << 3)) { var vh = new Vector(5381); var v33 = new Vector(33); @@ -115,7 +112,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) // First vectorized loop, with 8 unrolled iterations. // Assuming 256-bit registers (AVX2), a total of 256 bytes are processed // per iteration, with the partial hashes being accumulated for later use. - while ((byte*)length >= (byte*)(Vector.Count << 3)) + while (length >= (Vector.Count << 3)) { ref byte ri0 = ref Unsafe.Add(ref r0, offset + (Vector.Count * 0)); var vi0 = Unsafe.ReadUnaligned>(ref ri0); @@ -163,7 +160,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) // When this loop is reached, there are up to 255 bytes left (on AVX2). // Each iteration processed an additional 32 bytes and accumulates the results. - while ((byte*)length >= (byte*)Vector.Count) + while (length >= Vector.Count) { ref byte ri = ref Unsafe.Add(ref r0, offset); var vi = Unsafe.ReadUnaligned>(ref ri); @@ -186,9 +183,9 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) // Only use the loop working with 64-bit values if we are on a // 64-bit processor, otherwise the result would be much slower. // Each unrolled iteration processes 64 bytes. - if (sizeof(IntPtr) == sizeof(ulong)) + if (sizeof(nint) == sizeof(ulong)) { - while ((byte*)length >= (byte*)(sizeof(ulong) << 3)) + while (length >= (sizeof(ulong) << 3)) { ref byte ri0 = ref Unsafe.Add(ref r0, offset + (sizeof(ulong) * 0)); var value0 = Unsafe.ReadUnaligned(ref ri0); @@ -228,7 +225,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) } // Each unrolled iteration processes 32 bytes - while ((byte*)length >= (byte*)(sizeof(uint) << 3)) + while (length >= (sizeof(uint) << 3)) { ref byte ri0 = ref Unsafe.Add(ref r0, offset + (sizeof(uint) * 0)); var value0 = Unsafe.ReadUnaligned(ref ri0); @@ -271,7 +268,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) // left, both for the vectorized and non vectorized paths. // That number would go up to 63 on AVX512 systems, in which case it is // still useful to perform this last loop unrolling. - if ((byte*)length >= (byte*)(sizeof(ushort) << 3)) + if (length >= (sizeof(ushort) << 3)) { ref byte ri0 = ref Unsafe.Add(ref r0, offset + (sizeof(ushort) * 0)); var value0 = Unsafe.ReadUnaligned(ref ri0); @@ -310,7 +307,7 @@ public static unsafe int GetDjb2LikeByteHash(ref byte r0, IntPtr length) } // Handle the leftover items - while ((byte*)length > (byte*)0) + while (length > 0) { hash = unchecked(((hash << 5) + hash) ^ Unsafe.Add(ref r0, offset)); diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs index 9b267ed5da7..e63deb17727 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs @@ -218,7 +218,6 @@ public ActionInvoker( /// Processes the batch of actions at a specified index /// /// The index of the batch to process - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Invoke(int i) { int diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs index ba000e768f8..a6ff50a2893 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs @@ -312,7 +312,6 @@ public Action2DInvoker( /// Processes the batch of actions at a specified index /// /// The index of the batch to process - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Invoke(int i) { int diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs index 79c72ca122b..b2bcd086d45 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs @@ -135,8 +135,7 @@ public InActionInvoker( /// Processes the batch of actions at a specified index /// /// The index of the batch to process - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void Invoke(int i) + public void Invoke(int i) { int low = i * this.batchSize, @@ -147,7 +146,7 @@ public unsafe void Invoke(int i) for (int j = low; j < end; j++) { - ref TItem rj = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)j); + ref TItem rj = ref Unsafe.Add(ref r0, (nint)(uint)j); Unsafe.AsRef(this.action).Invoke(rj); } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs new file mode 100644 index 00000000000..2bf57ad4a21 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs @@ -0,0 +1,160 @@ +// 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.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Memory; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to work with parallel code in a highly optimized manner. + /// + public static partial class ParallelHelper + { + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(ReadOnlyMemory2D memory) + where TAction : struct, IInAction + { + ForEach(memory, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// + /// The minimum number of actions to run per individual thread. Set to 1 if all invocations + /// should be parallelized, or to a greater number if each individual invocation is fast + /// enough that it is more efficient to set a lower bound per each running thread. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(ReadOnlyMemory2D memory, int minimumActionsPerThread) + where TAction : struct, IInAction + { + ForEach(memory, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(ReadOnlyMemory2D memory, in TAction action) + where TAction : struct, IInAction + { + ForEach(memory, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// The instance representing the action to invoke. + /// + /// The minimum number of actions to run per individual thread. Set to 1 if all invocations + /// should be parallelized, or to a greater number if each individual invocation is fast + /// enough that it is more efficient to set a lower bound per each running thread. + /// + public static void ForEach(ReadOnlyMemory2D memory, in TAction action, int minimumActionsPerThread) + where TAction : struct, IInAction + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (memory.IsEmpty) + { + return; + } + + nint + maxBatches = 1 + ((memory.Length - 1) / minimumActionsPerThread), + clipBatches = maxBatches <= memory.Height ? maxBatches : memory.Height; + int + cores = Environment.ProcessorCount, + numBatches = (int)(clipBatches <= cores ? clipBatches : cores), + batchHeight = 1 + ((memory.Height - 1) / numBatches); + + var actionInvoker = new InActionInvokerWithReadOnlyMemory2D(batchHeight, memory, action); + + // Skip the parallel invocation when possible + if (numBatches == 1) + { + actionInvoker.Invoke(0); + + return; + } + + // Run the batched operations in parallel + Parallel.For( + 0, + numBatches, + new ParallelOptions { MaxDegreeOfParallelism = numBatches }, + actionInvoker.Invoke); + } + + // Wrapping struct acting as explicit closure to execute the processing batches + private readonly struct InActionInvokerWithReadOnlyMemory2D + where TAction : struct, IInAction + { + private readonly int batchHeight; + private readonly ReadOnlyMemory2D memory; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public InActionInvokerWithReadOnlyMemory2D( + int batchHeight, + ReadOnlyMemory2D memory, + in TAction action) + { + this.batchHeight = batchHeight; + this.memory = memory; + this.action = action; + } + + /// + /// Processes the batch of actions at a specified index + /// + /// The index of the batch to process + public void Invoke(int i) + { + int lowY = i * this.batchHeight; + nint highY = lowY + this.batchHeight; + int + stopY = (int)(highY <= this.memory.Height ? highY : this.memory.Height), + width = this.memory.Width; + + ReadOnlySpan2D span = this.memory.Span; + + for (int y = lowY; y < stopY; y++) + { + ref TItem r0 = ref span.DangerousGetReferenceAt(y, 0); + + for (int x = 0; x < width; x++) + { + ref TItem ryx = ref Unsafe.Add(ref r0, (nint)(uint)x); + + Unsafe.AsRef(this.action).Invoke(ryx); + } + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs index 18802dd0100..c3521952f34 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs @@ -135,8 +135,7 @@ public RefActionInvoker( /// Processes the batch of actions at a specified index /// /// The index of the batch to process - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void Invoke(int i) + public void Invoke(int i) { int low = i * this.batchSize, @@ -147,7 +146,7 @@ public unsafe void Invoke(int i) for (int j = low; j < end; j++) { - ref TItem rj = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)j); + ref TItem rj = ref Unsafe.Add(ref r0, (nint)(uint)j); Unsafe.AsRef(this.action).Invoke(ref rj); } diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs new file mode 100644 index 00000000000..261e169031a --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs @@ -0,0 +1,167 @@ +// 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.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Memory; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to work with parallel code in a highly optimized manner. + /// + public static partial class ParallelHelper + { + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(Memory2D memory) + where TAction : struct, IRefAction + { + ForEach(memory, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// + /// The minimum number of actions to run per individual thread. Set to 1 if all invocations + /// should be parallelized, or to a greater number if each individual invocation is fast + /// enough that it is more efficient to set a lower bound per each running thread. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(Memory2D memory, int minimumActionsPerThread) + where TAction : struct, IRefAction + { + ForEach(memory, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEach(Memory2D memory, in TAction action) + where TAction : struct, IRefAction + { + ForEach(memory, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop over the input data. + /// + /// The type of items to iterate over. + /// The type of action (implementing of ) to invoke over each item. + /// The input representing the data to process. + /// The instance representing the action to invoke. + /// + /// The minimum number of actions to run per individual thread. Set to 1 if all invocations + /// should be parallelized, or to a greater number if each individual invocation is fast + /// enough that it is more efficient to set a lower bound per each running thread. + /// + public static void ForEach(Memory2D memory, in TAction action, int minimumActionsPerThread) + where TAction : struct, IRefAction + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (memory.IsEmpty) + { + return; + } + + // The underlying data for a Memory2D instance is bound to int.MaxValue in both + // axes, but its total size can exceed this value. Because of this, we calculate + // the target chunks as nint to avoid overflows, and switch back to int values + // for the rest of the setup, since the number of batches is bound to the number + // of CPU cores (which is an int), and the height of each batch is necessarily + // smaller than or equal than int.MaxValue, as it can't be greater than the + // number of total batches, which again is capped at the number of CPU cores. + nint + maxBatches = 1 + ((memory.Length - 1) / minimumActionsPerThread), + clipBatches = maxBatches <= memory.Height ? maxBatches : memory.Height; + int + cores = Environment.ProcessorCount, + numBatches = (int)(clipBatches <= cores ? clipBatches : cores), + batchHeight = 1 + ((memory.Height - 1) / numBatches); + + var actionInvoker = new RefActionInvokerWithReadOnlyMemory2D(batchHeight, memory, action); + + // Skip the parallel invocation when possible + if (numBatches == 1) + { + actionInvoker.Invoke(0); + + return; + } + + // Run the batched operations in parallel + Parallel.For( + 0, + numBatches, + new ParallelOptions { MaxDegreeOfParallelism = numBatches }, + actionInvoker.Invoke); + } + + // Wrapping struct acting as explicit closure to execute the processing batches + private readonly struct RefActionInvokerWithReadOnlyMemory2D + where TAction : struct, IRefAction + { + private readonly int batchHeight; + private readonly Memory2D memory; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefActionInvokerWithReadOnlyMemory2D( + int batchHeight, + Memory2D memory, + in TAction action) + { + this.batchHeight = batchHeight; + this.memory = memory; + this.action = action; + } + + /// + /// Processes the batch of actions at a specified index + /// + /// The index of the batch to process + public void Invoke(int i) + { + int lowY = i * this.batchHeight; + nint highY = lowY + this.batchHeight; + int + stopY = (int)(highY <= this.memory.Height ? highY : this.memory.Height), + width = this.memory.Width; + + ReadOnlySpan2D span = this.memory.Span; + + for (int y = lowY; y < stopY; y++) + { + ref TItem r0 = ref span.DangerousGetReferenceAt(y, 0); + + for (int x = 0; x < width; x++) + { + ref TItem ryx = ref Unsafe.Add(ref r0, (nint)(uint)x); + + Unsafe.AsRef(this.action).Invoke(ref ryx); + } + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Internals/OverflowHelper.cs b/Microsoft.Toolkit.HighPerformance/Memory/Internals/OverflowHelper.cs new file mode 100644 index 00000000000..83576d39197 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Internals/OverflowHelper.cs @@ -0,0 +1,72 @@ +// 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 static System.Math; + +namespace Microsoft.Toolkit.HighPerformance.Memory.Internals +{ + /// + /// A helper to validate arithmetic operations for and . + /// + internal static class OverflowHelper + { + /// + /// Ensures that the input parameters will not exceed the maximum native int value when indexing. + /// + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// Throw when the inputs don't fit in the expected range. + /// The input parameters are assumed to always be positive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EnsureIsInNativeIntRange(int height, int width, int pitch) + { + // As per the layout used in the Memory2D and Span2D types, we have the + // following memory representation with respect to height, width and pitch: + // + // _________width_________ ________... + // / \/ + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |_ + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | |_height + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- |_| + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // ...__pitch__/ + // + // The indexing logic works on nint values in unchecked mode, with no overflow checks, + // which means it relies on the maximum element index to always be within <= nint.MaxValue. + // To ensure no overflows will ever occur there, we need to ensure that no instance can be + // created with parameters that could cause an overflow in case any item was accessed, so we + // need to ensure no overflows occurs when calculating the index of the last item in each view. + // The logic below calculates that index with overflow checks, throwing if one is detected. + // Note that we're subtracting 1 to the height as we don't want to include the trailing pitch + // for the 2D memory area, and also 1 to the width as the index is 0-based, as usual. + // Additionally, we're also ensuring that the stride is never greater than int.MaxValue, for + // consistency with how ND arrays work (int.MaxValue as upper bound for each axis), and to + // allow for faster iteration in the RefEnumerable type, when traversing columns. + _ = checked(((nint)(width + pitch) * Max(unchecked(height - 1), 0)) + Max(unchecked(width - 1), 0)); + } + + /// + /// Ensures that the input parameters will not exceed when indexing. + /// + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// The area resulting from the given parameters. + /// Throw when the inputs don't fit in the expected range. + /// The input parameters are assumed to always be positive. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeInt32Area(int height, int width, int pitch) + { + return checked(((width + pitch) * Max(unchecked(height - 1), 0)) + width); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Internals/ThrowHelper.cs b/Microsoft.Toolkit.HighPerformance/Memory/Internals/ThrowHelper.cs new file mode 100644 index 00000000000..4f372df3c1b --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Internals/ThrowHelper.cs @@ -0,0 +1,122 @@ +// 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; + +namespace Microsoft.Toolkit.HighPerformance.Memory.Internals +{ + /// + /// A helper class to throw exceptions for memory types. + /// + internal static class ThrowHelper + { + /// + /// Throws an when using the * constructor with a managed type. + /// + public static void ThrowArgumentExceptionForManagedType() + { + throw new ArgumentException("Can't use a void* constructor when T is a managed type"); + } + + /// + /// Throws an when the target span is too short. + /// + public static void ThrowArgumentExceptionForDestinationTooShort() + { + throw new ArgumentException("The target span is too short to copy all the current items to"); + } + + /// + /// Throws an when using an array of an invalid type. + /// + public static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException("The given array doesn't match the specified type T"); + } + + /// + /// Throws an when using an array of an invalid type. + /// + public static void ThrowArgumentExceptionForUnsupportedType() + { + throw new ArgumentException("The specified object type is not supported"); + } + + /// + /// Throws an when the a given coordinate is invalid. + /// + /// + /// Throwing is technically discouraged in the docs, but + /// we're doing that here for consistency with the official type(s) from the BCL. + /// + public static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + + /// + /// Throws an when more than one parameter are invalid. + /// + public static void ThrowArgumentException() + { + throw new ArgumentException("One or more input parameters were invalid"); + } + + /// + /// Throws an when the "depth" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForDepth() + { + throw new ArgumentOutOfRangeException("depth"); + } + + /// + /// Throws an when the "row" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForRow() + { + throw new ArgumentOutOfRangeException("row"); + } + + /// + /// Throws an when the "column" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForColumn() + { + throw new ArgumentOutOfRangeException("column"); + } + + /// + /// Throws an when the "offset" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForOffset() + { + throw new ArgumentOutOfRangeException("offset"); + } + + /// + /// Throws an when the "height" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForHeight() + { + throw new ArgumentOutOfRangeException("height"); + } + + /// + /// Throws an when the "width" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForWidth() + { + throw new ArgumentOutOfRangeException("width"); + } + + /// + /// Throws an when the "pitch" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForPitch() + { + throw new ArgumentOutOfRangeException("pitch"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs new file mode 100644 index 00000000000..6b3cb0cc82e --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs @@ -0,0 +1,906 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; +#endif +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +using Microsoft.Toolkit.HighPerformance.Memory.Views; +using static Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + /// represents a 2D region of arbitrary memory. It is to + /// what is to . For further details on how the internal layout + /// is structured, see the docs for . The type can wrap arrays + /// of any rank, provided that a valid series of parameters for the target memory area(s) are specified. + /// + /// The type of items in the current instance. + [DebuggerTypeProxy(typeof(MemoryDebugView2D<>))] + [DebuggerDisplay("{ToString(),raw}")] + public readonly struct Memory2D : IEquatable> + { + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The pitch of the specified 2D region. + /// + private readonly int pitch; + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public Memory2D(T[] array, int height, int width) + : this(array, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public Memory2D(T[] array, int offset, int height, int width, int pitch) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = array.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(offset)); + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// + /// Thrown when doesn't match . + /// + public Memory2D(T[,]? array) + { + if (array is null) + { + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + this.instance = array; + this.offset = GetArray2DDataByteOffset(); + this.height = array.GetLength(0); + this.width = array.GetLength(1); + this.pitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + public Memory2D(T[,]? array, int row, int column, int height, int width) + { + if (array is null) + { + if (row != 0 || column != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + int + rows = array.GetLength(0), + columns = array.GetLength(1); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(row, column)); + this.height = height; + this.width = width; + this.pitch = columns - width; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when a parameter is invalid. + public Memory2D(T[,,] array, int depth) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, 0, 0)); + this.height = array.GetLength(1); + this.width = array.GetLength(2); + this.pitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when a parameter is invalid. + public Memory2D(T[,,] array, int depth, int row, int column, int height, int width) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + int + rows = array.GetLength(1), + columns = array.GetLength(2); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, row, column)); + this.height = height; + this.width = width; + this.pitch = columns - width; + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public Memory2D(MemoryManager memoryManager, int height, int width) + : this(memoryManager, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public Memory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) + { + int length = memoryManager.GetSpan().Length; + + if ((uint)offset > (uint)length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = memoryManager; + this.offset = (nint)(uint)offset; + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + internal Memory2D(Memory memory, int height, int width) + : this(memory, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + internal Memory2D(Memory memory, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = memory.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + // Check if the Memory instance wraps a string. This is possible in case + // consumers do an unsafe cast for the entire Memory object, and while not + // really safe it is still supported in CoreCLR too, so we're following suit here. + if (typeof(T) == typeof(char) && + MemoryMarshal.TryGetString(Unsafe.As, Memory>(ref memory), out string? text, out int textStart, out _)) + { + ref char r0 = ref text.DangerousGetReferenceAt(textStart + offset); + + this.instance = text; + this.offset = text.DangerousGetObjectDataByteOffset(ref r0); + } + else if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + // Check if the input Memory instance wraps an array we can access. + // This is fine, since Memory on its own doesn't control the lifetime + // of the underlying array anyway, and this Memory2D type would do the same. + // Using the array directly makes retrieving a Span2D faster down the line, + // as we no longer have to jump through the boxed Memory first anymore. + T[] array = segment.Array!; + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(segment.Offset + offset)); + } + else if (MemoryMarshal.TryGetMemoryManager>(memory, out var memoryManager, out int memoryManagerStart, out _)) + { + this.instance = memoryManager; + this.offset = (nint)(uint)(memoryManagerStart + offset); + } + else + { + ThrowHelper.ThrowArgumentExceptionForUnsupportedType(); + + this.instance = null; + this.offset = default; + } + + this.height = height; + this.width = width; + this.pitch = pitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within . + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Memory2D(object instance, IntPtr offset, int height, int width, int pitch) + { + this.instance = instance; + this.offset = offset; + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Creates a new instance from an arbitrary object. + /// + /// The instance holding the data to map. + /// The target reference to point to (it must be within ). + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + /// A instance with the specified parameters. + /// The parameter is not validated, and it's responsability of the caller to ensure it's valid. + /// + /// Thrown when one of the input parameters is out of range. + /// + [Pure] + public static Memory2D DangerousCreate(object instance, ref T value, int height, int width, int pitch) + { + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + + IntPtr offset = instance.DangerousGetObjectDataByteOffset(ref value); + + return new Memory2D(instance, offset, height, width, pitch); + } + + /// + /// Gets an empty instance. + /// + public static Memory2D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the height of the underlying 2D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height; + } + + /// + /// Gets the width of the underlying 2D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets a instance from the current memory. + /// + public Span2D Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (!(this.instance is null)) + { +#if SPAN_RUNTIME_SUPPORT + if (this.instance is MemoryManager memoryManager) + { + ref T r0 = ref memoryManager.GetSpan().DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, this.offset); + + return new Span2D(ref r1, this.height, this.width, this.pitch); + } + else + { + ref T r0 = ref this.instance.DangerousGetObjectDataReferenceAt(this.offset); + + return new Span2D(ref r0, this.height, this.width, this.pitch); + } +#else + return new Span2D(this.instance, this.offset, this.height, this.width, this.pitch); +#endif + } + + return default; + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of rows to select. + /// The target range of columns to select. + /// + /// Thrown when either or are invalid. + /// + /// A new instance representing a slice of the current one. + public Memory2D this[Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var (row, height) = rows.GetOffsetAndLength(this.height); + var (column, width) = columns.GetOffsetAndLength(this.width); + + return Slice(row, column, height, width); + } + } +#endif + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The height to map within the current instance. + /// The width to map within the current instance. + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for the current instance. + /// + /// A new instance representing a slice of the current one. + [Pure] + public Memory2D Slice(int row, int column, int height, int width) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (Height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + int + shift = ((this.width + this.pitch) * row) + column, + pitch = this.pitch + (this.width - width); + + IntPtr offset = this.offset + (shift * Unsafe.SizeOf()); + + return new Memory2D(this.instance!, offset, height, width, pitch); + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Memory destination) => Span.CopyTo(destination.Span); + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Memory destination) => Span.TryCopyTo(destination.Span); + + /// + /// Copies the contents of this into a destination instance. + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Memory2D destination) => Span.CopyTo(destination.Span); + + /// + /// Attempts to copy the current instance to a destination . + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Memory2D destination) => Span.TryCopyTo(destination.Span); + + /// + /// Creates a handle for the memory. + /// The GC will not move the memory until the returned + /// is disposed, enabling taking and using the memory's address. + /// + /// + /// An instance with nonprimitive (non-blittable) members cannot be pinned. + /// + /// A instance wrapping the pinned handle. + public unsafe MemoryHandle Pin() + { + if (!(this.instance is null)) + { + if (this.instance is MemoryManager memoryManager) + { + return memoryManager.Pin(); + } + + GCHandle handle = GCHandle.Alloc(this.instance, GCHandleType.Pinned); + + void* pointer = Unsafe.AsPointer(ref this.instance.DangerousGetObjectDataReferenceAt(this.offset)); + + return new MemoryHandle(pointer, handle); + } + + return default; + } + + /// + /// Tries to get a instance, if the underlying buffer is contiguous and small enough. + /// + /// The resulting , in case of success. + /// Whether or not was correctly assigned. + public bool TryGetMemory(out Memory memory) + { + if (this.pitch == 0 && + Length <= int.MaxValue) + { + // Empty Memory2D instance + if (this.instance is null) + { + memory = default; + } + else if (typeof(T) == typeof(char) && this.instance.GetType() == typeof(string)) + { + string text = Unsafe.As(this.instance); + int index = text.AsSpan().IndexOf(in text.DangerousGetObjectDataReferenceAt(this.offset)); + ReadOnlyMemory temp = text.AsMemory(index, (int)Length); + + // The string type could still be present if a user ends up creating a + // Memory2D instance from a string using DangerousCreate. Similarly to + // how CoreCLR handles the equivalent case in Memory, here we just do + // the necessary steps to still retrieve a Memory instance correctly + // wrapping the target string. In this case, it is up to the caller + // to make sure not to ever actually write to the resulting Memory. + memory = MemoryMarshal.AsMemory(Unsafe.As, Memory>(ref temp)); + } + else if (this.instance is MemoryManager memoryManager) + { + unsafe + { + // If the object is a MemoryManager, just slice it as needed + memory = memoryManager.Memory.Slice((int)(void*)this.offset, this.height * this.width); + } + } + else if (this.instance.GetType() == typeof(T[])) + { + // If it's a T[] array, also handle the initial offset + T[] array = Unsafe.As(this.instance); + int index = array.AsSpan().IndexOf(ref array.DangerousGetObjectDataReferenceAt(this.offset)); + + memory = array.AsMemory(index, this.height * this.width); + } +#if SPAN_RUNTIME_SUPPORT + else if (this.instance.GetType() == typeof(T[,]) || + this.instance.GetType() == typeof(T[,,])) + { + // If the object is a 2D or 3D array, we can create a Memory from the RawObjectMemoryManager type. + // We just need to use the precomputed offset pointing to the first item in the current instance, + // and the current usable length. We don't need to retrieve the current index, as the manager just offsets. + memory = new RawObjectMemoryManager(this.instance, this.offset, this.height * this.width).Memory; + } +#endif + else + { + // Reuse a single failure path to reduce + // the number of returns in the method + goto Failure; + } + + return true; + } + + Failure: + + memory = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 2D array. + /// + /// A 2D array containing the data in the current instance. + [Pure] + public T[,] ToArray() => Span.ToArray(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + if (obj is Memory2D memory) + { + return Equals(memory); + } + + if (obj is ReadOnlyMemory2D readOnlyMemory) + { + return readOnlyMemory.Equals(this); + } + + return false; + } + + /// + public bool Equals(Memory2D other) + { + return + this.instance == other.instance && + this.offset == other.offset && + this.height == other.height && + this.width == other.width && + this.pitch == other.pitch; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + if (!(this.instance is null)) + { +#if !NETSTANDARD1_4 + return HashCode.Combine( + RuntimeHelpers.GetHashCode(this.instance), + this.offset, + this.height, + this.width, + this.pitch); +#else + Span values = stackalloc int[] + { + RuntimeHelpers.GetHashCode(this.instance), + this.offset.GetHashCode(), + this.height, + this.width, + this.pitch + }; + + return values.GetDjb2HashCode(); +#endif + } + + return 0; + } + + /// + public override string ToString() + { + return $"Microsoft.Toolkit.HighPerformance.Memory.Memory2D<{typeof(T)}>[{this.height}, {this.width}]"; + } + + /// + /// Defines an implicit conversion of an array to a + /// + public static implicit operator Memory2D(T[,]? array) => new Memory2D(array); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs new file mode 100644 index 00000000000..9b561233c1c --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs @@ -0,0 +1,924 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if SPAN_RUNTIME_SUPPORT +using Microsoft.Toolkit.HighPerformance.Buffers.Internals; +#endif +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +using Microsoft.Toolkit.HighPerformance.Memory.Views; +using static Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + /// A readonly version of . + /// + /// The type of items in the current instance. + [DebuggerTypeProxy(typeof(MemoryDebugView2D<>))] + [DebuggerDisplay("{ToString(),raw}")] + public readonly struct ReadOnlyMemory2D : IEquatable> + { + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The pitch of the specified 2D region. + /// + private readonly int pitch; + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public ReadOnlyMemory2D(string text, int height, int width) + : this(text, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public ReadOnlyMemory2D(string text, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)text.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = text.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = text; + this.offset = text.DangerousGetObjectDataByteOffset(ref text.DangerousGetReferenceAt(offset)); + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public ReadOnlyMemory2D(T[] array, int height, int width) + : this(array, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public ReadOnlyMemory2D(T[] array, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = array.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(offset)); + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + public ReadOnlyMemory2D(T[,]? array) + { + if (array is null) + { + this = default; + + return; + } + + this.instance = array; + this.offset = GetArray2DDataByteOffset(); + this.height = array.GetLength(0); + this.width = array.GetLength(1); + this.pitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + public ReadOnlyMemory2D(T[,]? array, int row, int column, int height, int width) + { + if (array is null) + { + if (row != 0 || column != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + int + rows = array.GetLength(0), + columns = array.GetLength(1); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(row, column)); + this.height = height; + this.width = width; + this.pitch = columns - width; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// Thrown when a parameter is invalid. + public ReadOnlyMemory2D(T[,,] array, int depth) + { + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, 0, 0)); + this.height = array.GetLength(1); + this.width = array.GetLength(2); + this.pitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// Thrown when a parameter is invalid. + public ReadOnlyMemory2D(T[,,] array, int depth, int row, int column, int height, int width) + { + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + int + rows = array.GetLength(1), + columns = array.GetLength(2); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, row, column)); + this.height = height; + this.width = width; + this.pitch = columns - width; + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public ReadOnlyMemory2D(MemoryManager memoryManager, int height, int width) + : this(memoryManager, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public ReadOnlyMemory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) + { + int length = memoryManager.GetSpan().Length; + + if ((uint)offset > (uint)length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = memoryManager; + this.offset = (nint)(uint)offset; + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + internal ReadOnlyMemory2D(ReadOnlyMemory memory, int height, int width) + : this(memory, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + internal ReadOnlyMemory2D(ReadOnlyMemory memory, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = memory.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + // Check the supported underlying objects, like in Memory2D + if (typeof(T) == typeof(char) && + MemoryMarshal.TryGetString(Unsafe.As, ReadOnlyMemory>(ref memory), out string? text, out int textStart, out _)) + { + ref char r0 = ref text.DangerousGetReferenceAt(textStart + offset); + + this.instance = text; + this.offset = text.DangerousGetObjectDataByteOffset(ref r0); + } + else if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + T[] array = segment.Array!; + + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(segment.Offset + offset)); + } + else if (MemoryMarshal.TryGetMemoryManager>(memory, out var memoryManager, out int memoryManagerStart, out _)) + { + this.instance = memoryManager; + this.offset = (nint)(uint)(memoryManagerStart + offset); + } + else + { + ThrowHelper.ThrowArgumentExceptionForUnsupportedType(); + + this.instance = null; + this.offset = default; + } + + this.height = height; + this.width = width; + this.pitch = pitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within . + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlyMemory2D(object instance, IntPtr offset, int height, int width, int pitch) + { + this.instance = instance; + this.offset = offset; + this.height = height; + this.width = width; + this.pitch = pitch; + } + + /// + /// Creates a new instance from an arbitrary object. + /// + /// The instance holding the data to map. + /// The target reference to point to (it must be within ). + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + /// A instance with the specified parameters. + /// The parameter is not validated, and it's responsability of the caller to ensure it's valid. + /// + /// Thrown when one of the input parameters is out of range. + /// + [Pure] + public static ReadOnlyMemory2D DangerousCreate(object instance, ref T value, int height, int width, int pitch) + { + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + + IntPtr offset = instance.DangerousGetObjectDataByteOffset(ref value); + + return new ReadOnlyMemory2D(instance, offset, height, width, pitch); + } + + /// + /// Gets an empty instance. + /// + public static ReadOnlyMemory2D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the height of the underlying 2D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height; + } + + /// + /// Gets the width of the underlying 2D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets a instance from the current memory. + /// + public ReadOnlySpan2D Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (!(this.instance is null)) + { +#if SPAN_RUNTIME_SUPPORT + if (this.instance is MemoryManager memoryManager) + { + ref T r0 = ref memoryManager.GetSpan().DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, this.offset); + + return new ReadOnlySpan2D(r1, this.height, this.width, this.pitch); + } + else + { + // This handles both arrays and strings + ref T r0 = ref this.instance.DangerousGetObjectDataReferenceAt(this.offset); + + return new ReadOnlySpan2D(r0, this.height, this.width, this.pitch); + } +#else + return new ReadOnlySpan2D(this.instance, this.offset, this.height, this.width, this.pitch); +#endif + } + + return default; + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of rows to select. + /// The target range of columns to select. + /// + /// Thrown when either or are invalid. + /// + /// A new instance representing a slice of the current one. + public ReadOnlyMemory2D this[Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var (row, height) = rows.GetOffsetAndLength(this.height); + var (column, width) = columns.GetOffsetAndLength(this.width); + + return Slice(row, column, height, width); + } + } +#endif + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The height to map within the current instance. + /// The width to map within the current instance. + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for the current instance. + /// + /// A new instance representing a slice of the current one. + [Pure] + public ReadOnlyMemory2D Slice(int row, int column, int height, int width) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (Height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + int + shift = ((this.width + this.pitch) * row) + column, + pitch = this.pitch + (this.width - width); + + IntPtr offset = this.offset + (shift * Unsafe.SizeOf()); + + return new ReadOnlyMemory2D(this.instance!, offset, height, width, pitch); + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Memory destination) => Span.CopyTo(destination.Span); + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Memory destination) => Span.TryCopyTo(destination.Span); + + /// + /// Copies the contents of this into a destination instance. + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Memory2D destination) => Span.CopyTo(destination.Span); + + /// + /// Attempts to copy the current instance to a destination . + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Memory2D destination) => Span.TryCopyTo(destination.Span); + + /// + /// Creates a handle for the memory. + /// The GC will not move the memory until the returned + /// is disposed, enabling taking and using the memory's address. + /// + /// + /// An instance with nonprimitive (non-blittable) members cannot be pinned. + /// + /// A instance wrapping the pinned handle. + public unsafe MemoryHandle Pin() + { + if (!(this.instance is null)) + { + if (this.instance is MemoryManager memoryManager) + { + return memoryManager.Pin(); + } + + GCHandle handle = GCHandle.Alloc(this.instance, GCHandleType.Pinned); + + void* pointer = Unsafe.AsPointer(ref this.instance.DangerousGetObjectDataReferenceAt(this.offset)); + + return new MemoryHandle(pointer, handle); + } + + return default; + } + + /// + /// Tries to get a instance, if the underlying buffer is contiguous and small enough. + /// + /// The resulting , in case of success. + /// Whether or not was correctly assigned. + public bool TryGetMemory(out ReadOnlyMemory memory) + { + if (this.pitch == 0 && + Length <= int.MaxValue) + { + // Empty Memory2D instance + if (this.instance is null) + { + memory = default; + } + else if (typeof(T) == typeof(char) && this.instance.GetType() == typeof(string)) + { + // Here we need to create a Memory from the wrapped string, and to do so we need to do an inverse + // lookup to find the initial index of the string with respect to the byte offset we're currently using, + // which refers to the raw string object data. This can include variable padding or other additional + // fields on different runtimes. The lookup operation is still O(1) and just computes the byte offset + // difference between the start of the Span (which directly wraps just the actual character data + // within the string), and the input reference, which we can get from the byte offset in use. The result + // is the character index which we can use to create the final Memory instance. + string text = Unsafe.As(this.instance); + int index = text.AsSpan().IndexOf(in text.DangerousGetObjectDataReferenceAt(this.offset)); + ReadOnlyMemory temp = text.AsMemory(index, (int)Length); + + memory = Unsafe.As, ReadOnlyMemory>(ref temp); + } + else if (this.instance is MemoryManager memoryManager) + { + unsafe + { + // If the object is a MemoryManager, just slice it as needed + memory = memoryManager.Memory.Slice((int)(void*)this.offset, this.height * this.width); + } + } + else if (this.instance.GetType() == typeof(T[])) + { + // If it's a T[] array, also handle the initial offset + T[] array = Unsafe.As(this.instance); + int index = array.AsSpan().IndexOf(ref array.DangerousGetObjectDataReferenceAt(this.offset)); + + memory = array.AsMemory(index, this.height * this.width); + } +#if SPAN_RUNTIME_SUPPORT + else if (this.instance.GetType() == typeof(T[,]) || + this.instance.GetType() == typeof(T[,,])) + { + memory = new RawObjectMemoryManager(this.instance, this.offset, this.height * this.width).Memory; + } +#endif + else + { + // Reuse a single failure path to reduce + // the number of returns in the method + goto Failure; + } + + return true; + } + + Failure: + + memory = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 2D array. + /// + /// A 2D array containing the data in the current instance. + [Pure] + public T[,] ToArray() => Span.ToArray(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + if (obj is ReadOnlyMemory2D readOnlyMemory) + { + return Equals(readOnlyMemory); + } + + if (obj is Memory2D memory) + { + return Equals(memory); + } + + return false; + } + + /// + public bool Equals(ReadOnlyMemory2D other) + { + return + this.instance == other.instance && + this.offset == other.offset && + this.height == other.height && + this.width == other.width && + this.pitch == other.pitch; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + if (!(this.instance is null)) + { +#if !NETSTANDARD1_4 + return HashCode.Combine( + RuntimeHelpers.GetHashCode(this.instance), + this.offset, + this.height, + this.width, + this.pitch); +#else + Span values = stackalloc int[] + { + RuntimeHelpers.GetHashCode(this.instance), + this.offset.GetHashCode(), + this.height, + this.width, + this.pitch + }; + + return values.GetDjb2HashCode(); +#endif + } + + return 0; + } + + /// + public override string ToString() + { + return $"Microsoft.Toolkit.HighPerformance.Memory.ReadOnlyMemory2D<{typeof(T)}>[{this.height}, {this.width}]"; + } + + /// + /// Defines an implicit conversion of an array to a + /// + public static implicit operator ReadOnlyMemory2D(T[,]? array) => new ReadOnlyMemory2D(array); + + /// + /// Defines an implicit conversion of a to a + /// + public static implicit operator ReadOnlyMemory2D(Memory2D memory) => Unsafe.As, ReadOnlyMemory2D>(ref memory); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs new file mode 100644 index 00000000000..30dfcbc6406 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs @@ -0,0 +1,201 @@ +// 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 Microsoft.Toolkit.HighPerformance.Enumerables; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +#if SPAN_RUNTIME_SUPPORT +using System.Runtime.InteropServices; +#else +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + public readonly ref partial struct ReadOnlySpan2D + { + /// + /// Gets an enumerable that traverses items in a specified row. + /// + /// The target row to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRefEnumerable GetRow(int row) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + nint startIndex = (nint)(uint)this.stride * (nint)(uint)row; + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if SPAN_RUNTIME_SUPPORT + return new ReadOnlyRefEnumerable(r1, Width, 1); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.instance, ref r1); + + return new ReadOnlyRefEnumerable(this.instance!, offset, this.width, 1); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified column. + /// + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRefEnumerable GetColumn(int column) + { + if ((uint)column >= Width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, (nint)(uint)column); + +#if SPAN_RUNTIME_SUPPORT + return new ReadOnlyRefEnumerable(r1, Height, this.stride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.instance, ref r1); + + return new ReadOnlyRefEnumerable(this.instance!, offset, Height, this.stride); +#endif + } + + /// + /// Returns an enumerator for the current instance. + /// + /// + /// An enumerator that can be used to traverse the items in the current instance + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Provides an enumerator for the elements of a instance. + /// + public ref struct Enumerator + { +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + /// Just like in , the length is the height of the 2D region. + private readonly ReadOnlySpan span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; +#endif + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The stride of the specified 2D region. + /// + private readonly int stride; + + /// + /// The current horizontal offset. + /// + private int x; + + /// + /// The current vertical offset. + /// + private int y; + + /// + /// Initializes a new instance of the struct. + /// + /// The target instance to enumerate. + internal Enumerator(ReadOnlySpan2D span) + { +#if SPAN_RUNTIME_SUPPORT + this.span = span.span; +#else + this.instance = span.instance; + this.offset = span.offset; + this.height = span.height; +#endif + this.width = span.width; + this.stride = span.stride; + this.x = -1; + this.y = 0; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int x = this.x + 1; + + // Horizontal move, within range + if (x < this.width) + { + this.x = x; + + return true; + } + + // We reached the end of a row and there is at least + // another row available: wrap to a new line and continue. + this.x = 0; + +#if SPAN_RUNTIME_SUPPORT + return ++this.y < this.span.Length; +#else + return ++this.y < this.height; +#endif + } + + /// + /// Gets the duck-typed property. + /// + public readonly ref readonly T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)this.y * (nint)(uint)this.stride) + (nint)(uint)this.x; + + return ref Unsafe.Add(ref r0, index); + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs new file mode 100644 index 00000000000..6ef680eb263 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs @@ -0,0 +1,1020 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +using Microsoft.Toolkit.HighPerformance.Memory.Views; +#if !SPAN_RUNTIME_SUPPORT +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + /// A readonly version of . + /// + /// The type of items in the current instance. + [DebuggerTypeProxy(typeof(MemoryDebugView2D<>))] + [DebuggerDisplay("{ToString(),raw}")] + public readonly ref partial struct ReadOnlySpan2D + { +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + private readonly ReadOnlySpan span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; +#endif + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The stride of the specified 2D region. + /// + private readonly int stride; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlySpan2D(in T value, int height, int width, int pitch) + { + this.span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(value), height); + this.width = width; + this.stride = width + pitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The pointer to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// Thrown when one of the parameters are negative. + public unsafe ReadOnlySpan2D(void* pointer, int height, int width, int pitch) + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentExceptionForManagedType(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + +#if SPAN_RUNTIME_SUPPORT + this.span = new ReadOnlySpan(pointer, height); +#else + this.instance = null; + this.offset = (IntPtr)pointer; + this.height = height; +#endif + this.width = width; + this.stride = width + pitch; + } + +#if !SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within the target instance. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlySpan2D(object? instance, IntPtr offset, int height, int width, int pitch) + { + this.instance = instance; + this.offset = offset; + this.height = height; + this.width = width; + this.stride = width + pitch; + } +#endif + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public ReadOnlySpan2D(T[] array, int height, int width) + : this(array, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public ReadOnlySpan2D(T[] array, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = array.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReferenceAt(offset), height); +#else + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(offset)); + this.height = height; +#endif + this.width = width; + this.stride = width + pitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + public ReadOnlySpan2D(T[,]? array) + { + if (array is null) + { + this = default; + + return; + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReference(), array.GetLength(0)); +#else + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(0, 0)); + this.height = array.GetLength(0); +#endif + this.width = this.stride = array.GetLength(1); + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + public ReadOnlySpan2D(T[,]? array, int row, int column, int height, int width) + { + if (array is null) + { + if (row != 0 || column != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + int + rows = array.GetLength(0), + columns = array.GetLength(1); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReferenceAt(row, column), height); +#else + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(row, column)); + this.height = height; +#endif + this.width = width; + this.stride = columns; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// Thrown when a parameter is invalid. + public ReadOnlySpan2D(T[,,] array, int depth) + { + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReferenceAt(depth, 0, 0), array.GetLength(1)); +#else + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, 0, 0)); + this.height = array.GetLength(1); +#endif + this.width = this.stride = array.GetLength(2); + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// Thrown when a parameter is invalid. + public ReadOnlySpan2D(T[,,] array, int depth, int row, int column, int height, int width) + { + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + int + rows = array.GetLength(1), + columns = array.GetLength(2); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReferenceAt(depth, row, column), height); +#else + this.instance = array; + this.offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, row, column)); + this.height = height; +#endif + this.width = width; + this.stride = columns; + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + internal ReadOnlySpan2D(ReadOnlySpan span, int height, int width) + : this(span, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + internal ReadOnlySpan2D(ReadOnlySpan span, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)span.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = span.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.span = MemoryMarshal.CreateSpan(ref span.DangerousGetReferenceAt(offset), height); + this.width = width; + this.stride = width + pitch; + } + + /// + /// Creates a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// A instance with the specified parameters. + /// Thrown when one of the parameters are negative. + [Pure] + public static ReadOnlySpan2D DangerousCreate(in T value, int height, int width, int pitch) + { + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + + return new ReadOnlySpan2D(value, height, width, pitch); + } +#endif + + /// + /// Gets an empty instance. + /// + public static ReadOnlySpan2D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)Height * (nint)(uint)this.width; + } + + /// + /// Gets the height of the underlying 2D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + return this.span.Length; +#else + return this.height; +#endif + } + } + + /// + /// Gets the width of the underlying 2D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either or are invalid. + /// + public ref readonly T this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if ((uint)row >= (uint)Height || + (uint)column >= (uint)Width) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } + + return ref DangerousGetReferenceAt(row, column); + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either or are invalid. + /// + public ref readonly T this[Index row, Index column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this[row.GetOffset(Height), column.GetOffset(this.width)]; + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of rows to select. + /// The target range of columns to select. + /// + /// Thrown when either or are invalid. + /// + /// A new instance representing a slice of the current one. + public ReadOnlySpan2D this[Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var (row, height) = rows.GetOffsetAndLength(Height); + var (column, width) = columns.GetOffsetAndLength(this.width); + + return Slice(row, column, height, width); + } + } +#endif + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out ReadOnlySpan span)) + { + span.CopyTo(destination); + } + else + { + if (Length > destination.Length) + { + ThrowHelper.ThrowArgumentExceptionForDestinationTooShort(); + } + + // Copy each row individually +#if SPAN_RUNTIME_SUPPORT + for (int i = 0, j = 0; i < Height; i++, j += this.width) + { + GetRowSpan(i).CopyTo(destination.Slice(j)); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nint offset = 0; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1, offset += 1) + { + Unsafe.Add(ref destinationRef, offset) = Unsafe.Add(ref sourceRef, j); + } + } +#endif + } + } + + /// + /// Copies the contents of this into a destination instance. + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span2D destination) + { + if (destination.Height != Height || + destination.Width != Width) + { + ThrowHelper.ThrowArgumentException(); + } + + if (IsEmpty) + { + return; + } + + if (destination.TryGetSpan(out Span span)) + { + CopyTo(span); + } + else + { + // Copy each row individually +#if SPAN_RUNTIME_SUPPORT + for (int i = 0; i < Height; i++) + { + GetRowSpan(i).CopyTo(destination.GetRowSpan(i)); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + ref T destinationRef = ref destination.DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1) + { + Unsafe.Add(ref destinationRef, j) = Unsafe.Add(ref sourceRef, j); + } + } +#endif + } + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span destination) + { + if (destination.Length >= Length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span2D destination) + { + if (destination.Height == Height && + destination.Width == Width) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Returns a reference to the 0th element of the instance. If the current + /// instance is empty, returns a reference. It can be used for pinning + /// and is required to support the use of span within a fixed statement. + /// + /// A reference to the 0th element, or a reference. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe ref T GetPinnableReference() + { + ref T r0 = ref Unsafe.AsRef(null); + + if (Length != 0) + { +#if SPAN_RUNTIME_SUPPORT + r0 = ref MemoryMarshal.GetReference(this.span); +#else + r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + } + + return ref r0; + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { +#if SPAN_RUNTIME_SUPPORT + return ref MemoryMarshal.GetReference(this.span); +#else + return ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + } + + /// + /// Returns a reference to a specified element within the current instance, with no bounds check. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReferenceAt(int i, int j) + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)i * (nint)(uint)this.stride) + (nint)(uint)j; + + return ref Unsafe.Add(ref r0, index); + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The width to map within the current instance. + /// The height to map within the current instance. + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for the current instance. + /// + /// A new instance representing a slice of the current one. + [Pure] + public ReadOnlySpan2D Slice(int row, int column, int width, int height) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if ((uint)height > (Height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + nint shift = ((nint)(uint)this.stride * (nint)(uint)row) + (nint)(uint)column; + int pitch = this.stride - width; + +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref this.span.DangerousGetReferenceAt(shift); + + return new ReadOnlySpan2D(r0, height, width, pitch); +#else + IntPtr offset = this.offset + (shift * (nint)(uint)Unsafe.SizeOf()); + + return new ReadOnlySpan2D(this.instance, offset, height, width, pitch); +#endif + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Gets a for a specified row. + /// + /// The index of the target row to retrieve. + /// Throw when is out of range. + /// The resulting row . + [Pure] + public ReadOnlySpan GetRowSpan(int row) + { + if ((uint)row >= (uint)Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref DangerousGetReferenceAt(row, 0); + + return MemoryMarshal.CreateReadOnlySpan(ref r0, this.width); + } +#endif + + /// + /// Tries to get a instance, if the underlying buffer is contiguous and small enough. + /// + /// The resulting , in case of success. + /// Whether or not was correctly assigned. + public bool TryGetSpan(out ReadOnlySpan span) + { + // We can only create a Span if the buffer is contiguous + if (this.stride == this.width && + Length <= int.MaxValue) + { +#if SPAN_RUNTIME_SUPPORT + span = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(this.span), (int)Length); + + return true; +#else + // An empty Span2D is still valid + if (IsEmpty) + { + span = default; + + return true; + } + + // Pinned ReadOnlySpan2D + if (this.instance is null) + { + unsafe + { + span = new Span((void*)this.offset, (int)Length); + } + + return true; + } + + // Without Span runtime support, we can only get a Span from a T[] instance + if (this.instance.GetType() == typeof(T[])) + { + span = Unsafe.As(this.instance).AsSpan((int)this.offset, (int)Length); + + return true; + } +#endif + } + + span = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 2D array. + /// + /// A 2D array containing the data in the current instance. + [Pure] + public T[,] ToArray() + { + T[,] array = new T[Height, this.width]; + +#if SPAN_RUNTIME_SUPPORT + CopyTo(array.AsSpan()); +#else + // Skip the initialization if the array is empty + if (Length > 0) + { + int height = Height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref array.DangerousGetReference(); + nint offset = 0; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1, offset += 1) + { + Unsafe.Add(ref destinationRef, offset) = Unsafe.Add(ref sourceRef, j); + } + } + } +#endif + + return array; + } + +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Equals() on Span will always throw an exception. Use == instead.")] + public override bool Equals(object? obj) + { + throw new NotSupportedException("Microsoft.Toolkit.HighPerformance.ReadOnlySpan2D.Equals(object) is not supported"); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("GetHashCode() on Span will always throw an exception.")] + public override int GetHashCode() + { + throw new NotSupportedException("Microsoft.Toolkit.HighPerformance.ReadOnlySpan2D.GetHashCode() is not supported"); + } +#pragma warning restore CS0809 + + /// + public override string ToString() + { + return $"Microsoft.Toolkit.HighPerformance.Memory.ReadOnlySpan2D<{typeof(T)}>[{Height}, {this.width}]"; + } + + /// + /// Checks whether two instances are equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are equal. + public static bool operator ==(ReadOnlySpan2D left, ReadOnlySpan2D right) + { + return +#if SPAN_RUNTIME_SUPPORT + left.span == right.span && +#else + ReferenceEquals(left.instance, right.instance) && + left.offset == right.offset && + left.height == right.height && +#endif + left.width == right.width && + left.stride == right.stride; + } + + /// + /// Checks whether two instances are not equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are not equal. + public static bool operator !=(ReadOnlySpan2D left, ReadOnlySpan2D right) + { + return !(left == right); + } + + /// + /// Implicily converts a given 2D array into a instance. + /// + /// The input 2D array to convert. + public static implicit operator ReadOnlySpan2D(T[,]? array) => new ReadOnlySpan2D(array); + + /// + /// Implicily converts a given into a instance. + /// + /// The input to convert. + public static implicit operator ReadOnlySpan2D(Span2D span) + { +#if SPAN_RUNTIME_SUPPORT + return new ReadOnlySpan2D(span.DangerousGetReference(), span.Height, span.Width, span.Stride - span.Width); +#else + return new ReadOnlySpan2D(span.Instance!, span.Offset, span.Height, span.Width, span.Stride - span.Width); +#endif + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs new file mode 100644 index 00000000000..fc5baf9b930 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs @@ -0,0 +1,201 @@ +// 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 Microsoft.Toolkit.HighPerformance.Enumerables; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +#if SPAN_RUNTIME_SUPPORT +using System.Runtime.InteropServices; +#else +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + public readonly ref partial struct Span2D + { + /// + /// Gets an enumerable that traverses items in a specified row. + /// + /// The target row to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefEnumerable GetRow(int row) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + nint startIndex = (nint)(uint)this.Stride * (nint)(uint)row; + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if SPAN_RUNTIME_SUPPORT + return new RefEnumerable(ref r1, Width, 1); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.Instance, ref r1); + + return new RefEnumerable(this.Instance, offset, this.width, 1); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified column. + /// + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefEnumerable GetColumn(int column) + { + if ((uint)column >= Width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, (nint)(uint)column); + +#if SPAN_RUNTIME_SUPPORT + return new RefEnumerable(ref r1, Height, this.Stride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.Instance, ref r1); + + return new RefEnumerable(this.Instance, offset, Height, this.Stride); +#endif + } + + /// + /// Returns an enumerator for the current instance. + /// + /// + /// An enumerator that can be used to traverse the items in the current instance + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Provides an enumerator for the elements of a instance. + /// + public ref struct Enumerator + { +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + /// Just like in , the length is the height of the 2D region. + private readonly Span span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial offset within . + /// + private readonly IntPtr offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; +#endif + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The stride of the specified 2D region. + /// + private readonly int stride; + + /// + /// The current horizontal offset. + /// + private int x; + + /// + /// The current vertical offset. + /// + private int y; + + /// + /// Initializes a new instance of the struct. + /// + /// The target instance to enumerate. + internal Enumerator(Span2D span) + { +#if SPAN_RUNTIME_SUPPORT + this.span = span.span; +#else + this.instance = span.Instance; + this.offset = span.Offset; + this.height = span.height; +#endif + this.width = span.width; + this.stride = span.Stride; + this.x = -1; + this.y = 0; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int x = this.x + 1; + + // Horizontal move, within range + if (x < this.width) + { + this.x = x; + + return true; + } + + // We reached the end of a row and there is at least + // another row available: wrap to a new line and continue. + this.x = 0; + +#if SPAN_RUNTIME_SUPPORT + return ++this.y < this.span.Length; +#else + return ++this.y < this.height; +#endif + } + + /// + /// Gets the duck-typed property. + /// + public readonly ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)this.y * (nint)(uint)this.stride) + (nint)(uint)this.x; + + return ref Unsafe.Add(ref r0, index); + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs new file mode 100644 index 00000000000..fe230070c5d --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs @@ -0,0 +1,1157 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory.Internals; +using Microsoft.Toolkit.HighPerformance.Memory.Views; +#if !SPAN_RUNTIME_SUPPORT +using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace Microsoft.Toolkit.HighPerformance.Memory +{ + /// + /// represents a 2D region of arbitrary memory. Like the type, + /// it can point to either managed or native memory, or to memory allocated on the stack. It is type- and memory-safe. + /// One key difference with and arrays is that the underlying buffer for a + /// instance might not be contiguous in memory: this is supported to enable mapping arbitrary 2D regions even if they + /// require padding between boundaries of sequential rows. All this logic is handled internally by the + /// type and it is transparent to the user, but note that working over discontiguous buffers has a performance impact. + /// + /// The type of items in the current instance. + [DebuggerTypeProxy(typeof(MemoryDebugView2D<>))] + [DebuggerDisplay("{ToString(),raw}")] + public readonly ref partial struct Span2D + { + // Let's consider a representation of a discontiguous 2D memory + // region within an existing array. The data is represented in + // row-major order as usual, and the 'XX' grid cells represent + // locations that are mapped by a given Span2D instance: + // + // _____________________stride_____... + // reference__ /________width_________ ________... + // \/ \/ + // | -- | -- | |- | -- | -- | -- | -- | -- | -- | -- |_ + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | |_height + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- |_| + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // ...__pitch__/ + // ...________/ + // + // The pitch is used to calculate the offset between each + // discontiguous row, so that any arbitrary memory locations + // can be used to internally represent a 2D span. This gives + // users much more flexibility when creating spans from data. +#if SPAN_RUNTIME_SUPPORT + /// + /// The instance pointing to the first item in the target memory area. + /// + /// + /// The field maps to the height of the 2D region. + /// This is done to save 4 bytes in the layout of the type. + /// + private readonly Span span; +#else + /// + /// The target instance, if present. + /// + internal readonly object? Instance; + + /// + /// The initial offset within . + /// + internal readonly IntPtr Offset; + + /// + /// The height of the specified 2D region. + /// + private readonly int height; +#endif + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The stride of the specified 2D region. + /// + /// + /// This combines both the width and pitch in a single value so that the indexing + /// logic can be simplified (no need to recompute the sum every time) and be faster. + /// + internal readonly int Stride; + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Span2D(ref T value, int height, int width, int pitch) + { + this.span = MemoryMarshal.CreateSpan(ref value, height); + this.width = width; + this.Stride = width + pitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The pointer to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// Thrown when one of the parameters are negative. + public unsafe Span2D(void* pointer, int height, int width, int pitch) + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentExceptionForManagedType(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + +#if SPAN_RUNTIME_SUPPORT + this.span = new Span(pointer, height); +#else + this.Instance = null; + this.Offset = (IntPtr)pointer; + this.height = height; +#endif + this.width = width; + this.Stride = width + pitch; + } + +#if !SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within the target instance. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Span2D(object? instance, IntPtr offset, int height, int width, int pitch) + { + this.Instance = instance; + this.Offset = offset; + this.height = height; + this.width = width; + this.Stride = width + pitch; + } +#endif + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + public Span2D(T[] array, int height, int width) + : this(array, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + public Span2D(T[] array, int offset, int height, int width, int pitch) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = array.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(offset), height); +#else + this.Instance = array; + this.Offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(offset)); + this.height = height; +#endif + this.width = width; + this.Stride = width + pitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// + /// Thrown when doesn't match . + /// + public Span2D(T[,]? array) + { + if (array is null) + { + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReference(), array.GetLength(0)); +#else + this.Instance = array; + this.Offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(0, 0)); + this.height = array.GetLength(0); +#endif + this.width = this.Stride = array.GetLength(1); + } + + /// + /// Initializes a new instance of the struct wrapping a 2D array. + /// + /// The given 2D array to wrap. + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for . + /// + public Span2D(T[,]? array, int row, int column, int height, int width) + { + if (array is null) + { + if (row != 0 || column != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + int + rows = array.GetLength(0), + columns = array.GetLength(1); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(row, column), height); +#else + this.Instance = array; + this.Offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(row, column)); + this.height = height; +#endif + this.width = width; + this.Stride = columns; + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when a parameter is invalid. + public Span2D(T[,,] array, int depth) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(depth, 0, 0), array.GetLength(1)); +#else + this.Instance = array; + this.Offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, 0, 0)); + this.height = array.GetLength(1); +#endif + this.width = this.Stride = array.GetLength(2); + } + + /// + /// Initializes a new instance of the struct wrapping a layer in a 3D array. + /// + /// The given 3D array to wrap. + /// The target layer to map within . + /// The target row to map within . + /// The target column to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// Thrown when a parameter is invalid. + public Span2D(T[,,] array, int depth, int row, int column, int height, int width) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)depth >= (uint)array.GetLength(0)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + int + rows = array.GetLength(1), + columns = array.GetLength(2); + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if SPAN_RUNTIME_SUPPORT + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(depth, row, column), height); +#else + this.Instance = array; + this.Offset = array.DangerousGetObjectDataByteOffset(ref array.DangerousGetReferenceAt(depth, row, column)); + this.height = height; +#endif + this.width = width; + this.Stride = columns; + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// + /// Thrown when either or are invalid. + /// + /// The total area must match the length of . + internal Span2D(Span span, int height, int width) + : this(span, 0, height, width, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The height of the resulting 2D area. + /// The width of each row in the resulting 2D area. + /// The pitch in the resulting 2D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested area is outside of bounds for . + /// + internal Span2D(Span span, int offset, int height, int width, int pitch) + { + if ((uint)offset > (uint)span.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + if (width == 0 || height == 0) + { + this = default; + + return; + } + + int + area = OverflowHelper.ComputeInt32Area(height, width, pitch), + remaining = span.Length - offset; + + if (area > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.span = MemoryMarshal.CreateSpan(ref span.DangerousGetReferenceAt(offset), height); + this.width = width; + this.Stride = width + pitch; + } + + /// + /// Creates a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The height of the 2D memory area to map. + /// The width of the 2D memory area to map. + /// The pitch of the 2D memory area to map (the distance between each row). + /// A instance with the specified parameters. + /// Thrown when one of the parameters are negative. + [Pure] + public static Span2D DangerousCreate(ref T value, int height, int width, int pitch) + { + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (pitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); + + return new Span2D(ref value, height, width, pitch); + } +#endif + + /// + /// Gets an empty instance. + /// + public static Span2D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)Height * (nint)(uint)this.width; + } + + /// + /// Gets the height of the underlying 2D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if SPAN_RUNTIME_SUPPORT + return this.span.Length; +#else + return this.height; +#endif + } + } + + /// + /// Gets the width of the underlying 2D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either or are invalid. + /// + public ref T this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if ((uint)row >= (uint)Height || + (uint)column >= (uint)this.width) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } + + return ref DangerousGetReferenceAt(row, column); + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either or are invalid. + /// + public ref T this[Index row, Index column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this[row.GetOffset(Height), column.GetOffset(this.width)]; + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of rows to select. + /// The target range of columns to select. + /// + /// Thrown when either or are invalid. + /// + /// A new instance representing a slice of the current one. + public Span2D this[Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var (row, height) = rows.GetOffsetAndLength(Height); + var (column, width) = columns.GetOffsetAndLength(this.width); + + return Slice(row, column, height, width); + } + } +#endif + + /// + /// Clears the contents of the current instance. + /// + public void Clear() + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.Clear(); + } + else + { + // Clear one row at a time +#if SPAN_RUNTIME_SUPPORT + for (int i = 0; i < Height; i++) + { + GetRowSpan(i).Clear(); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + for (int i = 0; i < height; i++) + { + ref T r0 = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1) + { + Unsafe.Add(ref r0, j) = default!; + } + } +#endif + } + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.CopyTo(destination); + } + else + { + if (Length > destination.Length) + { + ThrowHelper.ThrowArgumentExceptionForDestinationTooShort(); + } + + // Copy each row individually +#if SPAN_RUNTIME_SUPPORT + for (int i = 0, j = 0; i < Height; i++, j += this.width) + { + GetRowSpan(i).CopyTo(destination.Slice(j)); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nint offset = 0; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1, offset += 1) + { + Unsafe.Add(ref destinationRef, offset) = Unsafe.Add(ref sourceRef, j); + } + } +#endif + } + } + + /// + /// Copies the contents of this into a destination instance. + /// For this API to succeed, the target has to have the same shape as the current one. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span2D destination) + { + if (destination.Height != Height || + destination.width != this.width) + { + ThrowHelper.ThrowArgumentException(); + } + + if (IsEmpty) + { + return; + } + + if (destination.TryGetSpan(out Span span)) + { + CopyTo(span); + } + else + { + // Copy each row individually +#if SPAN_RUNTIME_SUPPORT + for (int i = 0; i < Height; i++) + { + GetRowSpan(i).CopyTo(destination.GetRowSpan(i)); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + ref T destinationRef = ref destination.DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1) + { + Unsafe.Add(ref destinationRef, j) = Unsafe.Add(ref sourceRef, j); + } + } +#endif + } + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span destination) + { + if (destination.Length >= Length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Attempts to copy the current instance to a destination . + /// + /// The target of the copy operation. + /// Whether or not the operation was successful. + public bool TryCopyTo(Span2D destination) + { + if (destination.Height == Height && + destination.Width == this.width) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Fills the elements of this span with a specified value. + /// + /// The value to assign to each element of the instance. + public void Fill(T value) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.Fill(value); + } + else + { + // Fill one row at a time +#if SPAN_RUNTIME_SUPPORT + for (int i = 0; i < Height; i++) + { + GetRowSpan(i).Fill(value); + } +#else + int height = Height; + nint width = (nint)(uint)this.width; + + for (int i = 0; i < height; i++) + { + ref T r0 = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1) + { + Unsafe.Add(ref r0, j) = value; + } + } +#endif + } + } + + /// + /// Returns a reference to the 0th element of the instance. If the current + /// instance is empty, returns a reference. It can be used for pinning + /// and is required to support the use of span within a fixed statement. + /// + /// A reference to the 0th element, or a reference. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe ref T GetPinnableReference() + { + ref T r0 = ref Unsafe.AsRef(null); + + if (Length != 0) + { +#if SPAN_RUNTIME_SUPPORT + r0 = ref MemoryMarshal.GetReference(this.span); +#else + r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + } + + return ref r0; + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { +#if SPAN_RUNTIME_SUPPORT + return ref MemoryMarshal.GetReference(this.span); +#else + return ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + } + + /// + /// Returns a reference to a specified element within the current instance, with no bounds check. + /// + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReferenceAt(int i, int j) + { +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + nint index = ((nint)(uint)i * (nint)(uint)this.Stride) + (nint)(uint)j; + + return ref Unsafe.Add(ref r0, index); + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The height to map within the current instance. + /// The width to map within the current instance. + /// + /// Thrown when either , or + /// are negative or not within the bounds that are valid for the current instance. + /// + /// A new instance representing a slice of the current one. + [Pure] + public Span2D Slice(int row, int column, int height, int width) + { + if ((uint)row >= Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)height > (Height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + nint shift = ((nint)(uint)this.Stride * (nint)(uint)row) + (nint)(uint)column; + int pitch = this.Stride - width; + +#if SPAN_RUNTIME_SUPPORT + ref T r0 = ref this.span.DangerousGetReferenceAt(shift); + + return new Span2D(ref r0, height, width, pitch); +#else + IntPtr offset = this.Offset + (shift * (nint)(uint)Unsafe.SizeOf()); + + return new Span2D(this.Instance, offset, height, width, pitch); +#endif + } + +#if SPAN_RUNTIME_SUPPORT + /// + /// Gets a for a specified row. + /// + /// The index of the target row to retrieve. + /// Throw when is out of range. + /// The resulting row . + [Pure] + public Span GetRowSpan(int row) + { + if ((uint)row >= (uint)Height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref DangerousGetReferenceAt(row, 0); + + return MemoryMarshal.CreateSpan(ref r0, this.width); + } +#endif + + /// + /// Tries to get a instance, if the underlying buffer is contiguous and small enough. + /// + /// The resulting , in case of success. + /// Whether or not was correctly assigned. + public bool TryGetSpan(out Span span) + { + // We can only create a Span if the buffer is contiguous + if (this.Stride == this.width && + Length <= int.MaxValue) + { +#if SPAN_RUNTIME_SUPPORT + span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(this.span), (int)Length); + + return true; +#else + // An empty Span2D is still valid + if (IsEmpty) + { + span = default; + + return true; + } + + // Pinned Span2D + if (this.Instance is null) + { + unsafe + { + span = new Span((void*)this.Offset, (int)Length); + } + + return true; + } + + // Without Span runtime support, we can only get a Span from a T[] instance + if (this.Instance.GetType() == typeof(T[])) + { + span = Unsafe.As(this.Instance).AsSpan((int)this.Offset, (int)Length); + + return true; + } +#endif + } + + span = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 2D array. + /// + /// A 2D array containing the data in the current instance. + [Pure] + public T[,] ToArray() + { + T[,] array = new T[Height, this.width]; + +#if SPAN_RUNTIME_SUPPORT + CopyTo(array.AsSpan()); +#else + // Skip the initialization if the array is empty + if (Length > 0) + { + int height = Height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref array.DangerousGetReference(); + nint offset = 0; + + for (int i = 0; i < height; i++) + { + ref T sourceRef = ref DangerousGetReferenceAt(i, 0); + + for (nint j = 0; j < width; j += 1, offset += 1) + { + Unsafe.Add(ref destinationRef, offset) = Unsafe.Add(ref sourceRef, j); + } + } + } +#endif + + return array; + } + +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Equals() on Span will always throw an exception. Use == instead.")] + public override bool Equals(object? obj) + { + throw new NotSupportedException("Microsoft.Toolkit.HighPerformance.Span2D.Equals(object) is not supported"); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("GetHashCode() on Span will always throw an exception.")] + public override int GetHashCode() + { + throw new NotSupportedException("Microsoft.Toolkit.HighPerformance.Span2D.GetHashCode() is not supported"); + } +#pragma warning restore CS0809 + + /// + public override string ToString() + { + return $"Microsoft.Toolkit.HighPerformance.Memory.Span2D<{typeof(T)}>[{Height}, {this.width}]"; + } + + /// + /// Checks whether two instances are equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are equal. + public static bool operator ==(Span2D left, Span2D right) + { + return +#if SPAN_RUNTIME_SUPPORT + left.span == right.span && +#else + ReferenceEquals(left.Instance, right.Instance) && + left.Offset == right.Offset && + left.height == right.height && +#endif + left.width == right.width && + left.Stride == right.Stride; + } + + /// + /// Checks whether two instances are not equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are not equal. + public static bool operator !=(Span2D left, Span2D right) + { + return !(left == right); + } + + /// + /// Implicily converts a given 2D array into a instance. + /// + /// The input 2D array to convert. + public static implicit operator Span2D(T[,]? array) => new Span2D(array); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Views/MemoryDebugView2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/Views/MemoryDebugView2D{T}.cs new file mode 100644 index 00000000000..9431210fc44 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Memory/Views/MemoryDebugView2D{T}.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics; + +namespace Microsoft.Toolkit.HighPerformance.Memory.Views +{ + /// + /// A debug proxy used to display items in a 2D layout. + /// + /// The type of items to display. + internal sealed class MemoryDebugView2D + { + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView2D(Memory2D memory) + { + this.Items = memory.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView2D(ReadOnlyMemory2D memory) + { + this.Items = memory.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView2D(Span2D span) + { + this.Items = span.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView2D(ReadOnlySpan2D span) + { + this.Items = span.ToArray(); + } + + /// + /// Gets the items to display for the current instance + /// + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public T[,]? Items { get; } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj index 7f2d3956c53..06c9e584da5 100644 --- a/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj +++ b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj @@ -2,18 +2,19 @@ netstandard1.4;netstandard2.0;netstandard2.1;netcoreapp2.1;netcoreapp3.1 - 8.0 + 9.0 enable true Windows Community Toolkit High Performance .NET Standard This package includes high performance .NET Standard helpers such as: + - Memory2D<T> and Span2D<T>: two types providing fast and allocation-free abstraction over 2D memory areas. - ArrayPoolBufferWriter<T>: an IBufferWriter<T> implementation using pooled arrays, which also supports IMemoryOwner<T>. - MemoryBufferWriter<T>: an IBufferWriter<T>: implementation that can wrap external Memory<T>: instances. - MemoryOwner<T>: an IMemoryOwner<T> implementation with an embedded length and a fast Span<T> accessor. - SpanOwner<T>: a stack-only type with the ability to rent a buffer of a specified length and getting a Span<T> from it. - StringPool: a configurable pool for string instances that be used to minimize allocations when creating multiple strings from char buffers. - - String, array, Span<T>, Memory<T> extensions and more, all focused on high performance. + - String, array, Memory<T>, Span<T> extensions and more, all focused on high performance. - HashCode<T>: a SIMD-enabled extension of HashCode to quickly process sequences of values. - BitHelper: a class with helper methods to perform bit operations on numeric types. - ParallelHelper: helpers to work with parallel code in a highly optimized manner. diff --git a/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs b/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs index c7156dbc63b..1945e1dffde 100644 --- a/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs @@ -36,6 +36,16 @@ public ReadOnlyRef(in T value) Span = MemoryMarshal.CreateReadOnlySpan(ref r0, 1); } + /// + /// Initializes a new instance of the struct. + /// + /// The pointer to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe ReadOnlyRef(void* pointer) + : this(Unsafe.AsRef(pointer)) + { + } + /// /// Gets the readonly reference represented by the current instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Ref{T}.cs b/Microsoft.Toolkit.HighPerformance/Ref{T}.cs index cc642ea48dc..e821a365969 100644 --- a/Microsoft.Toolkit.HighPerformance/Ref{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Ref{T}.cs @@ -34,6 +34,16 @@ public Ref(ref T value) Span = MemoryMarshal.CreateSpan(ref value, 1); } + /// + /// Initializes a new instance of the struct. + /// + /// The pointer to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe Ref(void* pointer) + : this(ref Unsafe.AsRef(pointer)) + { + } + /// /// Gets the reference represented by the current instance. /// diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_StringPool.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_StringPool.cs index b679609d40a..32f14caf797 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_StringPool.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_StringPool.cs @@ -38,13 +38,13 @@ public void Test_StringPool_Cctor_Ok(int minimumSize, int x, int y, int size) Assert.AreEqual(size, pool.Size); - Array maps = (Array)typeof(StringPool).GetField("maps", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(pool); + Array maps = (Array)typeof(StringPool).GetField("maps", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(pool)!; Assert.AreEqual(x, maps.Length); - Type bucketType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap, Microsoft.Toolkit.HighPerformance"); + Type bucketType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap, Microsoft.Toolkit.HighPerformance")!; - int[] buckets = (int[])bucketType.GetField("buckets", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(maps.GetValue(0)); + int[] buckets = (int[])bucketType.GetField("buckets", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(maps.GetValue(0))!; Assert.AreEqual(y, buckets.Length); } @@ -64,7 +64,7 @@ public void Test_StringPool_Cctor_Fail(int size) } catch (ArgumentOutOfRangeException e) { - var cctor = typeof(StringPool).GetConstructor(new[] { typeof(int) }); + var cctor = typeof(StringPool).GetConstructor(new[] { typeof(int) })!; Assert.AreEqual(cctor.GetParameters()[0].Name, e.ParamName); } @@ -158,7 +158,7 @@ public void Test_StringPool_Add_Overwrite() [MethodImpl(MethodImplOptions.NoInlining)] private static string ToStringNoInlining(object obj) { - return obj.ToString(); + return obj.ToString()!; } [TestCategory("StringPool")] @@ -285,15 +285,15 @@ public void Test_StringPool_GetOrAdd_Overflow() } // Get the buckets - Array maps = (Array)typeof(StringPool).GetField("maps", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(pool); + Array maps = (Array)typeof(StringPool).GetField("maps", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(pool)!; - Type bucketType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap, Microsoft.Toolkit.HighPerformance"); - FieldInfo timestampInfo = bucketType.GetField("timestamp", BindingFlags.Instance | BindingFlags.NonPublic); + Type bucketType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap, Microsoft.Toolkit.HighPerformance")!; + FieldInfo timestampInfo = bucketType.GetField("timestamp", BindingFlags.Instance | BindingFlags.NonPublic)!; // Force the timestamp to be the maximum value, or the test would take too long for (int i = 0; i < maps.LongLength; i++) { - object map = maps.GetValue(i); + object map = maps.GetValue(i)!; timestampInfo.SetValue(map, uint.MaxValue); @@ -305,16 +305,16 @@ public void Test_StringPool_GetOrAdd_Overflow() _ = pool.GetOrAdd(text); - Type heapEntryType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap+HeapEntry, Microsoft.Toolkit.HighPerformance"); + Type heapEntryType = Type.GetType("Microsoft.Toolkit.HighPerformance.Buffers.StringPool+FixedSizePriorityMap+HeapEntry, Microsoft.Toolkit.HighPerformance")!; foreach (var map in maps) { // Get the heap for each bucket - Array heapEntries = (Array)bucketType.GetField("heapEntries", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(map); - FieldInfo fieldInfo = heapEntryType.GetField("Timestamp"); + Array heapEntries = (Array)bucketType.GetField("heapEntries", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(map)!; + FieldInfo fieldInfo = heapEntryType.GetField("Timestamp")!; // Extract the array with the timestamps in the heap nodes - uint[] array = heapEntries.Cast().Select(entry => (uint)fieldInfo.GetValue(entry)).ToArray(); + uint[] array = heapEntries.Cast().Select(entry => (uint)fieldInfo.GetValue(entry)!).ToArray(); static bool IsMinHeap(uint[] array) { diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.1D.cs similarity index 69% rename from UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs rename to UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.1D.cs index 3faa4649c56..212d151dbd9 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.1D.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.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -9,6 +10,7 @@ namespace UnitTests.HighPerformance.Extensions { [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601", Justification = "Partial test class")] public partial class Test_ArrayExtensions { [TestCategory("ArrayExtensions")] @@ -17,6 +19,12 @@ public void Test_ArrayExtensions_DangerousGetReference() { string[] tokens = "aa,bb,cc,dd,ee,ff,gg,hh,ii".Split(','); + // In all these "DangerousGetReference" tests, we need to ensure that a reference to a given + // item within an array is effectively the one corresponding to the one whe expect, which is + // either a reference to the first item if we use "DangerousGetReference", or one to the n-th + // item if we use "DangerousGetReferenceAt". So all these tests just invoke the API and then + // compare the returned reference against an existing baseline (like the built-in array indexer) + // to ensure that the two are the same. These are all managed references, so no need for pinning. ref string r0 = ref Unsafe.AsRef(tokens.DangerousGetReference()); ref string r1 = ref Unsafe.AsRef(tokens[0]); diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs index cada6af8094..a25961532a7 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs @@ -3,9 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace UnitTests.HighPerformance.Extensions @@ -23,6 +24,7 @@ public void Test_ArrayExtensions_2D_DangerousGetReference_Int() { 9, 10, 11, 12 } }; + // See comments in Test_ArrayExtensions.1D for how these tests work ref int r0 = ref array.DangerousGetReference(); ref int r1 = ref array[0, 0]; @@ -81,11 +83,16 @@ public void Test_ArrayExtensions_2D_DangerousGetReferenceAt_Index() [TestCategory("ArrayExtensions")] [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayMid() + public void Test_ArrayExtensions_2D_AsSpan2DAndFillArrayMid() { bool[,] test = new bool[4, 5]; - test.Fill(true, 1, 1, 3, 2); + // To fill an array we now go through the Span2D type, which includes all + // the necessary logic to perform the operation. In these tests we just create + // one through the extension, slice it and then fill it. For instance in this + // one, we're creating a Span2D from coordinates (1, 1), with a height of + // 2 and a width of 2, and then filling it. Then we just compare the results. + test.AsSpan2D(1, 1, 2, 3).Fill(true); var expected = new[,] { @@ -100,12 +107,12 @@ public void Test_ArrayExtensions_2D_FillArrayMid() [TestCategory("ArrayExtensions")] [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayTwice() + public void Test_ArrayExtensions_2D_AsSpan2DAndFillArrayTwice() { bool[,] test = new bool[4, 5]; - test.Fill(true, 0, 0, 1, 2); - test.Fill(true, 1, 3, 2, 2); + test.AsSpan2D(0, 0, 2, 1).Fill(true); + test.AsSpan2D(1, 3, 2, 2).Fill(true); var expected = new[,] { @@ -120,30 +127,11 @@ public void Test_ArrayExtensions_2D_FillArrayTwice() [TestCategory("ArrayExtensions")] [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayNegativeSize() + public void Test_ArrayExtensions_2D_AsSpan2DAndFillArrayBottomEdgeBoundary() { bool[,] test = new bool[4, 5]; - test.Fill(true, 3, 4, -3, -2); - - var expected = new[,] - { - { false, false, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual(expected, test); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayBottomEdgeBoundary() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, 1, 2, 2, 4); + test.AsSpan2D(1, 2, 3, 2).Fill(true); var expected = new[,] { @@ -158,30 +146,11 @@ public void Test_ArrayExtensions_2D_FillArrayBottomEdgeBoundary() [TestCategory("ArrayExtensions")] [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayTopLeftCornerNegativeBoundary() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, -1, -1, 3, 3); - - var expected = new[,] - { - { true, true, false, false, false }, - { true, true, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual(expected, test); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_2D_FillArrayBottomRightCornerBoundary() + public void Test_ArrayExtensions_2D_AsSpan2DAndFillArrayBottomRightCornerBoundary() { bool[,] test = new bool[5, 4]; - test.Fill(true, 3, 2, 3, 3); + test.AsSpan2D(3, 2, 2, 2).Fill(true); var expected = new[,] { @@ -197,8 +166,6 @@ public void Test_ArrayExtensions_2D_FillArrayBottomRightCornerBoundary() [TestCategory("ArrayExtensions")] [TestMethod] - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312", Justification = "Dummy loop variable")] - [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501", Justification = "Empty test loop")] public void Test_ArrayExtensions_2D_GetRow_Rectangle() { int[,] array = @@ -208,23 +175,51 @@ public void Test_ArrayExtensions_2D_GetRow_Rectangle() { 9, 10, 11, 12 } }; + // Here we use the enumerator on the RefEnumerator type to traverse items in a row + // by reference. For each one, we check that the reference does in fact point to the + // item we expect in the underlying array (in this case, items on row 1). int j = 0; foreach (ref int value in array.GetRow(1)) { Assert.IsTrue(Unsafe.AreSame(ref value, ref array[1, j++])); } + // Check that RefEnumerable.ToArray() works correctly CollectionAssert.AreEqual(array.GetRow(1).ToArray(), new[] { 5, 6, 7, 8 }); - Assert.ThrowsException(() => + // Test an empty array + Assert.AreSame(new int[1, 0].GetRow(0).ToArray(), Array.Empty()); + + Assert.ThrowsException(() => array.GetRow(-1)); + Assert.ThrowsException(() => array.GetRow(3)); + + Assert.ThrowsException(() => array.GetRow(20)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_GetColumn_Rectangle() + { + int[,] array = { - foreach (var _ in array.GetRow(-1)) { } - }); + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; - Assert.ThrowsException(() => + // Same as above, but this time we iterate a column instead (so non contiguous items) + int i = 0; + foreach (ref int value in array.GetColumn(1)) { - foreach (var _ in array.GetRow(20)) { } - }); + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[i++, 1])); + } + + CollectionAssert.AreEqual(array.GetColumn(1).ToArray(), new[] { 2, 6, 10 }); + + Assert.ThrowsException(() => array.GetColumn(-1)); + Assert.ThrowsException(() => array.GetColumn(4)); + + Assert.ThrowsException(() => array.GetColumn(20)); } [TestCategory("ArrayExtensions")] @@ -233,39 +228,203 @@ public void Test_ArrayExtensions_2D_GetRow_Empty() { int[,] array = new int[0, 0]; + // Try to get a row from an empty array (the row index isn't in range) Assert.ThrowsException(() => array.GetRow(0).ToArray()); } [TestCategory("ArrayExtensions")] [TestMethod] - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312", Justification = "Dummy loop variable")] - [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501", Justification = "Empty test loop")] - public void Test_ArrayExtensions_2D_GetColumn_Rectangle() + public void Test_ArrayExtensions_2D_GetRowOrColumn_Helpers() { int[,] array = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, - { 9, 10, 11, 12 } + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } }; - int i = 0; - foreach (ref int value in array.GetColumn(1)) + // Get a row and test the Clear method. Note that the Span2D here is sliced + // starting from the second column, so this method should clear the row from index 1. + array.AsSpan2D(1, 1, 3, 3).GetRow(0).Clear(); + + int[,] expected = { - Assert.IsTrue(Unsafe.AreSame(ref value, ref array[i++, 1])); - } + { 1, 2, 3, 4 }, + { 5, 0, 0, 0 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }; - CollectionAssert.AreEqual(array.GetColumn(1).ToArray(), new[] { 2, 6, 10 }); + CollectionAssert.AreEqual(array, expected); - Assert.ThrowsException(() => + // Same as before, but this time we fill a column with a value + array.GetColumn(2).Fill(42); + + expected = new[,] { - foreach (var _ in array.GetColumn(-1)) { } - }); + { 1, 2, 42, 4 }, + { 5, 0, 42, 0 }, + { 9, 10, 42, 12 }, + { 13, 14, 42, 16 } + }; + + CollectionAssert.AreEqual(array, expected); + + int[] copy = new int[4]; - Assert.ThrowsException(() => + // Get a row and copy items to a target span (in this case, wrapping an array) + array.GetRow(2).CopyTo(copy); + + int[] result = { 9, 10, 42, 12 }; + + CollectionAssert.AreEqual(copy, result); + + // Same as above, but copying from a column (so we test non contiguous sequences too) + array.GetColumn(1).CopyTo(copy); + + result = new[] { 2, 0, 10, 14 }; + + CollectionAssert.AreEqual(copy, result); + + // Some invalid attempts to copy to an empty span or sequence + Assert.ThrowsException(() => array.GetRow(0).CopyTo(default(RefEnumerable))); + Assert.ThrowsException(() => array.GetRow(0).CopyTo(default(Span))); + + Assert.ThrowsException(() => array.GetColumn(0).CopyTo(default(RefEnumerable))); + Assert.ThrowsException(() => array.GetColumn(0).CopyTo(default(Span))); + + // Same as CopyTo, but this will fail gracefully with an invalid target + Assert.IsTrue(array.GetRow(2).TryCopyTo(copy)); + Assert.IsFalse(array.GetRow(0).TryCopyTo(default(Span))); + + result = new[] { 9, 10, 42, 12 }; + + CollectionAssert.AreEqual(copy, result); + + // Also fill a row and then further down clear a column (trying out all possible combinations) + array.GetRow(2).Fill(99); + + expected = new[,] + { + { 1, 2, 42, 4 }, + { 5, 0, 42, 0 }, + { 99, 99, 99, 99 }, + { 13, 14, 42, 16 } + }; + + CollectionAssert.AreEqual(array, expected); + + array.GetColumn(2).Clear(); + + expected = new[,] + { + { 1, 2, 0, 4 }, + { 5, 0, 0, 0 }, + { 99, 99, 0, 99 }, + { 13, 14, 0, 16 } + }; + + CollectionAssert.AreEqual(array, expected); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_ReadOnlyGetRowOrColumn_Helpers() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }; + + // This test pretty much does the same things as the method above, but this time + // using a source ReadOnlySpan2D, so that the sequence type being tested is + // ReadOnlyRefEnumerable instead (which shares most features but is separate). + ReadOnlySpan2D span2D = array; + + int[] copy = new int[4]; + + span2D.GetRow(2).CopyTo(copy); + + int[] result = { 9, 10, 11, 12 }; + + CollectionAssert.AreEqual(copy, result); + + span2D.GetColumn(1).CopyTo(copy); + + result = new[] { 2, 6, 10, 14 }; + + CollectionAssert.AreEqual(copy, result); + + Assert.ThrowsException(() => ((ReadOnlySpan2D)array).GetRow(0).CopyTo(default(RefEnumerable))); + Assert.ThrowsException(() => ((ReadOnlySpan2D)array).GetRow(0).CopyTo(default(Span))); + + Assert.ThrowsException(() => ((ReadOnlySpan2D)array).GetColumn(0).CopyTo(default(RefEnumerable))); + Assert.ThrowsException(() => ((ReadOnlySpan2D)array).GetColumn(0).CopyTo(default(Span))); + + Assert.IsTrue(span2D.GetRow(2).TryCopyTo(copy)); + Assert.IsFalse(span2D.GetRow(2).TryCopyTo(default(Span))); + + result = new[] { 9, 10, 11, 12 }; + + CollectionAssert.AreEqual(copy, result); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_RefEnumerable_Misc() + { + int[,] array1 = { - foreach (var _ in array.GetColumn(20)) { } - }); + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }; + + int[,] array2 = new int[4, 4]; + + // Copy to enumerable with source step == 1, destination step == 1 + array1.GetRow(0).CopyTo(array2.GetRow(0)); + + // Copy enumerable with source step == 1, destination step != 1 + array1.GetRow(1).CopyTo(array2.GetColumn(1)); + + // Copy enumerable with source step != 1, destination step == 1 + array1.GetColumn(2).CopyTo(array2.GetRow(2)); + + // Copy enumerable with source step != 1, destination step != 1 + array1.GetColumn(3).CopyTo(array2.GetColumn(3)); + + int[,] result = + { + { 1, 5, 3, 4 }, + { 0, 6, 0, 8 }, + { 3, 7, 11, 12 }, + { 0, 8, 0, 16 } + }; + + CollectionAssert.AreEqual(array2, result); + + // Test a valid and an invalid TryCopyTo call with the RefEnumerable overload + bool shouldBeTrue = array1.GetRow(0).TryCopyTo(array2.GetColumn(0)); + bool shouldBeFalse = array1.GetRow(0).TryCopyTo(default(RefEnumerable)); + + result = new[,] + { + { 1, 5, 3, 4 }, + { 2, 6, 0, 8 }, + { 3, 7, 11, 12 }, + { 4, 8, 0, 16 } + }; + + CollectionAssert.AreEqual(array2, result); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); } [TestCategory("ArrayExtensions")] @@ -286,6 +445,7 @@ public void Test_ArrayExtensions_2D_AsSpan_Empty() Span span = array.AsSpan(); + // Check that the empty array was loaded properly Assert.AreEqual(span.Length, array.Length); Assert.IsTrue(span.IsEmpty); } @@ -303,11 +463,14 @@ public void Test_ArrayExtensions_2D_AsSpan_Populated() Span span = array.AsSpan(); + // Test the total length of the span Assert.AreEqual(span.Length, array.Length); ref int r0 = ref array[0, 0]; ref int r1 = ref span[0]; + // Similarly to the top methods, here we compare a given reference to + // ensure they point to the right element back in the original array. Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); } #endif diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.3D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.3D.cs new file mode 100644 index 00000000000..4e7b1d24ce3 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.3D.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + public partial class Test_ArrayExtensions + { + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_3D_DangerousGetReference_Int() + { + int[,,] array = new int[10, 20, 12]; + + // See comments in Test_ArrayExtensions.1D for how these tests work + ref int r0 = ref array.DangerousGetReference(); + ref int r1 = ref array[0, 0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_3D_DangerousGetReference_String() + { + string[,,] array = new string[10, 20, 12]; + + ref string r0 = ref array.DangerousGetReference(); + ref string r1 = ref array[0, 0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_3D_DangerousGetReferenceAt_Zero() + { + int[,,] array = new int[10, 20, 12]; + + ref int r0 = ref array.DangerousGetReferenceAt(0, 0, 0); + ref int r1 = ref array[0, 0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_3D_DangerousGetReferenceAt_Index() + { + int[,,] array = new int[10, 20, 12]; + + ref int r0 = ref array.DangerousGetReferenceAt(5, 3, 4); + ref int r1 = ref array[5, 3, 4]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs index bc99e19d7f6..db03bb8e669 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs @@ -15,16 +15,18 @@ public class Test_BoolExtensions [TestMethod] public void Test_BoolExtensions_True() { - Assert.AreEqual(1, true.ToInt(), nameof(Test_BoolExtensions_True)); - Assert.AreEqual(1, (DateTime.Now.Year > 0).ToInt(), nameof(Test_BoolExtensions_True)); + // There tests all just run a couple of boolean expressions and validate that the extension + // correctly produces either 1 or 0 depending on whether the expression was true or false. + Assert.AreEqual(1, true.ToByte(), nameof(Test_BoolExtensions_True)); + Assert.AreEqual(1, (DateTime.Now.Year > 0).ToByte(), nameof(Test_BoolExtensions_True)); } [TestCategory("BoolExtensions")] [TestMethod] public void Test_BoolExtensions_False() { - Assert.AreEqual(0, false.ToInt(), nameof(Test_BoolExtensions_False)); - Assert.AreEqual(0, (DateTime.Now.Year > 3000).ToInt(), nameof(Test_BoolExtensions_False)); + Assert.AreEqual(0, false.ToByte(), nameof(Test_BoolExtensions_False)); + Assert.AreEqual(0, (DateTime.Now.Year > 3000).ToByte(), nameof(Test_BoolExtensions_False)); } [TestCategory("BoolExtensions")] diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs index 10f1591aeab..e06148d04bb 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -103,7 +103,7 @@ private sealed class Int : IEquatable public Int(int value) => Value = value; - public bool Equals(Int other) + public bool Equals(Int? other) { if (other is null) { @@ -118,7 +118,7 @@ public bool Equals(Int other) return this.Value == other.Value; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { return ReferenceEquals(this, obj) || (obj is Int other && Equals(other)); } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs index 80b6b4dfcf8..3bea7fcb032 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Extensions; @@ -12,6 +13,7 @@ namespace UnitTests.HighPerformance.Extensions { [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601", Justification = "Partial test class")] public partial class Test_ReadOnlySpanExtensions { [TestCategory("ReadOnlySpanExtensions")] @@ -267,5 +269,47 @@ public void Test_ReadOnlySpanExtensions_Tokenize_CommaWithMissingValues() CollectionAssert.AreEqual(result, tokens); } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_CopyTo_RefEnumerable() + { + int[,] array = new int[4, 5]; + + ReadOnlySpan + values1 = new[] { 10, 20, 30, 40, 50 }, + values2 = new[] { 11, 22, 33, 44, 55 }; + + // Copy a span to a target row and column with valid lengths + values1.CopyTo(array.GetRow(0)); + values2.Slice(0, 4).CopyTo(array.GetColumn(1)); + + int[,] result = + { + { 10, 11, 30, 40, 50 }, + { 0, 22, 0, 0, 0 }, + { 0, 33, 0, 0, 0 }, + { 0, 44, 0, 0, 0 } + }; + + CollectionAssert.AreEqual(array, result); + + // Try to copy to a valid row and an invalid column (too short for the source span) + bool shouldBeTrue = values1.TryCopyTo(array.GetRow(2)); + bool shouldBeFalse = values2.TryCopyTo(array.GetColumn(3)); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); + + result = new[,] + { + { 10, 11, 30, 40, 50 }, + { 0, 22, 0, 0, 0 }, + { 10, 20, 30, 40, 50 }, + { 0, 44, 0, 0, 0 } + }; + + CollectionAssert.AreEqual(array, result); + } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs index fd89a5c6794..4e6753173d2 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs @@ -168,5 +168,47 @@ public void Test_SpanExtensions_Enumerate_Empty() Assert.Fail("Empty source sequence"); } } + + [TestCategory("SpanExtensions")] + [TestMethod] + public void Test_SpanExtensions_CopyTo_RefEnumerable() + { + int[,] array = new int[4, 5]; + + int[] + values1 = { 10, 20, 30, 40, 50 }, + values2 = { 11, 22, 33, 44, 55 }; + + // Copy a span to a target row and column with valid lengths + values1.AsSpan().CopyTo(array.GetRow(0)); + values2.AsSpan(0, 4).CopyTo(array.GetColumn(1)); + + int[,] result = + { + { 10, 11, 30, 40, 50 }, + { 0, 22, 0, 0, 0 }, + { 0, 33, 0, 0, 0 }, + { 0, 44, 0, 0, 0 } + }; + + CollectionAssert.AreEqual(array, result); + + // Try to copy to a valid row and an invalid column (too short for the source span) + bool shouldBeTrue = values1.AsSpan().TryCopyTo(array.GetRow(2)); + bool shouldBeFalse = values2.AsSpan().TryCopyTo(array.GetColumn(3)); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); + + result = new[,] + { + { 10, 11, 30, 40, 50 }, + { 0, 22, 0, 0, 0 }, + { 10, 20, 30, 40, 50 }, + { 0, 44, 0, 0, 0 } + }; + + CollectionAssert.AreEqual(array, result); + } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs index 86828480387..26d148acd92 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs @@ -63,7 +63,6 @@ public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat() TestForType(); } -#if NETCOREAPP3_1 [TestCategory("HashCodeOfT")] [TestMethod] public void Test_HashCodeOfT_ManagedType_TestRepeat() @@ -89,7 +88,6 @@ public void Test_HashCodeOfT_ManagedType_TestRepeat() Assert.AreEqual(hash1, hash2, $"Failed {typeof(string)} test with count {count}: got {hash1} and then {hash2}"); } } -#endif /// /// Performs a test for a specified type. diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In2D.cs new file mode 100644 index 00000000000..2fd83fe931c --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In2D.cs @@ -0,0 +1,83 @@ +// 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.Threading; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + [TestCategory("ParallelHelper")] + [TestMethod] + [DataRow(1, 1, 0, 0, 1, 1)] + [DataRow(1, 2, 0, 0, 1, 2)] + [DataRow(2, 3, 0, 0, 2, 3)] + [DataRow(2, 3, 0, 1, 2, 2)] + [DataRow(3, 3, 1, 1, 2, 2)] + [DataRow(12, 12, 2, 4, 3, 3)] + [DataRow(64, 64, 0, 0, 32, 32)] + [DataRow(64, 64, 13, 14, 23, 22)] + public unsafe void Test_ParallelHelper_ForEach_In2D( + int sizeY, + int sizeX, + int row, + int column, + int height, + int width) + { + int[,] data = CreateRandomData2D(sizeY, sizeX); + + // Create a memory wrapping the random array with the given parameters + ReadOnlyMemory2D memory = data.AsMemory2D(row, column, height, width); + + Assert.AreEqual(memory.Length, height * width); + Assert.AreEqual(memory.Height, height); + Assert.AreEqual(memory.Width, width); + + int sum = 0; + + // Sum all the items in parallel. The Summer type takes a pointer to a target value + // and adds values to it in a thread-safe manner (with an interlocked add). + ParallelHelper.ForEach(memory, new Summer(&sum)); + + int expected = 0; + + // Calculate the sum iteratively as a baseline for comparison + foreach (int n in memory.Span) + { + expected += n; + } + + Assert.AreEqual(sum, expected, $"The sum doesn't match, was {sum} instead of {expected}"); + } + + /// + /// Creates a random 2D array filled with random numbers. + /// + /// The height of the array to create. + /// The width of the array to create. + /// An array of random elements. + [Pure] + private static int[,] CreateRandomData2D(int height, int width) + { + var random = new Random((height * 33) + width); + + int[,] data = new int[height, width]; + + foreach (ref int n in data.AsSpan2D()) + { + n = random.Next(0, byte.MaxValue); + } + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref2D.cs new file mode 100644 index 00000000000..d3a27abcea8 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref2D.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 Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + [TestCategory("ParallelHelper")] + [TestMethod] + [DataRow(1, 1, 0, 0, 1, 1)] + [DataRow(1, 2, 0, 0, 1, 2)] + [DataRow(2, 3, 0, 0, 2, 3)] + [DataRow(2, 3, 0, 1, 2, 2)] + [DataRow(3, 3, 1, 1, 2, 2)] + [DataRow(12, 12, 2, 4, 3, 3)] + [DataRow(64, 64, 0, 0, 32, 32)] + [DataRow(64, 64, 13, 14, 23, 22)] + public void Test_ParallelHelper_ForEach_Ref2D( + int sizeY, + int sizeX, + int row, + int column, + int height, + int width) + { + int[,] + data = CreateRandomData2D(sizeY, sizeX), + copy = (int[,])data.Clone(); + + // Prepare the target data iteratively + foreach (ref int n in copy.AsSpan2D(row, column, height, width)) + { + n = unchecked(n * 397); + } + + Memory2D memory = data.AsMemory2D(row, column, height, width); + + Assert.AreEqual(memory.Length, height * width); + Assert.AreEqual(memory.Height, height); + Assert.AreEqual(memory.Width, width); + + // Do the same computation in paralellel, then compare the two arrays + ParallelHelper.ForEach(memory, new Multiplier(397)); + + CollectionAssert.AreEqual(data, copy); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Memory2D{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Memory2D{T}.cs new file mode 100644 index 00000000000..3ebe61d5972 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Memory2D{T}.cs @@ -0,0 +1,544 @@ +// 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.CodeAnalysis; +#if !WINDOWS_UWP +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +#endif +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Memory +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_Memory2DT + { + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Empty() + { + // Create a few empty Memory2D instances in different ways and + // check to ensure the right parameters were used to initialize them. + Memory2D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(empty1.Length, 0); + Assert.AreEqual(empty1.Width, 0); + Assert.AreEqual(empty1.Height, 0); + + Memory2D empty2 = Memory2D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(empty2.Length, 0); + Assert.AreEqual(empty2.Width, 0); + Assert.AreEqual(empty2.Height, 0); + + Memory2D empty3 = new int[4, 0]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(empty3.Length, 0); + Assert.AreEqual(empty3.Width, 0); + Assert.AreEqual(empty3.Height, 4); + + Memory2D empty4 = new int[0, 7]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(empty4.Length, 0); + Assert.AreEqual(empty4.Width, 7); + Assert.AreEqual(empty4.Height, 0); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, 6 + }; + + // Create a memory over a 1D array with 2D data in row-major order. This tests + // the T[] array constructor for Memory2D with custom size and pitch. + Memory2D memory2d = new Memory2D(array, 1, 2, 2, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + // Also ensure the right exceptions are thrown with invalid parameters, such as + // negative indices, indices out of range, values that are too big, etc. + Assert.ThrowsException(() => new Memory2D(new string[1], 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, -99, 1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 0, -10, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 0, 1, 1, -1)); + Assert.ThrowsException(() => new Memory2D(array, 0, 1, -100, 1)); + Assert.ThrowsException(() => new Memory2D(array, 0, 2, 4, 0)); + Assert.ThrowsException(() => new Memory2D(array, 0, 3, 3, 0)); + Assert.ThrowsException(() => new Memory2D(array, 1, 2, 3, 0)); + Assert.ThrowsException(() => new Memory2D(array, 0, 10, 1, 120)); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Array2DConstructor_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Test the constructor taking a T[,] array that is mapped directly (no slicing) + Memory2D memory2d = new Memory2D(array); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 6); + Assert.AreEqual(memory2d.Width, 3); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 1], 2); + Assert.AreEqual(memory2d.Span[1, 2], 6); + + // Here we test the check for covariance: we can't create a Memory2D from a U[,] array + // where U is assignable to T (as in, U : T). This would cause a type safety violation on write. + Assert.ThrowsException(() => new Memory2D(new string[1, 2])); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Array2DConstructor_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but this time we also slice the memory to test the other constructor + Memory2D memory2d = new Memory2D(array, 0, 1, 2, 2); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + Assert.ThrowsException(() => new Memory2D(new string[1, 2], 0, 0, 2, 2)); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but we test the constructor taking a layer within a 3D array + Memory2D memory2d = new Memory2D(array, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 6); + Assert.AreEqual(memory2d.Width, 3); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 1], 20); + Assert.AreEqual(memory2d.Span[1, 2], 60); + + // A couple of tests for invalid parameters, ie. layers out of range + Assert.ThrowsException(() => new Memory2D(array, -1)); + Assert.ThrowsException(() => new Memory2D(array, 2)); + Assert.ThrowsException(() => new Memory2D(array, 20)); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but we also slice the target layer in the 3D array. In this case we're creating + // a Memory instance from a slice in the layer at depth 1 in our 3D array, and with an area + // starting at coorsinates (0, 1), with a height of 2 and width of 2. So we want to wrap the + // square with items [20, 30, 50, 60] in the second layer of the 3D array above. + Memory2D memory2d = new Memory2D(array, 1, 0, 1, 2, 2); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 20); + Assert.AreEqual(memory2d.Span[1, 1], 60); + + // Same as above, testing a few cases with invalid parameters + Assert.ThrowsException(() => new Memory2D(array, -1, 1, 1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 1, -1, 1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 1, 1, -1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 1, 1, 1, -1, 1)); + Assert.ThrowsException(() => new Memory2D(array, 1, 1, 1, 1, -1)); + Assert.ThrowsException(() => new Memory2D(array, 2, 0, 0, 2, 3)); + Assert.ThrowsException(() => new Memory2D(array, 0, 0, 1, 2, 3)); + Assert.ThrowsException(() => new Memory2D(array, 0, 0, 0, 2, 4)); + Assert.ThrowsException(() => new Memory2D(array, 0, 0, 0, 3, 3)); + } + +#if !WINDOWS_UWP + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_MemoryConstructor() + { + Memory memory = new[] + { + 1, 2, 3, 4, 5, 6 + }; + + // We also test the constructor that takes an input Memory instance. + // This is only available on runtimes with fast Span support, as otherwise + // the implementation would be too complex and slow to work in this case. + // Conceptually, this works the same as when wrapping a 1D array with row-major items. + Memory2D memory2d = memory.AsMemory2D(1, 2, 2, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + Assert.ThrowsException(() => memory.AsMemory2D(-99, 1, 1, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, -10, 1, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 1, 1, -1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 1, -100, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 2, 4, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 3, 3, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(1, 2, 3, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 10, 1, 120)); + } +#endif + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Slice_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Memory2D memory2d = new Memory2D(array); + + // Test a slice from a Memory2D with valid parameters + Memory2D slice1 = memory2d.Slice(1, 1, 1, 2); + + Assert.AreEqual(slice1.Length, 2); + Assert.AreEqual(slice1.Height, 1); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1.Span[0, 0], 5); + Assert.AreEqual(slice1.Span[0, 1], 6); + + // Same above, but we test slicing a pre-sliced instance as well. This + // is done to verify that the internal offsets are properly tracked + // across multiple slicing operations, instead of just in the first. + Memory2D slice2 = memory2d.Slice(0, 1, 2, 2); + + Assert.AreEqual(slice2.Length, 4); + Assert.AreEqual(slice2.Height, 2); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2.Span[0, 0], 2); + Assert.AreEqual(slice2.Span[1, 0], 5); + Assert.AreEqual(slice2.Span[1, 1], 6); + + // A few invalid slicing operations, with out of range parameters + Assert.ThrowsException(() => new Memory2D(array).Slice(-1, 1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, -1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, 1, 1, -1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, 1, -1, 1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(10, 1, 1, 1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, 12, 1, 12)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, 1, 55, 1)); + Assert.ThrowsException(() => new Memory2D(array).Slice(0, 0, 2, 4)); + Assert.ThrowsException(() => new Memory2D(array).Slice(0, 0, 3, 3)); + Assert.ThrowsException(() => new Memory2D(array).Slice(0, 1, 2, 3)); + Assert.ThrowsException(() => new Memory2D(array).Slice(1, 0, 2, 3)); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Slice_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Memory2D memory2d = new Memory2D(array); + + // Mostly the same test as above, just with different parameters + Memory2D slice1 = memory2d.Slice(0, 0, 2, 2); + + Assert.AreEqual(slice1.Length, 4); + Assert.AreEqual(slice1.Height, 2); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1.Span[0, 0], 1); + Assert.AreEqual(slice1.Span[1, 1], 5); + + Memory2D slice2 = slice1.Slice(1, 0, 1, 2); + + Assert.AreEqual(slice2.Length, 2); + Assert.AreEqual(slice2.Height, 1); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2.Span[0, 0], 4); + Assert.AreEqual(slice2.Span[0, 1], 5); + + Memory2D slice3 = slice2.Slice(0, 1, 1, 1); + + Assert.AreEqual(slice3.Length, 1); + Assert.AreEqual(slice3.Height, 1); + Assert.AreEqual(slice3.Width, 1); + Assert.AreEqual(slice3.Span[0, 0], 5); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_TryGetMemory_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Memory2D memory2d = new Memory2D(array); + + // Here we test that we can get a Memory from a 2D one when the underlying + // data is contiguous. Note that in this case this can only work on runtimes + // with fast Span support, because otherwise it's not possible to get a + // Memory (or a Span too, for that matter) from a 2D array. + bool success = memory2d.TryGetMemory(out Memory memory); + +#if WINDOWS_UWP + Assert.IsFalse(success); + Assert.IsTrue(memory.IsEmpty); +#else + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, array.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 0], ref memory.Span[0])); +#endif + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_TryGetMemory_2() + { + int[] array = { 1, 2, 3, 4 }; + + Memory2D memory2d = new Memory2D(array, 2, 2); + + // Same test as above, but this will always succeed on all runtimes, + // as creating a Memory from a 1D array is always supported. + bool success = memory2d.TryGetMemory(out Memory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, array.Length); + Assert.AreEqual(memory.Span[2], 3); + } + +#if !WINDOWS_UWP + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_TryGetMemory_3() + { + Memory data = new[] { 1, 2, 3, 4 }; + + Memory2D memory2d = data.AsMemory2D(2, 2); + + // Same as above, just with the extra Memory indirection. Same as above, + // this test is only supported on runtimes with fast Span support. + // On others, we just don't expose the Memory.AsMemory2D extension. + bool success = memory2d.TryGetMemory(out Memory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, data.Length); + Assert.AreEqual(memory.Span[2], 3); + } +#endif + + [TestCategory("Memory2DT")] + [TestMethod] + public unsafe void Test_Memory2DT_Pin_1() + { + int[] array = { 1, 2, 3, 4 }; + + // We create a Memory2D from an array and verify that pinning this + // instance correctly returns a pointer to the right array element. + Memory2D memory2d = new Memory2D(array, 2, 2); + + using var pin = memory2d.Pin(); + + Assert.AreEqual(((int*)pin.Pointer)[0], 1); + Assert.AreEqual(((int*)pin.Pointer)[3], 4); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public unsafe void Test_Memory2DT_Pin_2() + { + int[] array = { 1, 2, 3, 4 }; + + // Same as above, but we test with a sliced Memory2D instance + Memory2D memory2d = new Memory2D(array, 2, 2); + + using var pin = memory2d.Pin(); + + Assert.AreEqual(((int*)pin.Pointer)[0], 1); + Assert.AreEqual(((int*)pin.Pointer)[3], 4); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_ToArray_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Here we create a Memory2D instance from a 2D array and then verify that + // calling ToArray() creates an array that matches the contents of the first. + Memory2D memory2d = new Memory2D(array); + + int[,] copy = memory2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_ToArray_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with a sliced Memory2D instance + Memory2D memory2d = new Memory2D(array, 0, 0, 2, 2); + + int[,] copy = memory2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), 2); + Assert.AreEqual(copy.GetLength(1), 2); + + int[,] expected = + { + { 1, 2 }, + { 4, 5 } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_Equals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Here we want to verify that the Memory2D.Equals method works correctly. This is true + // when the wrapped instance is the same, and the various internal offsets and sizes match. + Memory2D memory2d = new Memory2D(array); + + Assert.IsFalse(memory2d.Equals(null)); + Assert.IsFalse(memory2d.Equals(new Memory2D(array, 0, 1, 2, 2))); + Assert.IsTrue(memory2d.Equals(new Memory2D(array))); + Assert.IsTrue(memory2d.Equals(memory2d)); + + // This should work also when casting to a ReadOnlyMemory2D instance + ReadOnlyMemory2D readOnlyMemory2d = memory2d; + + Assert.IsTrue(memory2d.Equals(readOnlyMemory2d)); + Assert.IsFalse(memory2d.Equals(readOnlyMemory2d.Slice(0, 1, 2, 2))); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_GetHashCode() + { + // An emoty Memory2D has just 0 as the hashcode + Assert.AreEqual(Memory2D.Empty.GetHashCode(), 0); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Memory2D memory2d = new Memory2D(array); + + // Ensure that the GetHashCode method is repeatable + int a = memory2d.GetHashCode(), b = memory2d.GetHashCode(); + + Assert.AreEqual(a, b); + + // The hashcode shouldn't match when the size is different + int c = new Memory2D(array, 0, 1, 2, 2).GetHashCode(); + + Assert.AreNotEqual(a, c); + } + + [TestCategory("Memory2DT")] + [TestMethod] + public void Test_Memory2DT_ToString() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Memory2D memory2d = new Memory2D(array); + + // Here we just want to verify that the type is nicely printed as expected, along with the size + string text = memory2d.ToString(); + + const string expected = "Microsoft.Toolkit.HighPerformance.Memory.Memory2D[2, 3]"; + + Assert.AreEqual(text, expected); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlyMemory2D{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlyMemory2D{T}.cs new file mode 100644 index 00000000000..4cbc4c23c86 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlyMemory2D{T}.cs @@ -0,0 +1,492 @@ +// 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.CodeAnalysis; +#if !WINDOWS_UWP +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +#endif +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Memory +{ + /* ==================================================================== + * NOTE + * ==================================================================== + * All the tests here mirror the ones for Memory2D, as the two types + * are basically the same except for some small differences in return types + * or some checks being done upon construction. See comments in the test + * file for Memory2D for more info on these tests. */ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_ReadOnlyMemory2DT + { + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Empty() + { + ReadOnlyMemory2D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(empty1.Length, 0); + Assert.AreEqual(empty1.Width, 0); + Assert.AreEqual(empty1.Height, 0); + + ReadOnlyMemory2D empty2 = ReadOnlyMemory2D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(empty2.Length, 0); + Assert.AreEqual(empty2.Width, 0); + Assert.AreEqual(empty2.Height, 0); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, 6 + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 1, 2, 2, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + // Here we check to ensure a covariant array conversion is allowed for ReadOnlyMemory2D + _ = new ReadOnlyMemory2D(new string[1], 1, 1); + + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, -99, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, -10, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 1, 1, -1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 1, -100, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 2, 4, 0)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 3, 3, 0)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 1, 2, 3, 0)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 10, 1, 120)); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Array2DConstructor_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 6); + Assert.AreEqual(memory2d.Width, 3); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 1], 2); + Assert.AreEqual(memory2d.Span[1, 2], 6); + + _ = new ReadOnlyMemory2D(new string[1, 2]); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Array2DConstructor_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 0, 1, 2, 2); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + _ = new ReadOnlyMemory2D(new string[1, 2]); + + Assert.ThrowsException(() => new ReadOnlyMemory2D(new string[1, 2], 0, 0, 2, 2)); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 6); + Assert.AreEqual(memory2d.Width, 3); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 1], 20); + Assert.AreEqual(memory2d.Span[1, 2], 60); + + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, -1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 20)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 2)); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 1, 0, 1, 2, 2); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 20); + Assert.AreEqual(memory2d.Span[1, 1], 60); + + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, -1, 1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 1, -1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 1, 1, -1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 1, 1, 1, -1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 1, 1, 1, 1, -1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 2, 0, 0, 2, 3)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 0, 1, 2, 3)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 0, 0, 2, 4)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array, 0, 0, 0, 3, 3)); + } + +#if !WINDOWS_UWP + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_ReadOnlyMemoryConstructor() + { + ReadOnlyMemory memory = new[] + { + 1, 2, 3, 4, 5, 6 + }; + + ReadOnlyMemory2D memory2d = memory.AsMemory2D(1, 2, 2, 1); + + Assert.IsFalse(memory2d.IsEmpty); + Assert.AreEqual(memory2d.Length, 4); + Assert.AreEqual(memory2d.Width, 2); + Assert.AreEqual(memory2d.Height, 2); + Assert.AreEqual(memory2d.Span[0, 0], 2); + Assert.AreEqual(memory2d.Span[1, 1], 6); + + Assert.ThrowsException(() => memory.AsMemory2D(-99, 1, 1, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, -10, 1, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 1, 1, -1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 1, -100, 1)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 2, 4, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 3, 3, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(1, 2, 3, 0)); + Assert.ThrowsException(() => memory.AsMemory2D(0, 10, 1, 120)); + } +#endif + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Slice_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + ReadOnlyMemory2D slice1 = memory2d.Slice(1, 1, 1, 2); + + Assert.AreEqual(slice1.Length, 2); + Assert.AreEqual(slice1.Height, 1); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1.Span[0, 0], 5); + Assert.AreEqual(slice1.Span[0, 1], 6); + + ReadOnlyMemory2D slice2 = memory2d.Slice(0, 1, 2, 2); + + Assert.AreEqual(slice2.Length, 4); + Assert.AreEqual(slice2.Height, 2); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2.Span[0, 0], 2); + Assert.AreEqual(slice2.Span[1, 0], 5); + Assert.AreEqual(slice2.Span[1, 1], 6); + + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(-1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, -1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, 1, 1, -1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, 1, -1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(10, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, 12, 1, 12)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, 1, 55, 1)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(0, 0, 2, 4)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(0, 0, 3, 3)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(0, 1, 2, 3)); + Assert.ThrowsException(() => new ReadOnlyMemory2D(array).Slice(1, 0, 2, 3)); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Slice_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + ReadOnlyMemory2D slice1 = memory2d.Slice(0, 0, 2, 2); + + Assert.AreEqual(slice1.Length, 4); + Assert.AreEqual(slice1.Height, 2); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1.Span[0, 0], 1); + Assert.AreEqual(slice1.Span[1, 1], 5); + + ReadOnlyMemory2D slice2 = slice1.Slice(1, 0, 1, 2); + + Assert.AreEqual(slice2.Length, 2); + Assert.AreEqual(slice2.Height, 1); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2.Span[0, 0], 4); + Assert.AreEqual(slice2.Span[0, 1], 5); + + ReadOnlyMemory2D slice3 = slice2.Slice(0, 1, 1, 1); + + Assert.AreEqual(slice3.Length, 1); + Assert.AreEqual(slice3.Height, 1); + Assert.AreEqual(slice3.Width, 1); + Assert.AreEqual(slice3.Span[0, 0], 5); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_TryGetReadOnlyMemory_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + bool success = memory2d.TryGetMemory(out ReadOnlyMemory memory); + +#if WINDOWS_UWP + Assert.IsFalse(success); + Assert.IsTrue(memory.IsEmpty); +#else + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, array.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 0], ref Unsafe.AsRef(memory.Span[0]))); +#endif + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_TryGetReadOnlyMemory_2() + { + int[] array = { 1, 2, 3, 4 }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 2, 2); + + bool success = memory2d.TryGetMemory(out ReadOnlyMemory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, array.Length); + Assert.AreEqual(memory.Span[2], 3); + } + +#if !WINDOWS_UWP + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_TryGetReadOnlyMemory_3() + { + ReadOnlyMemory data = new[] { 1, 2, 3, 4 }; + + ReadOnlyMemory2D memory2d = data.AsMemory2D(2, 2); + + bool success = memory2d.TryGetMemory(out ReadOnlyMemory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, data.Length); + Assert.AreEqual(memory.Span[2], 3); + } +#endif + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public unsafe void Test_ReadOnlyMemory2DT_Pin_1() + { + int[] array = { 1, 2, 3, 4 }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 2, 2); + + using var pin = memory2d.Pin(); + + Assert.AreEqual(((int*)pin.Pointer)[0], 1); + Assert.AreEqual(((int*)pin.Pointer)[3], 4); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public unsafe void Test_ReadOnlyMemory2DT_Pin_2() + { + int[] array = { 1, 2, 3, 4 }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 2, 2); + + using var pin = memory2d.Pin(); + + Assert.AreEqual(((int*)pin.Pointer)[0], 1); + Assert.AreEqual(((int*)pin.Pointer)[3], 4); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_ToArray_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + int[,] copy = memory2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_ToArray_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array, 0, 0, 2, 2); + + int[,] copy = memory2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), 2); + Assert.AreEqual(copy.GetLength(1), 2); + + int[,] expected = + { + { 1, 2 }, + { 4, 5 } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_Equals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D readOnlyMemory2D = new ReadOnlyMemory2D(array); + + Assert.IsFalse(readOnlyMemory2D.Equals(null)); + Assert.IsFalse(readOnlyMemory2D.Equals(new ReadOnlyMemory2D(array, 0, 1, 2, 2))); + Assert.IsTrue(readOnlyMemory2D.Equals(new ReadOnlyMemory2D(array))); + Assert.IsTrue(readOnlyMemory2D.Equals(readOnlyMemory2D)); + + Memory2D memory2d = array; + + Assert.IsTrue(readOnlyMemory2D.Equals((object)memory2d)); + Assert.IsFalse(readOnlyMemory2D.Equals((object)memory2d.Slice(0, 1, 2, 2))); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_GetHashCode() + { + Assert.AreEqual(ReadOnlyMemory2D.Empty.GetHashCode(), 0); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + int a = memory2d.GetHashCode(), b = memory2d.GetHashCode(); + + Assert.AreEqual(a, b); + + int c = new ReadOnlyMemory2D(array, 0, 1, 2, 2).GetHashCode(); + + Assert.AreNotEqual(a, c); + } + + [TestCategory("ReadOnlyMemory2DT")] + [TestMethod] + public void Test_ReadOnlyMemory2DT_ToString() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlyMemory2D memory2d = new ReadOnlyMemory2D(array); + + string text = memory2d.ToString(); + + const string expected = "Microsoft.Toolkit.HighPerformance.Memory.ReadOnlyMemory2D[2, 3]"; + + Assert.AreEqual(text, expected); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlySpan2D{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlySpan2D{T}.cs new file mode 100644 index 00000000000..e8099e280ab --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_ReadOnlySpan2D{T}.cs @@ -0,0 +1,924 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Memory +{ + /* ==================================================================== + * NOTE + * ==================================================================== + * All the tests here mirror the ones for ReadOnlySpan2D. See comments + * in the test file for Span2D for more info on these tests. */ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_ReadOnlySpan2DT + { + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Empty() + { + ReadOnlySpan2D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(empty1.Length, 0); + Assert.AreEqual(empty1.Width, 0); + Assert.AreEqual(empty1.Height, 0); + + ReadOnlySpan2D empty2 = ReadOnlySpan2D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(empty2.Length, 0); + Assert.AreEqual(empty2.Width, 0); + Assert.AreEqual(empty2.Height, 0); + } + +#if !WINDOWS_UWP + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_RefConstructor() + { + ReadOnlySpan span = stackalloc[] + { + 1, 2, 3, 4, 5, 6 + }; + + ReadOnlySpan2D span2d = ReadOnlySpan2D.DangerousCreate(span[0], 2, 3, 0); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 1); + Assert.AreEqual(span2d[1, 2], 6); + + Assert.ThrowsException(() => ReadOnlySpan2D.DangerousCreate(Unsafe.AsRef(null), -1, 0, 0)); + Assert.ThrowsException(() => ReadOnlySpan2D.DangerousCreate(Unsafe.AsRef(null), 1, -2, 0)); + Assert.ThrowsException(() => ReadOnlySpan2D.DangerousCreate(Unsafe.AsRef(null), 1, 0, -5)); + } +#endif + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_PtrConstructor() + { + int* ptr = stackalloc[] + { + 1, + 2, + 3, + 4, + 5, + 6 + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(ptr, 2, 3, 0); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 1); + Assert.AreEqual(span2d[1, 2], 6); + + Assert.ThrowsException(() => new ReadOnlySpan2D((void*)0, -1, 0, 0)); + Assert.ThrowsException(() => new ReadOnlySpan2D((void*)0, 1, -2, 0)); + Assert.ThrowsException(() => new ReadOnlySpan2D((void*)0, 1, 0, -5)); + Assert.ThrowsException(() => new ReadOnlySpan2D((void*)0, 2, 2, 0)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, 6 + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 1, 2, 2, 1); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 2); + Assert.AreEqual(span2d[1, 1], 6); + + // Same for ReadOnlyMemory2D, we need to check that covariant array conversions are allowed + _ = new ReadOnlySpan2D(new string[1], 1, 1); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array, -99, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 0, -10, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 0, 1, 1, -1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 0, 1, -100, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 0, 10, 1, 120)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Array2DConstructor_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 1], 2); + Assert.AreEqual(span2d[1, 2], 6); + + _ = new ReadOnlySpan2D(new string[1, 2]); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Array2DConstructor_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 0, 1, 2, 2); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 2); + Assert.AreEqual(span2d[1, 1], 6); + + _ = new ReadOnlySpan2D(new string[1, 2]); + + Assert.ThrowsException(() => new ReadOnlySpan2D(new string[1, 2], 0, 0, 2, 2)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 1); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 10); + Assert.AreEqual(span2d[0, 1], 20); + Assert.AreEqual(span2d[1, 2], 60); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array, -1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 20)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 1, 0, 1, 2, 2); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + Assert.AreEqual(span2d[0, 0], 20); + Assert.AreEqual(span2d[0, 1], 30); + Assert.AreEqual(span2d[1, 1], 60); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array, -1, 1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 1, -1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 1, 1, -1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 1, 1, 1, -1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 1, 1, 1, 1, -1)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_CopyTo_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + int[] target = new int[array.Length]; + + span2d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).CopyTo(Span.Empty)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_CopyTo_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 0, 1, 2, 2); + + int[] target = new int[4]; + + span2d.CopyTo(target); + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(target, expected); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).CopyTo(Span.Empty)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_CopyTo2D_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + int[,] target = new int[2, 3]; + + span2d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).CopyTo(Span2D.Empty)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_CopyTo2D_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 0, 1, 2, 2); + + int[,] target = new int[2, 2]; + + span2d.CopyTo(target); + + int[,] expected = + { + { 2, 3 }, + { 5, 6 } + }; + + CollectionAssert.AreEqual(target, expected); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).CopyTo(new Span2D(target))); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_TryCopyTo() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + int[] target = new int[array.Length]; + + Assert.IsTrue(span2d.TryCopyTo(target)); + Assert.IsFalse(span2d.TryCopyTo(Span.Empty)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_TryCopyTo2D() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + int[,] target = new int[2, 3]; + + Assert.IsTrue(span2d.TryCopyTo(target)); + Assert.IsFalse(span2d.TryCopyTo(Span2D.Empty)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_GetPinnableReference() + { + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref ReadOnlySpan2D.Empty.GetPinnableReference())); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + ref int r0 = ref span2d.GetPinnableReference(); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0])); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_DangerousGetReference() + { + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref ReadOnlySpan2D.Empty.DangerousGetReference())); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + ref int r0 = ref span2d.DangerousGetReference(); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0])); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Slice_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + ReadOnlySpan2D slice1 = span2d.Slice(1, 1, 2, 1); + + Assert.AreEqual(slice1.Length, 2); + Assert.AreEqual(slice1.Height, 1); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1[0, 0], 5); + Assert.AreEqual(slice1[0, 1], 6); + + ReadOnlySpan2D slice2 = span2d.Slice(0, 1, 2, 2); + + Assert.AreEqual(slice2.Length, 4); + Assert.AreEqual(slice2.Height, 2); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2[0, 0], 2); + Assert.AreEqual(slice2[1, 0], 5); + Assert.AreEqual(slice2[1, 1], 6); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(-1, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(1, -1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(1, 1, -1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(1, 1, 1, -1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(10, 1, 1, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(1, 12, 12, 1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).Slice(1, 1, 1, 55)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_Slice_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + ReadOnlySpan2D slice1 = span2d.Slice(0, 0, 2, 2); + + Assert.AreEqual(slice1.Length, 4); + Assert.AreEqual(slice1.Height, 2); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1[0, 0], 1); + Assert.AreEqual(slice1[1, 1], 5); + + ReadOnlySpan2D slice2 = slice1.Slice(1, 0, 2, 1); + + Assert.AreEqual(slice2.Length, 2); + Assert.AreEqual(slice2.Height, 1); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2[0, 0], 4); + Assert.AreEqual(slice2[0, 1], 5); + + ReadOnlySpan2D slice3 = slice2.Slice(0, 1, 1, 1); + + Assert.AreEqual(slice3.Length, 1); + Assert.AreEqual(slice3.Height, 1); + Assert.AreEqual(slice3.Width, 1); + Assert.AreEqual(slice3[0, 0], 5); + } + +#if !WINDOWS_UWP + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_GetRowReadOnlySpan() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + ReadOnlySpan span = span2d.GetRowSpan(1); + + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(span[0]), + ref array[1, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(span[2]), + ref array[1, 2])); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetRowSpan(-1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetRowSpan(5)); + } +#endif + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_TryGetReadOnlySpan_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + bool success = span2d.TryGetSpan(out ReadOnlySpan span); + +#if WINDOWS_UWP + // Can't get a ReadOnlySpan over a T[,] array on UWP + Assert.IsFalse(success); + Assert.AreEqual(span.Length, 0); +#else + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span2d.Length); +#endif + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_TryGetReadOnlySpan_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 0, 0, 2, 2); + + bool success = span2d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsFalse(success); + Assert.IsTrue(span.IsEmpty); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ToArray_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + int[,] copy = span2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ToArray_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array, 0, 0, 2, 2); + + int[,] copy = span2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), 2); + Assert.AreEqual(copy.GetLength(1), 2); + + int[,] expected = + { + { 1, 2 }, + { 4, 5 } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Test_ReadOnlySpan2DT_Equals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + _ = span2d.Equals(null); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Test_ReadOnlySpan2DT_GetHashCode() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + _ = span2d.GetHashCode(); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ToString() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d = new ReadOnlySpan2D(array); + + string text = span2d.ToString(); + + const string expected = "Microsoft.Toolkit.HighPerformance.Memory.ReadOnlySpan2D[2, 3]"; + + Assert.AreEqual(text, expected); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_opEquals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d_1 = new ReadOnlySpan2D(array); + ReadOnlySpan2D span2d_2 = new ReadOnlySpan2D(array); + + Assert.IsTrue(span2d_1 == span2d_2); + Assert.IsFalse(span2d_1 == ReadOnlySpan2D.Empty); + Assert.IsTrue(ReadOnlySpan2D.Empty == ReadOnlySpan2D.Empty); + + ReadOnlySpan2D span2d_3 = new ReadOnlySpan2D(array, 0, 0, 2, 2); + + Assert.IsFalse(span2d_1 == span2d_3); + Assert.IsFalse(span2d_3 == ReadOnlySpan2D.Empty); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ImplicitCast() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + ReadOnlySpan2D span2d_1 = array; + ReadOnlySpan2D span2d_2 = new ReadOnlySpan2D(array); + + Assert.IsTrue(span2d_1 == span2d_2); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_GetRow() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan2D(array).GetRow(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(value), ref array[1, i++])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan2D(array).GetRow(1); + + int[] expected = { 4, 5, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.AreSame(default(ReadOnlyRefEnumerable).ToArray(), Array.Empty()); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetRow(-1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetRow(2)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetRow(1000)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_Pointer_GetRow() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan2D(array, 2, 3, 0).GetRow(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(value), ref array[3 + i++])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan2D(array, 2, 3, 0).GetRow(1); + + int[] expected = { 4, 5, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetRow(-1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetRow(2)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetRow(1000)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_GetColumn() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan2D(array).GetColumn(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(value), ref array[i++, 1])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan2D(array).GetColumn(2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetColumn(-1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetColumn(3)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array).GetColumn(1000)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_Pointer_GetColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan2D(array, 2, 3, 0).GetColumn(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(value), ref array[(i++ * 3) + 1])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan2D(array, 2, 3, 0).GetColumn(2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetColumn(-1)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetColumn(3)); + Assert.ThrowsException(() => new ReadOnlySpan2D(array, 2, 3, 0).GetColumn(1000)); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_GetEnumerator() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + int[] result = new int[4]; + int i = 0; + + foreach (var item in new ReadOnlySpan2D(array, 0, 1, 2, 2)) + { + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public unsafe void Test_ReadOnlySpan2DT_Pointer_GetEnumerator() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + int[] result = new int[4]; + int i = 0; + + foreach (var item in new ReadOnlySpan2D(array + 1, 2, 2, 1)) + { + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_GetEnumerator_Empty() + { + var enumerator = ReadOnlySpan2D.Empty.GetEnumerator(); + + Assert.IsFalse(enumerator.MoveNext()); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ReadOnlyRefEnumerable_Misc() + { + int[,] array1 = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }; + + ReadOnlySpan2D span1 = array1; + + int[,] array2 = new int[4, 4]; + + // Copy to enumerable with source step == 1, destination step == 1 + span1.GetRow(0).CopyTo(array2.GetRow(0)); + + // Copy enumerable with source step == 1, destination step != 1 + span1.GetRow(1).CopyTo(array2.GetColumn(1)); + + // Copy enumerable with source step != 1, destination step == 1 + span1.GetColumn(2).CopyTo(array2.GetRow(2)); + + // Copy enumerable with source step != 1, destination step != 1 + span1.GetColumn(3).CopyTo(array2.GetColumn(3)); + + int[,] result = + { + { 1, 5, 3, 4 }, + { 0, 6, 0, 8 }, + { 3, 7, 11, 12 }, + { 0, 8, 0, 16 } + }; + + CollectionAssert.AreEqual(array2, result); + + // Test a valid and an invalid TryCopyTo call with the RefEnumerable overload + bool shouldBeTrue = span1.GetRow(0).TryCopyTo(array2.GetColumn(0)); + bool shouldBeFalse = span1.GetRow(0).TryCopyTo(default(RefEnumerable)); + + result = new[,] + { + { 1, 5, 3, 4 }, + { 2, 6, 0, 8 }, + { 3, 7, 11, 12 }, + { 4, 8, 0, 16 } + }; + + CollectionAssert.AreEqual(array2, result); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); + } + + [TestCategory("ReadOnlySpan2DT")] + [TestMethod] + public void Test_ReadOnlySpan2DT_ReadOnlyRefEnumerable_Cast() + { + int[,] array1 = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }; + + int[] result = { 5, 6, 7, 8 }; + + // Cast a RefEnumerable to a readonly one and verify the contents + int[] row = ((ReadOnlyRefEnumerable)array1.GetRow(1)).ToArray(); + + CollectionAssert.AreEqual(result, row); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Span2D{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Span2D{T}.cs new file mode 100644 index 00000000000..c5c495a1504 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Memory/Test_Span2D{T}.cs @@ -0,0 +1,1034 @@ +// 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.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; +using Microsoft.Toolkit.HighPerformance.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Memory +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_Span2DT + { + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Empty() + { + // Like in the tests for Memory2D, here we validate a number of empty spans + Span2D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(empty1.Length, 0); + Assert.AreEqual(empty1.Width, 0); + Assert.AreEqual(empty1.Height, 0); + + Span2D empty2 = Span2D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(empty2.Length, 0); + Assert.AreEqual(empty2.Width, 0); + Assert.AreEqual(empty2.Height, 0); + + Span2D empty3 = new int[4, 0]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(empty3.Length, 0); + Assert.AreEqual(empty3.Width, 0); + Assert.AreEqual(empty3.Height, 4); + + Span2D empty4 = new int[0, 7]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(empty4.Length, 0); + Assert.AreEqual(empty4.Width, 7); + Assert.AreEqual(empty4.Height, 0); + } + +#if !WINDOWS_UWP + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_RefConstructor() + { + Span span = stackalloc[] + { + 1, 2, 3, 4, 5, 6 + }; + + // Test for a Span2D instance created from a target reference. This is only supported + // on runtimes with fast Span support (as we need the API to power this with just a ref). + Span2D span2d = Span2D.DangerousCreate(ref span[0], 2, 3, 0); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 0] = 99; + span2d[1, 2] = 101; + + // Validate that those values were mapped to the right spot in the target span + Assert.AreEqual(span[0], 99); + Assert.AreEqual(span[5], 101); + + // A few cases with invalid indices + Assert.ThrowsException(() => Span2D.DangerousCreate(ref Unsafe.AsRef(null), -1, 0, 0)); + Assert.ThrowsException(() => Span2D.DangerousCreate(ref Unsafe.AsRef(null), 1, -2, 0)); + Assert.ThrowsException(() => Span2D.DangerousCreate(ref Unsafe.AsRef(null), 1, 0, -5)); + } +#endif + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_PtrConstructor() + { + int* ptr = stackalloc[] + { + 1, + 2, + 3, + 4, + 5, + 6 + }; + + // Same as above, but creating a Span2D from a raw pointer + Span2D span2d = new Span2D(ptr, 2, 3, 0); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 0] = 99; + span2d[1, 2] = 101; + + Assert.AreEqual(ptr[0], 99); + Assert.AreEqual(ptr[5], 101); + + Assert.ThrowsException(() => new Span2D((void*)0, -1, 0, 0)); + Assert.ThrowsException(() => new Span2D((void*)0, 1, -2, 0)); + Assert.ThrowsException(() => new Span2D((void*)0, 1, 0, -5)); + Assert.ThrowsException(() => new Span2D((void*)0, 2, 2, 0)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, 6 + }; + + // Same as above, but wrapping a 1D array with data in row-major order + Span2D span2d = new Span2D(array, 1, 2, 2, 1); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 0] = 99; + span2d[1, 1] = 101; + + Assert.AreEqual(array[1], 99); + Assert.AreEqual(array[5], 101); + + // The first check fails due to the array covariance test mentioned in the Memory2D tests. + // The others just validate a number of cases with invalid arguments (eg. out of range). + Assert.ThrowsException(() => new Span2D(new string[1], 1, 1)); + Assert.ThrowsException(() => new Span2D(array, -99, 1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array, 0, -10, 1, 1)); + Assert.ThrowsException(() => new Span2D(array, 0, 1, 1, -1)); + Assert.ThrowsException(() => new Span2D(array, 0, 1, -100, 1)); + Assert.ThrowsException(() => new Span2D(array, 0, 10, 1, 120)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Array2DConstructor_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but directly wrapping a 2D array + Span2D span2d = new Span2D(array); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 1] = 99; + span2d[1, 2] = 101; + + Assert.AreEqual(array[0, 1], 99); + Assert.AreEqual(array[1, 2], 101); + + Assert.ThrowsException(() => new Span2D(new string[1, 2])); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Array2DConstructor_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with a custom slicing over the target 2D array + Span2D span2d = new Span2D(array, 0, 1, 2, 2); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 0] = 99; + span2d[1, 1] = 101; + + Assert.AreEqual(array[0, 1], 99); + Assert.AreEqual(array[1, 2], 101); + + Assert.ThrowsException(() => new Span2D(new string[1, 2], 0, 0, 2, 2)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Here we wrap a layer in a 3D array instead, the rest is the same + Span2D span2d = new Span2D(array, 1); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 6); + Assert.AreEqual(span2d.Width, 3); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 1] = 99; + span2d[1, 2] = 101; + + Assert.AreEqual(span2d[0, 0], 10); + Assert.AreEqual(array[1, 0, 1], 99); + Assert.AreEqual(array[1, 1, 2], 101); + + Assert.ThrowsException(() => new Span2D(array, -1)); + Assert.ThrowsException(() => new Span2D(array, 20)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but also slicing a target 2D area in the 3D array layer + Span2D span2d = new Span2D(array, 1, 0, 1, 2, 2); + + Assert.IsFalse(span2d.IsEmpty); + Assert.AreEqual(span2d.Length, 4); + Assert.AreEqual(span2d.Width, 2); + Assert.AreEqual(span2d.Height, 2); + + span2d[0, 1] = 99; + span2d[1, 1] = 101; + + Assert.AreEqual(span2d[0, 0], 20); + Assert.AreEqual(array[1, 0, 2], 99); + Assert.AreEqual(array[1, 1, 2], 101); + + Assert.ThrowsException(() => new Span2D(array, -1, 1, 1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array, 1, -1, 1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array, 1, 1, -1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array, 1, 1, 1, -1, 1)); + Assert.ThrowsException(() => new Span2D(array, 1, 1, 1, 1, -1)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_FillAndClear_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Tests for the Fill and Clear APIs for Span2D. These should fill + // or clear the entire wrapped 2D array (just like eg. Span.Fill). + Span2D span2d = new Span2D(array); + + span2d.Fill(42); + + Assert.IsTrue(array.Cast().All(n => n == 42)); + + span2d.Clear(); + + Assert.IsTrue(array.Cast().All(n => n == 0)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Fill_Empty() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with an initial slicing as well to ensure + // these method work correctly with different internal offsets + Span2D span2d = new Span2D(array, 0, 0, 0, 0); + + span2d.Fill(42); + + CollectionAssert.AreEqual(array, array); + + span2d.Clear(); + + CollectionAssert.AreEqual(array, array); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_FillAndClear_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, just with different slicing to a target smaller 2D area + Span2D span2d = new Span2D(array, 0, 1, 2, 2); + + span2d.Fill(42); + + int[,] filled = + { + { 1, 42, 42 }, + { 4, 42, 42 } + }; + + CollectionAssert.AreEqual(array, filled); + + span2d.Clear(); + + int[,] cleared = + { + { 1, 0, 0 }, + { 4, 0, 0 } + }; + + CollectionAssert.AreEqual(array, cleared); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_CopyTo_Empty() + { + Span2D span2d = Span2D.Empty; + + int[] target = new int[0]; + + // Copying an emoty Span2D to an empty array is just a no-op + span2d.CopyTo(target); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_CopyTo_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + int[] target = new int[array.Length]; + + // Here we copy a Span2D to a target Span mapping an array. + // This is valid, and the data will just be copied in row-major order. + span2d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + // Exception due to the target span being too small for the source Span2D instance + Assert.ThrowsException(() => new Span2D(array).CopyTo(Span.Empty)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_CopyTo_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with different initial slicing + Span2D span2d = new Span2D(array, 0, 1, 2, 2); + + int[] target = new int[4]; + + span2d.CopyTo(target); + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(target, expected); + + Assert.ThrowsException(() => new Span2D(array).CopyTo(Span.Empty)); + Assert.ThrowsException(() => new Span2D(array, 0, 1, 2, 2).CopyTo(Span.Empty)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_CopyTo2D_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + int[,] target = new int[2, 3]; + + // Same as above, but copying to a target Span2D instead. Note + // that this method uses the implicit T[,] to Span2D conversion. + span2d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + Assert.ThrowsException(() => new Span2D(array).CopyTo(Span2D.Empty)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_CopyTo2D_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with extra initial slicing + Span2D span2d = new Span2D(array, 0, 1, 2, 2); + + int[,] target = new int[2, 2]; + + span2d.CopyTo(target); + + int[,] expected = + { + { 2, 3 }, + { 5, 6 } + }; + + CollectionAssert.AreEqual(target, expected); + + Assert.ThrowsException(() => new Span2D(array).CopyTo(new Span2D(target))); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_TryCopyTo() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + int[] target = new int[array.Length]; + + // Here we test the safe TryCopyTo method, which will fail gracefully + Assert.IsTrue(span2d.TryCopyTo(target)); + Assert.IsFalse(span2d.TryCopyTo(Span.Empty)); + + int[] expected = { 1, 2, 3, 4, 5, 6 }; + + CollectionAssert.AreEqual(target, expected); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_TryCopyTo2D() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but copying to a 2D array with the safe TryCopyTo method + Span2D span2d = new Span2D(array); + + int[,] target = new int[2, 3]; + + Assert.IsTrue(span2d.TryCopyTo(target)); + Assert.IsFalse(span2d.TryCopyTo(Span2D.Empty)); + + int[,] expected = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + CollectionAssert.AreEqual(target, expected); + } + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_GetPinnableReference() + { + // Here we test that a ref from an empty Span2D returns a null ref + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref Span2D.Empty.GetPinnableReference())); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + ref int r0 = ref span2d.GetPinnableReference(); + + // Here we test that GetPinnableReference returns a ref to the first array element + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0])); + } + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_DangerousGetReference() + { + // Same as above, but using DangerousGetReference instead (faster, no conditional check) + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref Span2D.Empty.DangerousGetReference())); + + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + ref int r0 = ref span2d.DangerousGetReference(); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0])); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Slice_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Here we have a number of tests that just take an initial 2D array, create a Span2D, + // perform a number of slicing operations and then validate the parameters for the resulting + // instances, and that the indexer works correctly and maps to the right original elements. + Span2D span2d = new Span2D(array); + + Span2D slice1 = span2d.Slice(1, 1, 1, 2); + + Assert.AreEqual(slice1.Length, 2); + Assert.AreEqual(slice1.Height, 1); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1[0, 0], 5); + Assert.AreEqual(slice1[0, 1], 6); + + Span2D slice2 = span2d.Slice(0, 1, 2, 2); + + Assert.AreEqual(slice2.Length, 4); + Assert.AreEqual(slice2.Height, 2); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2[0, 0], 2); + Assert.AreEqual(slice2[1, 0], 5); + Assert.AreEqual(slice2[1, 1], 6); + + // Some checks for invalid arguments + Assert.ThrowsException(() => new Span2D(array).Slice(-1, 1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array).Slice(1, -1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array).Slice(1, 1, 1, -1)); + Assert.ThrowsException(() => new Span2D(array).Slice(1, 1, -1, 1)); + Assert.ThrowsException(() => new Span2D(array).Slice(10, 1, 1, 1)); + Assert.ThrowsException(() => new Span2D(array).Slice(1, 12, 1, 12)); + Assert.ThrowsException(() => new Span2D(array).Slice(1, 1, 55, 1)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_Slice_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // Same as above, but with some different slicing + Span2D slice1 = span2d.Slice(0, 0, 2, 2); + + Assert.AreEqual(slice1.Length, 4); + Assert.AreEqual(slice1.Height, 2); + Assert.AreEqual(slice1.Width, 2); + Assert.AreEqual(slice1[0, 0], 1); + Assert.AreEqual(slice1[1, 1], 5); + + Span2D slice2 = slice1.Slice(1, 0, 1, 2); + + Assert.AreEqual(slice2.Length, 2); + Assert.AreEqual(slice2.Height, 1); + Assert.AreEqual(slice2.Width, 2); + Assert.AreEqual(slice2[0, 0], 4); + Assert.AreEqual(slice2[0, 1], 5); + + Span2D slice3 = slice2.Slice(0, 1, 1, 1); + + Assert.AreEqual(slice3.Length, 1); + Assert.AreEqual(slice3.Height, 1); + Assert.AreEqual(slice3.Width, 1); + Assert.AreEqual(slice3[0, 0], 5); + } + +#if !WINDOWS_UWP + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_GetRowSpan() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // Here we create a Span2D from a 2D array and want to get a Span from + // a specific row. This is only supported on runtimes with fast Span support + // for the same reason mentioned in the Memory2D tests (we need the Span + // constructor that only takes a target ref). Then we just get some references + // to items in this span and compare them against references into the original + // 2D array to ensure they match and point to the correct elements from there. + Span span = span2d.GetRowSpan(1); + + Assert.IsTrue(Unsafe.AreSame( + ref span[0], + ref array[1, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref span[2], + ref array[1, 2])); + + Assert.ThrowsException(() => new Span2D(array).GetRowSpan(-1)); + Assert.ThrowsException(() => new Span2D(array).GetRowSpan(5)); + } +#endif + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_TryGetSpan_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // This API tries to get a Span for the entire contents of Span2D. + // This only works on runtimes if the underlying data is contiguous + // and of a size that can fit into a single Span. In this specific test, + // this is not expected to work on UWP because it can't create a Span + // from a 2D array (reasons explained in the comments for the test above). + bool success = span2d.TryGetSpan(out Span span); + +#if WINDOWS_UWP + // Can't get a Span over a T[,] array on UWP + Assert.IsFalse(success); + Assert.AreEqual(span.Length, 0); +#else + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span2d.Length); +#endif + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_TryGetSpan_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but this will always fail because we're creating + // a Span2D wrapping non contiguous data (the pitch is not 0). + Span2D span2d = new Span2D(array, 0, 0, 2, 2); + + bool success = span2d.TryGetSpan(out Span span); + + Assert.IsFalse(success); + Assert.IsTrue(span.IsEmpty); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_ToArray_1() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Here we create a Span2D and verify that ToArray() produces + // a 2D array that is identical to the original one being wrapped. + Span2D span2d = new Span2D(array); + + int[,] copy = span2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_ToArray_2() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but with extra initial slicing + Span2D span2d = new Span2D(array, 0, 0, 2, 2); + + int[,] copy = span2d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), 2); + Assert.AreEqual(copy.GetLength(1), 2); + + int[,] expected = + { + { 1, 2 }, + { 4, 5 } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestCategory("Span2DT")] + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Test_Span2DT_Equals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // Span2D.Equals always throw (this mirrors the behavior of Span.Equals) + _ = span2d.Equals(null); + } + + [TestCategory("Span2DT")] + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Test_Span2DT_GetHashCode() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // Same as above, this always throws + _ = span2d.GetHashCode(); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_ToString() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + Span2D span2d = new Span2D(array); + + // Verify that we get the nicely formatted string + string text = span2d.ToString(); + + const string expected = "Microsoft.Toolkit.HighPerformance.Memory.Span2D[2, 3]"; + + Assert.AreEqual(text, expected); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_opEquals() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Create two Span2D instances wrapping the same array with the same + // parameters, and verify that the equality operators work correctly. + Span2D span2d_1 = new Span2D(array); + Span2D span2d_2 = new Span2D(array); + + Assert.IsTrue(span2d_1 == span2d_2); + Assert.IsFalse(span2d_1 == Span2D.Empty); + Assert.IsTrue(Span2D.Empty == Span2D.Empty); + + // Same as above, but verify that a sliced span is not reported as equal + Span2D span2d_3 = new Span2D(array, 0, 0, 2, 2); + + Assert.IsFalse(span2d_1 == span2d_3); + Assert.IsFalse(span2d_3 == Span2D.Empty); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_ImplicitCast() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Verify that an explicit constructor and the implicit conversion + // operator generate an identical Span2D instance from the array. + Span2D span2d_1 = array; + Span2D span2d_2 = new Span2D(array); + + Assert.IsTrue(span2d_1 == span2d_2); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_GetRow() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Get a target row and verify the contents match with our data + RefEnumerable enumerable = new Span2D(array).GetRow(1); + + int[] expected = { 4, 5, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new Span2D(array).GetRow(-1)); + Assert.ThrowsException(() => new Span2D(array).GetRow(2)); + Assert.ThrowsException(() => new Span2D(array).GetRow(1000)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_Pointer_GetRow() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + // Same as above, but with a Span2D wrapping a raw pointer + RefEnumerable enumerable = new Span2D(array, 2, 3, 0).GetRow(1); + + int[] expected = { 4, 5, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetRow(-1)); + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetRow(2)); + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetRow(1000)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_GetColumn() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + // Same as above, but getting a column instead + RefEnumerable enumerable = new Span2D(array).GetColumn(2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new Span2D(array).GetColumn(-1)); + Assert.ThrowsException(() => new Span2D(array).GetColumn(3)); + Assert.ThrowsException(() => new Span2D(array).GetColumn(1000)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_Pointer_GetColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + // Same as above, but wrapping a raw pointer + RefEnumerable enumerable = new Span2D(array, 2, 3, 0).GetColumn(2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetColumn(-1)); + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetColumn(3)); + Assert.ThrowsException(() => new Span2D(array, 2, 3, 0).GetColumn(1000)); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_GetEnumerator() + { + int[,] array = + { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + int[] result = new int[4]; + int i = 0; + + // Here we want to test the Span2D enumerator. We create a Span2D instance over + // a given section of the initial 2D array, then iterate over it and store the items + // into a temporary array. We then just compare the contents to ensure they match. + foreach (ref var item in new Span2D(array, 0, 1, 2, 2)) + { + // Check the reference to ensure it points to the right original item + Assert.IsTrue(Unsafe.AreSame( + ref array[i / 2, (i % 2) + 1], + ref item)); + + // Also store the value to compare it later (redundant, but just in case) + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestCategory("Span2DT")] + [TestMethod] + public unsafe void Test_Span2DT_Pointer_GetEnumerator() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6 + }; + + int[] result = new int[4]; + int i = 0; + + // Same test as above, but wrapping a raw pointer + foreach (ref var item in new Span2D(array + 1, 2, 2, 1)) + { + // Check the reference again + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(&array[((i / 2) * 3) + (i % 2) + 1]), + ref item)); + + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestCategory("Span2DT")] + [TestMethod] + public void Test_Span2DT_GetEnumerator_Empty() + { + var enumerator = Span2D.Empty.GetEnumerator(); + + // Ensure that an enumerator from an empty Span2D can't move next + Assert.IsFalse(enumerator.MoveNext()); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems index cb1c3a5a85f..6e776be38bb 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems @@ -16,8 +16,9 @@ + - + @@ -33,12 +34,18 @@ + + + + + +