diff --git a/Microsoft.Toolkit.HighPerformance/Box{T}.cs b/Microsoft.Toolkit.HighPerformance/Box{T}.cs new file mode 100644 index 00000000000..c98de01e580 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Box{T}.cs @@ -0,0 +1,219 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance +{ + /// + /// A that represents a boxed value on the managed heap. + /// + /// The type of value being boxed. + [DebuggerDisplay("{ToString(),raw}")] + public sealed class Box + where T : struct + { + // Boxed value types in the CLR are represented in memory as simple objects that store the method table of + // the corresponding T value type being boxed, and then the data of the value being boxed: + // [ sync block || pMethodTable || boxed T value ] + // ^ ^ + // | \-- Unsafe.Unbox(Box) + // \-- Box reference + // For more info, see: https://mattwarren.org/2017/08/02/A-look-at-the-internals-of-boxing-in-the-CLR/. + // Note that there might be some padding before the actual data representing the boxed value, + // which might depend on both the runtime and the exact CPU architecture. + // This is automatically handled by the unbox !!T instruction in IL, which + // unboxes a given value type T and returns a reference to its boxed data. + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor is never used, it is only declared in order to mark it with + /// the visibility modifier and prevent direct use. + /// + private Box() + { + } + + /// + /// Returns a reference from the input instance. + /// + /// The input instance, representing a boxed value. + /// A reference pointing to . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Box GetFrom(object obj) + { + if (obj.GetType() != typeof(T)) + { + ThrowInvalidCastExceptionForGetFrom(); + } + + return Unsafe.As>(obj); + } + + /// + /// Returns a reference from the input instance. + /// + /// The input instance, representing a boxed value. + /// A reference pointing to . + /// + /// This method doesn't check the actual type of , so it is responsability of the caller + /// to ensure it actually represents a boxed value and not some other instance. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Box DangerousGetFrom(object obj) + { + return Unsafe.As>(obj); + } + + /// + /// Tries to get a reference from an input representing a boxed value. + /// + /// The input instance to check. + /// The resulting reference, if was a boxed value. + /// if a instance was retrieved correctly, otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1115", Justification = "Comment for [NotNullWhen] attribute")] + public static bool TryGetFrom( + object obj, +#if NETSTANDARD2_1 + /* On .NET Standard 2.1, we can add the [NotNullWhen] attribute + * to let the code analysis engine know that whenever this method + * returns true, box will always be assigned to a non-null value. + * This will eliminate the null warnings when in a branch that + * is only taken when this method returns true. */ + [NotNullWhen(true)] +#endif + out Box? box) + { + if (obj.GetType() == typeof(T)) + { + box = Unsafe.As>(obj); + + return true; + } + + box = null; + + return false; + } + + /// + /// Implicitly gets the value from a given instance. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(Box box) + { + return Unsafe.Unbox(box); + } + + /// + /// Implicitly creates a new instance from a given value. + /// + /// The input value to wrap. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Box(T value) + { + /* The Box type is never actually instantiated. + * Here we are just boxing the input T value, and then reinterpreting + * that object reference as a Box reference. As such, the Box + * type is really only used as an interface to access the contents + * of a boxed value type. This also makes it so that additional methods + * like ToString() or GetHashCode() will automatically be referenced from + * the method table of the boxed object, meaning that they don't need to + * manually be implemented in the Box type. For instance, boxing a float + * and calling ToString() on it directly, on its boxed object or on a Box + * reference retrieved from it will produce the same result in all cases. */ + return Unsafe.As>(value); + } + + /// + public override string ToString() + { + /* Here we're overriding the base object virtual methods to ensure + * calls to those methods have a correct results on all runtimes. + * For instance, not doing so is causing issue on .NET Core 2.1 Release + * due to how the runtime handles the Box reference to an actual + * boxed T value (not a concrete Box instance as it would expect). + * To fix that, the overrides will simply call the expected methods + * directly on the boxed T values. These methods will be directly + * invoked by the JIT compiler when using a Box reference. When + * an object reference is used instead, the call would be forwarded + * to those same methods anyway, since the method table for an object + * representing a T instance is the one of type T anyway. */ + return this.GetReference().ToString(); + } + + /// + public override bool Equals(object obj) + { + return Equals(this, obj); + } + + /// + public override int GetHashCode() + { + return this.GetReference().GetHashCode(); + } + + /// + /// Throws an when a cast from an invalid is attempted. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidCastExceptionForGetFrom() + { + throw new InvalidCastException($"Can't cast the input object to the type Box<{typeof(T)}>"); + } + } + + /// + /// Helpers for working with the type. + /// + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402", Justification = "Extension class to replace instance methods for Box")] + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204", Justification = "Extensions being declared after the type they apply to")] + public static class BoxExtensions + { + /// + /// Gets a reference from a instance. + /// + /// The type of reference to retrieve. + /// The input instance. + /// A reference to the boxed value within . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReference(this Box box) + where T : struct + { + /* The reason why this method is an extension and is not part of + * the Box type itself is that Box is really just a mask + * used over object references, but it is never actually instantiated. + * Because of this, the method table of the objects in the heap will + * be the one of type T created by the runtime, and not the one of + * the Box type. To avoid potential issues when invoking this method + * on different runtimes, which might handle that scenario differently, + * we use an extension method, which is just syntactic sugar for a static + * method belonging to another class. This isn't technically necessary, + * but it's just an extra precaution since the syntax for users remains + * exactly the same anyway. Here we just call the Unsafe.Unbox(object) + * API, which is hidden away for users of the type for simplicity. + * Note that this API will always actually involve a conditional + * branch, which is introduced by the JIT compiler to validate the + * object instance being unboxed. But since the alternative of + * manually tracking the offset to the boxed data would be both + * more error prone, and it would still introduce some overhead, + * this doesn't really matter in this case anyway. */ + return ref Unsafe.Unbox(box); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs new file mode 100644 index 00000000000..dbed50f49c1 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/ArrayPoolBufferWriter{T}.cs @@ -0,0 +1,353 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Views; +using Microsoft.Toolkit.HighPerformance.Extensions; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Buffers +{ + /// + /// Represents a heap-based, array-backed output sink into which data can be written. + /// + /// The type of items to write to the current instance. + /// + /// This is a custom implementation that replicates the + /// functionality and API surface of the array-based buffer writer available in + /// .NET Standard 2.1, with the main difference being the fact that in this case + /// the arrays in use are rented from the shared instance, + /// and that is also available on .NET Standard 2.0. + /// + [DebuggerTypeProxy(typeof(ArrayPoolBufferWriterDebugView<>))] + [DebuggerDisplay("{ToString(),raw}")] + public sealed class ArrayPoolBufferWriter : IBufferWriter, IMemoryOwner + { + /// + /// The default buffer size to use to expand empty arrays. + /// + private const int DefaultInitialBufferSize = 256; + + /// + /// The underlying array. + /// + private T[]? array; + +#pragma warning disable IDE0032 + /// + /// The starting offset within . + /// + private int index; +#pragma warning restore IDE0032 + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferWriter() + { + /* Since we're using pooled arrays, we can rent the buffer with the + * default size immediately, we don't need to use lazy initialization + * to save unnecessary memory allocations in this case. */ + this.array = ArrayPool.Shared.Rent(DefaultInitialBufferSize); + this.index = 0; + } + + /// + /// Initializes a new instance of the class. + /// + /// The minimum capacity with which to initialize the underlying buffer. + /// + /// Thrown when is not positive (i.e. less than or equal to 0). + /// + public ArrayPoolBufferWriter(int initialCapacity) + { + if (initialCapacity <= 0) + { + ThrowArgumentOutOfRangeExceptionForInitialCapacity(); + } + + this.array = ArrayPool.Shared.Rent(initialCapacity); + this.index = 0; + } + + /// + /// Finalizes an instance of the class. + /// + ~ArrayPoolBufferWriter() => this.Dispose(); + + /// + Memory IMemoryOwner.Memory + { + /* This property is explicitly implemented so that it's hidden + * under normal usage, as the name could be confusing when + * displayed besides WrittenMemory and GetMemory(). + * The IMemoryOwner interface is implemented primarily + * so that the AsStream() extension can be used on this type, + * allowing users to first create a ArrayPoolBufferWriter + * instance to write data to, then get a stream through the + * extension and let it take care of returning the underlying + * buffer to the shared pool when it's no longer necessary. + * Inlining is not needed here since this will always be a callvirt. */ + get => MemoryMarshal.AsMemory(WrittenMemory); + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public ReadOnlyMemory WrittenMemory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsMemory(0, this.index); + } + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsSpan(0, this.index); + } + } + + /// + /// Gets the amount of data written to the underlying buffer so far. + /// + public int WrittenCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.index; + } + + /// + /// Gets the total amount of space within the underlying buffer. + /// + public int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length; + } + } + + /// + /// Gets the amount of space available that can still be written into without forcing the underlying buffer to grow. + /// + public int FreeCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length - this.index; + } + } + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// You must clear the before trying to re-use it. + /// + public void Clear() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + array.AsSpan(0, this.index).Clear(); + this.index = 0; + } + + /// + public void Advance(int count) + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + if (count < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeCount(); + } + + if (this.index > array!.Length - count) + { + ThrowArgumentExceptionForAdvancedTooFar(); + } + + this.index += count; + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + + return this.array.AsMemory(this.index); + } + + /// + public Span GetSpan(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + + return this.array.AsSpan(this.index); + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckAndResizeBuffer(int sizeHint) + { + if (this.array is null) + { + ThrowObjectDisposedException(); + } + + if (sizeHint < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeSizeHint(); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > FreeCapacity) + { + int minimumSize = this.index + sizeHint; + + ArrayPool.Shared.Resize(ref this.array, minimumSize); + } + } + + /// + public void Dispose() + { + T[]? array = this.array; + + if (array is null) + { + return; + } + + GC.SuppressFinalize(this); + + this.array = null; + + ArrayPool.Shared.Return(array); + } + + /// + [Pure] + public override string ToString() + { + // See comments in MemoryOwner about this + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new string(chars, 0, this.index); + } + + // Same representation used in Span + return $"Microsoft.Toolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<{typeof(T)}>[{this.index}]"; + } + + /// + /// Throws an when the initial capacity is invalid. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204", Justification = "Exception throwers at the end of class")] + private static void ThrowArgumentOutOfRangeExceptionForInitialCapacity() + { + throw new ArgumentOutOfRangeException("initialCapacity", "The initial capacity must be a positive value"); + } + + /// + /// Throws an when the requested count is negative. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForNegativeCount() + { + throw new ArgumentOutOfRangeException("count", "The count can't be a negative value"); + } + + /// + /// Throws an when the size hint is negative. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint() + { + throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value"); + } + + /// + /// Throws an when the requested count is negative. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentExceptionForAdvancedTooFar() + { + throw new ArgumentException("The buffer writer has advanced too far"); + } + + /// + /// Throws an when is . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException("The current buffer has already been disposed"); + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Enums/AllocationMode.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Enums/AllocationMode.cs new file mode 100644 index 00000000000..08663ba76b8 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Enums/AllocationMode.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.HighPerformance.Buffers +{ + /// + /// An that indicates a mode to use when allocating buffers. + /// + public enum AllocationMode + { + /// + /// The default allocation mode for pooled memory (rented buffers are not cleared). + /// + Default, + + /// + /// Clear pooled buffers when renting them. + /// + Clear + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs new file mode 100644 index 00000000000..626293d5b85 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs @@ -0,0 +1,282 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Views; +using Microsoft.Toolkit.HighPerformance.Extensions; + +#nullable enable + +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<>))] + [DebuggerDisplay("{ToString(),raw}")] + public sealed class MemoryOwner : IMemoryOwner + { + /// + /// The starting offset within . + /// + private readonly int start; + +#pragma warning disable IDE0032 + /// + /// The usable length within (starting from ). + /// + private readonly int length; +#pragma warning restore IDE0032 + + /// + /// The underlying array. + /// + private T[]? array; + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + private MemoryOwner(int length, AllocationMode mode) + { + this.start = 0; + this.length = length; + this.array = ArrayPool.Shared.Rent(length); + + if (mode == AllocationMode.Clear) + { + this.array.AsSpan(0, length).Clear(); + } + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input array to use. + /// The starting offset within . + /// The length of the array to use. + private MemoryOwner(T[] array, int start, int length) + { + this.start = start; + this.length = length; + this.array = array; + } + + /// + /// Finalizes an instance of the class. + /// + ~MemoryOwner() => this.Dispose(); + + /// + /// Gets an empty instance. + /// + [Pure] + public static MemoryOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new MemoryOwner(0, AllocationMode.Default); + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size) => new MemoryOwner(size, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryOwner Allocate(int size, AllocationMode mode) => new MemoryOwner(size, mode); + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.length; + } + + /// + public Memory Memory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new Memory(array!, this.start, this.length); + } + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new Span(array!, this.start, this.length); + } + } + + /// + /// 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. + /// Thrown when the buffer in use has already been disposed. + /// + /// This method does not perform bounds checks on the underlying buffer, but does check whether + /// the buffer itself has been disposed or not. This check should not be removed, and it's also + /// the reason why the method to get a reference at a specified offset is not present. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return ref array!.DangerousGetReferenceAt(this.start); + } + + /// + /// Slices the buffer currently in use and returns a new instance. + /// + /// The starting offset within the current buffer. + /// The length of the buffer to use. + /// A new instance using the target range of items. + /// Thrown when the buffer in use has already been disposed. + /// Thrown when or are not valid. + /// + /// Using this method will dispose the current instance, and should only be used when an oversized + /// buffer is rented and then adjusted in size, to avoid having to rent a new buffer of the new + /// size and copy the previous items into the new one, or needing an additional variable/field + /// to manually handle to track the used range within a given instance. + /// + [Pure] + public MemoryOwner Slice(int start, int length) + { + T[]? array = this.array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + this.array = null; + + if ((uint)start > this.length) + { + ThrowInvalidOffsetException(); + } + + if ((uint)length > (this.length - start)) + { + ThrowInvalidLengthException(); + } + + return new MemoryOwner(array!, start, length); + } + + /// + public void Dispose() + { + T[]? array = this.array; + + if (array is null) + { + return; + } + + GC.SuppressFinalize(this); + + this.array = null; + + ArrayPool.Shared.Return(array); + } + + /// + [Pure] + public override string ToString() + { + /* Normally we would throw if the array has been disposed, + * but in this case we'll just return the non formatted + * representation as a fallback, since the ToString method + * is generally expected not to throw exceptions. */ + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new string(chars, this.start, this.length); + } + + // Same representation used in Span + return $"Microsoft.Toolkit.HighPerformance.Buffers.MemoryOwner<{typeof(T)}>[{this.length}]"; + } + + /// + /// Throws an when is . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204", Justification = "Exception throwers at the end of class")] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(MemoryOwner), "The current buffer has already been disposed"); + } + + /// + /// Throws an when the is invalid. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidOffsetException() + { + throw new ArgumentOutOfRangeException(nameof(start), "The input start parameter was not valid"); + } + + /// + /// Throws an when the is invalid. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidLengthException() + { + throw new ArgumentOutOfRangeException(nameof(length), "The input length parameter was not valid"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs new file mode 100644 index 00000000000..c4a24758103 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs @@ -0,0 +1,155 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers.Views; +using Microsoft.Toolkit.HighPerformance.Extensions; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Buffers +{ + /// + /// A stack-only type with the ability to rent a buffer of a specified length and getting a from it. + /// This type mirrors but without allocations and with further optimizations. + /// As this is a stack-only type, it relies on the duck-typed pattern introduced with C# 8. + /// It should be used like so: + /// + /// using (SpanOwner<byte> buffer = SpanOwner<byte>.Allocate(1024)) + /// { + /// // Use the buffer here... + /// } + /// + /// As soon as the code leaves the scope of that block, the underlying buffer will automatically + /// be disposed. The APIs in rely on this pattern for extra performance, eg. they don't perform + /// the additional checks that are done in to ensure that the buffer hasn't been disposed + /// before returning a or instance from it. + /// As such, this type should always be used with a block or expression. + /// 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<>))] + [DebuggerDisplay("{ToString(),raw}")] + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + public readonly ref struct SpanOwner + { +#pragma warning disable IDE0032 + /// + /// The usable length within . + /// + private readonly int length; +#pragma warning restore IDE0032 + + /// + /// The underlying array. + /// + private readonly T[] array; + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + private SpanOwner(int length, AllocationMode mode) + { + this.length = length; + this.array = ArrayPool.Shared.Rent(length); + + if (mode == AllocationMode.Clear) + { + this.array.AsSpan(0, length).Clear(); + } + } + + /// + /// Gets an empty instance. + /// + [Pure] + public static SpanOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new SpanOwner(0, AllocationMode.Default); + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size) => new SpanOwner(size, AllocationMode.Default); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The length of the new memory buffer to use. + /// Indicates the allocation mode to use for the new buffer to rent. + /// A instance of the requested length. + /// Thrown when is not valid. + /// This method is just a proxy for the constructor, for clarity. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanOwner Allocate(int size, AllocationMode mode) => new SpanOwner(size, mode); + + /// + /// Gets the number of items in the current instance + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.length; + } + + /// + /// Gets a wrapping the memory belonging to the current instance. + /// + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new Span(array, 0, this.length); + } + + /// + /// 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() + { + return ref array.DangerousGetReference(); + } + + /// + /// Implements the duck-typed method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + ArrayPool.Shared.Return(array); + } + + /// + [Pure] + public override string ToString() + { + if (typeof(T) == typeof(char) && + this.array is char[] chars) + { + return new string(chars, 0, this.length); + } + + // Same representation used in Span + return $"Microsoft.Toolkit.HighPerformance.Buffers.SpanOwner<{typeof(T)}>[{this.length}]"; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs new file mode 100644 index 00000000000..8518c4d3901 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Views/ArrayPoolBufferWriterDebugView{T}.cs @@ -0,0 +1,32 @@ +// 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; + +#nullable enable + +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/MemoryOwnerDebugView{T}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryOwnerDebugView{T}.cs new file mode 100644 index 00000000000..f63e94f2740 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Views/MemoryOwnerDebugView{T}.cs @@ -0,0 +1,32 @@ +// 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; + +#nullable enable + +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 new file mode 100644 index 00000000000..a945769f01a --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Buffers/Views/SpanOwnerDebugView{T}.cs @@ -0,0 +1,32 @@ +// 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; + +#nullable enable + +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 new file mode 100644 index 00000000000..2698a2029f4 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DColumnEnumerable{T}.cs @@ -0,0 +1,163 @@ +// 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.CodeAnalysis; +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. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [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]; + + for (int i = 0; i < height; i++) + { + array.DangerousGetReferenceAt(i) = this.array.DangerousGetReferenceAt(i, this.column); + } + + 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 column to iterate within . + /// + private readonly int column; + + /// + /// The height of a column in . + /// + private readonly int height; + + /// + /// The current row. + /// + private int row; + + /// + /// 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(); + } + + this.array = array; + this.column = column; + this.height = array.GetLength(0); + this.row = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int row = this.row + 1; + + if (row < this.height) + { + this.row = row; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + public ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this.array.DangerousGetReferenceAt(this.row, this.column); + } + } + + /// + /// Throws an when the is invalid. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForInvalidColumn() + { + throw new ArgumentOutOfRangeException(nameof(column), "The target column parameter was not valid"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.cs new file mode 100644 index 00000000000..15c4aa335fd --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/Array2DRowEnumerable{T}.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. + +#if NETSTANDARD2_0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +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. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [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 => ref this.array.DangerousGetReferenceAt(this.row, this.column); + } + } + + /// + /// Throws an when the is invalid. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForInvalidRow() + { + throw new ArgumentOutOfRangeException(nameof(row), "The target row parameter was not valid"); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs new file mode 100644 index 00000000000..bd0b42c87df --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanEnumerable{T}.cs @@ -0,0 +1,107 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that enumerates the items in a given instance. + /// + /// The type of items to enumerate. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct ReadOnlySpanEnumerable + { + /// + /// The source instance + /// + private readonly ReadOnlySpan span; + + /// + /// Initializes a new instance of the struct. + /// + /// The source to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpanEnumerable(ReadOnlySpan span) + { + this.span = span; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this.span); + + /// + /// An enumerator for a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct Enumerator + { + /// + /// The source instance. + /// + private readonly ReadOnlySpan span; + + /// + /// The current index within . + /// + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(ReadOnlySpan span) + { + this.span = span; + this.index = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int newIndex = this.index + 1; + + if (newIndex < this.span.Length) + { + this.index = newIndex; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1008", Justification = "ValueTuple return type")] + public (int Index, T Value) Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + int currentIndex = this.index; + T value = Unsafe.Add(ref MemoryMarshal.GetReference(this.span), currentIndex); + + return (currentIndex, value); + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs new file mode 100644 index 00000000000..8d4287c52b2 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlySpanTokenizer{T}.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that tokenizes a given instance. + /// + /// The type of items to enumerate. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct ReadOnlySpanTokenizer + where T : IEquatable + { + /// + /// The source instance + /// + private readonly ReadOnlySpan span; + + /// + /// The separator item to use. + /// + private readonly T separator; + + /// + /// Initializes a new instance of the struct. + /// + /// The source to tokenize. + /// The separator item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpanTokenizer(ReadOnlySpan span, T separator) + { + this.span = span; + this.separator = separator; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this.span, this.separator); + + /// + /// An enumerator for a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct Enumerator + { + /// + /// The source instance. + /// + private readonly ReadOnlySpan span; + + /// + /// The separator item to use. + /// + private readonly T separator; + + /// + /// The current initial offset. + /// + private int start; + + /// + /// The current final offset. + /// + private int end; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + /// The separator item to use. + public Enumerator(ReadOnlySpan span, T separator) + { + this.span = span; + this.separator = separator; + this.start = 0; + this.end = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int + newEnd = this.end + 1, + length = this.span.Length; + + // Additional check if the separator is not the last character + if (newEnd <= length) + { + this.start = newEnd; + + int index = this.span.Slice(newEnd).IndexOf(this.separator); + + // Extract the current subsequence + if (index >= 0) + { + this.end = newEnd + index; + + return true; + } + + this.end = length; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + public ReadOnlySpan Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.span.Slice(this.start, this.end - this.start); + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs new file mode 100644 index 00000000000..0c245b6b475 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanEnumerable{T}.cs @@ -0,0 +1,193 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that enumerates the items in a given instance. + /// + /// The type of items to enumerate. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct SpanEnumerable + { + /// + /// The source instance + /// + private readonly Span span; + + /// + /// Initializes a new instance of the struct. + /// + /// The source to enumerate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanEnumerable(Span span) + { + this.span = span; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this.span); + + /// + /// An enumerator for a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct Enumerator + { + /// + /// The source instance. + /// + private readonly Span span; + + /// + /// The current index within . + /// + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(Span span) + { + this.span = span; + this.index = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int newIndex = this.index + 1; + + if (newIndex < this.span.Length) + { + this.index = newIndex; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + public Item Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NETSTANDARD2_1 + ref T r0 = ref MemoryMarshal.GetReference(this.span); + ref T ri = ref Unsafe.Add(ref r0, this.index); + + /* On .NET Standard 2.1 we can save 4 bytes by piggybacking + * the current index in the length of the wrapped span. + * We're going to use the first item as the target reference, + * and the length as a host for the current original offset. + * This is not possible on .NET Standard 2.1 as we lack + * the API to create spans from arbitrary references. */ + return new Item(ref ri, this.index); +#else + return new Item(this.span, this.index); +#endif + } + } + } + + /// + /// An item from a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct Item + { + /// + /// The source instance. + /// + private readonly Span span; + +#if NETSTANDARD2_1 + /// + /// Initializes a new instance of the struct. + /// + /// A reference to the target value. + /// The index of the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Item(ref T value, int index) + { + this.span = MemoryMarshal.CreateSpan(ref value, index); + } +#else + /// + /// The current index within . + /// + private readonly int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + /// The current index within . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Item(Span span, int index) + { + this.span = span; + this.index = index; + } +#endif + + /// + /// Gets the reference to the current value. + /// + public ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NETSTANDARD2_1 + 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); + + return ref ri; +#endif + } + } + + /// + /// Gets the current index. + /// + public int Index + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NETSTANDARD2_1 + return this.span.Length; +#else + return this.index; +#endif + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs new file mode 100644 index 00000000000..075b5c1c567 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.HighPerformance.Enumerables +{ + /// + /// A that tokenizes a given instance. + /// + /// The type of items to enumerate. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref struct SpanTokenizer + where T : IEquatable + { + /// + /// The source instance + /// + private readonly Span span; + + /// + /// The separator item to use. + /// + private readonly T separator; + + /// + /// Initializes a new instance of the struct. + /// + /// The source to tokenize. + /// The separator item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanTokenizer(Span span, T separator) + { + this.span = span; + this.separator = separator; + } + + /// + /// Implements the duck-typed method. + /// + /// An instance targeting the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this.span, this.separator); + + /// + /// An enumerator for a source instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct Enumerator + { + /// + /// The source instance. + /// + private readonly Span span; + + /// + /// The separator item to use. + /// + private readonly T separator; + + /// + /// The current initial offset. + /// + private int start; + + /// + /// The current final offset. + /// + private int end; + + /// + /// Initializes a new instance of the struct. + /// + /// The source instance. + /// The separator item to use. + public Enumerator(Span span, T separator) + { + this.span = span; + this.separator = separator; + this.start = 0; + this.end = -1; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int + newEnd = this.end + 1, + length = this.span.Length; + + // Additional check if the separator is not the last character + if (newEnd <= length) + { + this.start = newEnd; + + int index = this.span.Slice(newEnd).IndexOf(this.separator); + + // Extract the current subsequence + if (index >= 0) + { + this.end = newEnd + index; + + return true; + } + + this.end = length; + + return true; + } + + return false; + } + + /// + /// Gets the duck-typed property. + /// + public Span Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.span.Slice(this.start, this.end - this.start); + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs new file mode 100644 index 00000000000..958a70546c3 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs @@ -0,0 +1,260 @@ +// 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.Drawing; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; + +#nullable enable + +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 2D array, with no bounds checks. + /// + /// The type of elements in the input 2D 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) + { + var arrayData = Unsafe.As(array); + ref T r0 = ref Unsafe.As(ref arrayData.Data); + + return ref r0; + } + + /// + /// Returns a reference to an element at a specified coordinate within a given 2D array, with no bounds checks. + /// + /// The type of elements in the input 2D array instance. + /// The input 2D array instance. + /// 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) + { + var arrayData = Unsafe.As(array); + int offset = (i * arrayData.Width) + j; + ref T r0 = ref Unsafe.As(ref arrayData.Data); + ref T ri = ref Unsafe.Add(ref r0, offset); + + return ref ri; + } + + // Description adapted from CoreCLR: see https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,285. + // CLR 2D arrays are laid out in memory as follows: + // [ sync block || pMethodTable || Length (padded to IntPtr) || HxW || HxW bounds || array data .. ] + // ^ ^ + // | \-- ref Unsafe.As(array).Data + // \-- array + // The length is always padded to IntPtr just like with SZ arrays. + // The total data padding is therefore 20 bytes on x86 (4 + 4 + 4 + 4 + 4), or 24 bytes on x64. + [StructLayout(LayoutKind.Sequential)] + private sealed class RawArray2DData + { +#pragma warning disable CS0649 // Unassigned fields +#pragma warning disable SA1401 // Fields should be private + public IntPtr Length; + public int Height; + public int Width; + public int HeightLowerBound; + public int WidthLowerBound; + public byte Data; +#pragma warning restore CS0649 +#pragma warning restore SA1401 + } + + /// + /// 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 NETSTANDARD2_1 + ref T r0 = ref array[i, bounds.Left]; + + // Span.Fill will use vectorized instructions when possible + MemoryMarshal.CreateSpan(ref r0, bounds.Width).Fill(value); +#else + ref T r0 = ref array.DangerousGetReferenceAt(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. + /// + /// 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 . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static +#if NETSTANDARD2_1 + 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) + { + if ((uint)row >= (uint)array.GetLength(0)) + { + throw new ArgumentOutOfRangeException(nameof(row)); + } + +#if NETSTANDARD2_1 + ref T r0 = ref array.DangerousGetReferenceAt(row, 0); + + return MemoryMarshal.CreateSpan(ref r0, array.GetLength(1)); +#else + return new Array2DRowEnumerable(array, row); +#endif + } + + /// + /// Returns an enumerable 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 = + /// { + /// { 1, 2, 3 }, + /// { 4, 5, 6 }, + /// { 7, 8, 9 } + /// }; + /// + /// foreach (ref int number in matrix.GetColumn(1)) + /// { + /// // Access the current number by reference here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of elements in the input 2D array instance. + /// 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. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Array2DColumnEnumerable GetColumn(this T[,] array, int column) + { + return new Array2DColumnEnumerable(array, column); + } + +#if NETSTANDARD2_1 + /// + /// Cretes 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 Span AsSpan(this T[,] array) + { + var arrayData = Unsafe.As(array); + + /* 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); + + return MemoryMarshal.CreateSpan(ref r0, length); + } + + /// + /// Counts the number of occurrences of a given value into a target 2D array instance. + /// + /// The type of items in the input 2D array instance. + /// The input 2D 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 + { + return ReadOnlySpanExtensions.Count(array.AsSpan(), value); + } + + /// + /// Gets a content hash from the input 2D array instance using the Djb2 algorithm. + /// + /// The type of items in the input 2D array instance. + /// The input 2D array instance. + /// The Djb2 value for the input 2D 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 + { + return ReadOnlySpanExtensions.GetDjb2HashCode(array.AsSpan()); + } +#endif + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs new file mode 100644 index 00000000000..4af7e3fce53 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; + +#nullable enable + +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 array, with no bounds checks. + /// + /// The type of elements in the input 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) + { + var arrayData = Unsafe.As(array); + ref T r0 = ref Unsafe.As(ref arrayData.Data); + + return ref r0; + } + + /// + /// Returns a reference to an element at a specified index within a given array, with no bounds checks. + /// + /// The type of elements in the input array instance. + /// The input array 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 T[] array, int i) + { + var arrayData = Unsafe.As(array); + ref T r0 = ref Unsafe.As(ref arrayData.Data); + ref T ri = ref Unsafe.Add(ref r0, i); + + return ref ri; + } + + // Description taken from CoreCLR: see https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,285. + // CLR arrays are laid out in memory as follows (multidimensional array bounds are optional): + // [ sync block || pMethodTable || num components || MD array bounds || array data .. ] + // ^ ^ ^ returned reference + // | \-- ref Unsafe.As(array).Data + // \-- array + // The base size of an array includes all the fields before the array data, + // including the sync block and method table. The reference to RawData.Data + // points at the number of components, skipping over these two pointer-sized fields. + [StructLayout(LayoutKind.Sequential)] + private sealed class RawArrayData + { +#pragma warning disable CS0649 // Unassigned fields +#pragma warning disable SA1401 // Fields should be private + public IntPtr Length; + public byte Data; +#pragma warning restore CS0649 +#pragma warning restore SA1401 + } + + /// + /// Counts the number of occurrences of a given value into a target array instance. + /// + /// The type of items in the input array instance. + /// The input 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 + { + return ReadOnlySpanExtensions.Count(array, value); + } + + /// + /// Enumerates the items in the input array instance, as pairs of reference/index values. + /// This extension should be used directly within a loop: + /// + /// int[] numbers = new[] { 1, 2, 3, 4, 5, 6, 7 }; + /// + /// foreach (var item in numbers.Enumerate()) + /// { + /// // Access the index and value of each item here... + /// int index = item.Index; + /// ref int value = ref item.Value; + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items to enumerate. + /// The source array to enumerate. + /// A wrapper type that will handle the reference/index enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanEnumerable Enumerate(this T[] array) + { + return new SpanEnumerable(array); + } + + /// + /// Tokenizes the values in the input array instance using a specified separator. + /// This extension should be used directly within a loop: + /// + /// char[] text = "Hello, world!".ToCharArray(); + /// + /// foreach (var token in text.Tokenize(',')) + /// { + /// // Access the tokens here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items in the array to tokenize. + /// The source array to tokenize. + /// The separator item to use. + /// A wrapper type that will handle the tokenization for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanTokenizer Tokenize(this T[] array, T separator) + where T : IEquatable + { + return new SpanTokenizer(array, separator); + } + + /// + /// Gets a content hash from the input array instance using the Djb2 algorithm. + /// + /// The type of items in the input array instance. + /// The input array instance. + /// The Djb2 value for the input 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 + { + return ReadOnlySpanExtensions.GetDjb2HashCode(array); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolExtensions.cs new file mode 100644 index 00000000000..b5ef49b007a --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolExtensions.cs @@ -0,0 +1,58 @@ +// 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; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class ArrayPoolExtensions + { + /// + /// Changes the number of elements of a rented one-dimensional array to the specified new size. + /// + /// The type of items into the target array to resize. + /// The target instance to use to resize the array. + /// The rented array to resize, or to create a new array. + /// The size of the new array. + /// Indicates whether the contents of the array should be cleared before reuse. + /// Thrown when is less than 0. + /// When this method returns, the caller must not use any references to the old array anymore. + public static void Resize(this ArrayPool pool, ref T[]? array, int newSize, bool clearArray = false) + { + // If the old array is null, just create a new one with the requested size + if (array is null) + { + array = pool.Rent(newSize); + + return; + } + + // If the new size is the same as the current size, do nothing + if (array.Length == newSize) + { + return; + } + + /* Rent a new array with the specified size, and copy as many items from the current array + * as possible to the new array. This mirrors the behavior of the Array.Resize API from + * the BCL: if the new size is greater than the length of the current array, copy all the + * items from the original array into the new one. Otherwise, copy as many items as possible, + * until the new array is completely filled, and ignore the remaining items in the first array. */ + T[] newArray = pool.Rent(newSize); + int itemsToCopy = Math.Min(array.Length, newSize); + + array.AsSpan(0, itemsToCopy).CopyTo(newArray); + + pool.Return(array, clearArray); + + array = newArray; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs new file mode 100644 index 00000000000..89fc34499b0 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs @@ -0,0 +1,28 @@ +// 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.Contracts; +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class BoolExtensions + { + /// + /// Converts the given value into an . + /// + /// The input value to convert. + /// 1 if is , 0 otherwise. + /// This method does not contain branching instructions. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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 new file mode 100644 index 00000000000..33b26d509c7 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/HashCodeExtensions.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Toolkit.HighPerformance.Helpers; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class HashCodeExtensions + { + /// + /// Adds a sequence of values to the hash code. + /// + /// The type of elements in the input instance. + /// The input instance. + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref this HashCode hashCode, ReadOnlySpan span) +#if NETSTANDARD2_1 + where T : notnull +#else + // Same type constraints as HashCode, see comments there + where T : unmanaged +#endif + { + int hash = HashCode.CombineValues(span); + + hashCode.Add(hash); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IMemoryOwnerExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IMemoryOwnerExtensions.cs new file mode 100644 index 00000000000..a984e72aedb --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IMemoryOwnerExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Diagnostics.Contracts; +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Streams; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class IMemoryOwnerExtensions + { + /// + /// Returns a wrapping the contents of the given of instance. + /// + /// The input of instance. + /// A wrapping the data within . + /// + /// The caller does not need to track the lifetime of the input of + /// instance, as the returned will take care of disposing that buffer when it is closed. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this IMemoryOwner memory) + { + return new IMemoryOwnerStream(memory); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs new file mode 100644 index 00000000000..2781507dc81 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -0,0 +1,38 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class MemoryExtensions + { + /// + /// Returns a wrapping the contents of the given of instance. + /// + /// The input of instance. + /// A wrapping the data within . + /// + /// Since this method only receives a instance, which does not track + /// the lifetime of its underlying buffer, it is responsability of the caller to manage that. + /// In particular, the caller must ensure that the target buffer is not disposed as long + /// as the returned is in use, to avoid unexpected issues. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this Memory memory) + { + return new MemoryStream(memory); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ObjectExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ObjectExtensions.cs new file mode 100644 index 00000000000..2bb1c8d0014 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ObjectExtensions.cs @@ -0,0 +1,78 @@ +// 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.Contracts; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with instances. + /// + public static class ObjectExtensions + { + /// + /// Tries to get a boxed value from an input instance. + /// + /// The type of value to try to unbox. + /// The input instance to check. + /// The resulting value, if was in fact a boxed value. + /// if a value was retrieved correctly, otherwise. + /// + /// This extension behaves just like the following method: + /// + /// public static bool TryUnbox<T>(this object obj, out T value) + /// { + /// if (obj is T) + /// { + /// value = (T)obj; + /// + /// return true; + /// } + /// + /// value = default; + /// + /// return false; + /// } + /// + /// But in a more efficient way, and with the ability to also assign the unboxed value + /// directly on an existing T variable, which is not possible with the code above. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryUnbox(this object obj, out T value) + where T : struct + { + if (obj.GetType() == typeof(T)) + { + value = Unsafe.Unbox(obj); + + return true; + } + + value = default; + + return false; + } + + /// + /// Unboxes a value from an input instance. + /// + /// The type of value to unbox. + /// The input instance, representing a boxed value. + /// The value boxed in . + /// + /// This method doesn't check the actual type of , so it is responsability of the caller + /// to ensure it actually represents a boxed value and not some other instance. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T DangerousUnbox(this object obj) + where T : struct + { + return ref Unsafe.Unbox(obj); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs new file mode 100644 index 00000000000..bf5a50c9656 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -0,0 +1,38 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class ReadOnlyMemoryExtensions + { + /// + /// Returns a wrapping the contents of the given of instance. + /// + /// The input of instance. + /// A wrapping the data within . + /// + /// Since this method only receives a instance, which does not track + /// the lifetime of its underlying buffer, it is responsability of the caller to manage that. + /// In particular, the caller must ensure that the target buffer is not disposed as long + /// as the returned is in use, to avoid unexpected issues. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this ReadOnlyMemory memory) + { + return new MemoryStream(memory); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.Count.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.Count.cs new file mode 100644 index 00000000000..d470e456457 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.Count.cs @@ -0,0 +1,252 @@ +// 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.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static partial class ReadOnlySpanExtensions + { + /// + /// Counts the number of occurrences of a given value into a target instance. + /// + /// The type of items in the input instance. + /// The input instance to read. + /// The value to look for. + /// The number of occurrences of in . + [Pure] + [MethodImpl(MethodImplOptions.NoInlining)] + public static int Count(this ReadOnlySpan span, T value) + where T : IEquatable + { + // Special vectorized version when using a supported type + if (typeof(T) == typeof(byte) || + typeof(T) == typeof(sbyte) || + typeof(T) == typeof(bool)) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref sbyte r1 = ref Unsafe.As(ref r0); + int length = span.Length; + sbyte target = Unsafe.As(ref value); + + return Count(ref r1, length, target, sbyte.MaxValue); + } + + if (typeof(T) == typeof(char) || + typeof(T) == typeof(ushort) || + typeof(T) == typeof(short)) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref short r1 = ref Unsafe.As(ref r0); + int length = span.Length; + short target = Unsafe.As(ref value); + + return Count(ref r1, length, target, short.MaxValue); + } + + if (typeof(T) == typeof(int) || + typeof(T) == typeof(uint)) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref int r1 = ref Unsafe.As(ref r0); + int length = span.Length; + int target = Unsafe.As(ref value); + + return Count(ref r1, length, target, int.MaxValue); + } + + if (typeof(T) == typeof(long) || + typeof(T) == typeof(ulong)) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref long r1 = ref Unsafe.As(ref r0); + int length = span.Length; + long target = Unsafe.As(ref value); + + return Count(ref r1, length, target, int.MaxValue); + } + + return Count(ref MemoryMarshal.GetReference(span), span.Length, value); + } + + /// + /// Counts the number of occurrences of a given value into a target search space. + /// + /// A reference to the start of the search space. + /// The number of items in the search space. + /// The value to look for. + /// The type of value to look for. + /// The number of occurrences of in the search space + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Count(ref T r0, int length, T value) + where T : IEquatable + { + int + i = 0, + result = 0, + end8 = length - 8; + + // Main loop with 8 unrolled iterations + for (; i <= end8; i += 8) + { + result += Unsafe.Add(ref r0, i + 0).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 1).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 2).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 3).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 4).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 5).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 6).Equals(value).ToInt(); + result += Unsafe.Add(ref r0, i + 7).Equals(value).ToInt(); + } + + // Iterate over the remaining values and count those that match + for (; i < length; i++) + { + result += Unsafe.Add(ref r0, i).Equals(value).ToInt(); + } + + return result; + } + + /// + /// Counts the number of occurrences of a given value into a target search space. + /// + /// A reference to the start of the search space. + /// The number of items in the search space. + /// The value to look for. + /// The maximum amount that a value can reach. + /// The type of value to look for. + /// The number of occurrences of in the search space + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Count(ref T r0, int length, T value, int max) + where T : unmanaged, IEquatable + { + int i = 0, result = 0; + + // Only run the SIMD-enabled branch if the Vector APIs are hardware accelerated + if (Vector.IsHardwareAccelerated) + { + int end = length - Vector.Count; + + var partials = Vector.Zero; + var vc = new Vector(value); + + /* Run the fast path if the input span is short enough. + * There are two branches here because if the search space is large + * enough, the partial results could overflow if the target value + * is present too many times. This upper limit depends on the type + * being used, as the smaller the type is, the shorter the supported + * fast range will be. In the worst case scenario, the same value appears + * always at the offset aligned with the same SIMD value in the current + * register. Therefore, if the input span is longer than that minimum + * threshold, additional checks need to be performed to avoid overflows. + * This value is equal to the maximum (signed) numerical value for the current + * type, divided by the number of value that can fit in a register, minus 1. + * This is because the partial results are accumulated with a dot product, + * which sums them horizontally while still working on the original type. + * Dividing the max value by their count ensures that overflows can't happen. + * The check is moved outside of the loop to enable a branchless version + * of this method if the input span is guaranteed not to cause overflows. + * Otherwise, the safe but slower variant is used. */ + int threshold = (max / Vector.Count) - 1; + + if (length <= threshold) + { + for (; i <= end; i += Vector.Count) + { + ref T ri = ref Unsafe.Add(ref r0, i); + + /* Load the current Vector register, and then use + * Vector.Equals to check for matches. This API sets the + * values corresponding to matching pairs to all 1s. + * Since the input type is guaranteed to always be signed, + * this means that a value with all 1s represents -1, as + * signed numbers are represented in two's complement. + * So we can subtract this intermediate value to the + * partial results, which effectively sums 1 for each match. */ + var vi = Unsafe.As>(ref ri); + var ve = Vector.Equals(vi, vc); + + partials -= ve; + } + } + else + { + for (int j = 0; i <= end; i += Vector.Count, j++) + { + ref T ri = ref Unsafe.Add(ref r0, i); + + // Same as before + var vi = Unsafe.As>(ref ri); + var ve = Vector.Equals(vi, vc); + + partials -= ve; + + // Additional checks to avoid overflows + if (j == threshold) + { + j = 0; + result += CastToInt(Vector.Dot(partials, Vector.One)); + partials = Vector.Zero; + } + } + } + + // Compute the horizontal sum of the partial results + result += CastToInt(Vector.Dot(partials, Vector.One)); + } + + // Iterate over the remaining values and count those that match + for (; i < length; i++) + { + result += Unsafe.Add(ref r0, i).Equals(value).ToInt(); + } + + return result; + } + + /// + /// Casts a value of a given type to . + /// + /// The input type to cast. + /// The input value to cast to . + /// The cast of . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CastToInt(T value) + where T : unmanaged + { + if (typeof(T) == typeof(sbyte)) + { + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(short)) + { + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(int)) + { + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(long)) + { + return (int)Unsafe.As(ref value); + } + + throw new ArgumentException($"Invalid input type {typeof(T)}", nameof(value)); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs new file mode 100644 index 00000000000..c3fffe8cfa5 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static partial class ReadOnlySpanExtensions + { + /// + /// Returns a reference to the first element within a given , with no bounds checks. + /// + /// The type of elements in the input instance. + /// The input instance. + /// A reference to the first element within . + /// 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 ReadOnlySpan span) + { + return ref MemoryMarshal.GetReference(span); + } + + /// + /// 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, int i) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref T ri = ref Unsafe.Add(ref r0, i); + + return ref ri; + } + + /// + /// Casts a of one primitive type to of bytes. + /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// The type if items in the source + /// The source slice, of type . + /// A of bytes. + /// + /// Thrown when contains pointers. + /// + /// + /// Thrown if the property of the new would exceed . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan AsBytes(this ReadOnlySpan span) + where T : unmanaged + { + return MemoryMarshal.AsBytes(span); + } + + /// + /// Casts a of one primitive type to another primitive type . + /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// The type of items in the source . + /// The type of items in the destination . + /// The source slice, of type . + /// A of type + /// + /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. + /// + /// + /// Thrown when or contains pointers. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan Cast(this ReadOnlySpan span) + where TFrom : struct + where TTo : struct + { + return MemoryMarshal.Cast(span); + } + + /// + /// Enumerates the items in the input instance, as pairs of value/index values. + /// This extension should be used directly within a loop: + /// + /// ReadOnlySpan<string> words = new[] { "Hello", ", ", "world", "!" }; + /// + /// foreach (var item in words.Enumerate()) + /// { + /// // Access the index and value of each item here... + /// int index = item.Index; + /// string value = item.Value; + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items to enumerate. + /// The source to enumerate. + /// A wrapper type that will handle the value/index enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpanEnumerable Enumerate(this ReadOnlySpan span) + { + return new ReadOnlySpanEnumerable(span); + } + + /// + /// Tokenizes the values in the input instance using a specified separator. + /// This extension should be used directly within a loop: + /// + /// ReadOnlySpan<char> text = "Hello, world!"; + /// + /// foreach (var token in text.Tokenize(',')) + /// { + /// // Access the tokens here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items in the to tokenize. + /// The source to tokenize. + /// The separator item to use. + /// A wrapper type that will handle the tokenization for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpanTokenizer Tokenize(this ReadOnlySpan span, T separator) + where T : IEquatable + { + return new ReadOnlySpanTokenizer(span, separator); + } + + /// + /// Gets a content hash from the input instance using the Djb2 algorithm. + /// + /// The type of items in the input instance. + /// The input instance. + /// The Djb2 value for the input instance. + /// The Djb2 hash is fully deterministic and with no random components. + [Pure] + public static int GetDjb2HashCode(this ReadOnlySpan span) + where T : notnull + { + ref T r0 = ref MemoryMarshal.GetReference(span); + int + hash = 5381, + length = span.Length, + i = 0, + end8 = length - 8; + + // Main loop with 8 unrolled iterations + for (; i <= end8; i += 8) + { + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 0).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 1).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 2).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 3).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 4).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 5).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 6).GetHashCode()); + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i + 7).GetHashCode()); + } + + // Handle the leftover items + for (; i < length; i++) + { + hash = unchecked((hash << 5) + hash + Unsafe.Add(ref r0, i).GetHashCode()); + } + + return hash; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs new file mode 100644 index 00000000000..5710c08e783 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class SpanExtensions + { + /// + /// Returns a reference to the first element within a given , with no bounds checks. + /// + /// The type of elements in the input instance. + /// The input instance. + /// A reference to the first element within . + /// 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 Span span) + { + return ref MemoryMarshal.GetReference(span); + } + + /// + /// 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, int i) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + ref T ri = ref Unsafe.Add(ref r0, i); + + return ref ri; + } + + /// + /// Casts a of one primitive type to of bytes. + /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// The type if items in the source + /// The source slice, of type . + /// A of bytes. + /// + /// Thrown when contains pointers. + /// + /// + /// Thrown if the property of the new would exceed . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsBytes(this Span span) + where T : unmanaged + { + return MemoryMarshal.AsBytes(span); + } + + /// + /// Casts a of one primitive type to another primitive type . + /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// The type of items in the source . + /// The type of items in the destination . + /// The source slice, of type . + /// A of type + /// + /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. + /// + /// + /// Thrown when or contains pointers. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span Cast(this Span span) + where TFrom : struct + where TTo : struct + { + return MemoryMarshal.Cast(span); + } + + /// + /// Counts the number of occurrences of a given value into a target instance. + /// + /// The type of items in the input instance. + /// The input instance to read. + /// The value to look for. + /// The number of occurrences of in . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Count(this Span span, T value) + where T : IEquatable + { + return ReadOnlySpanExtensions.Count(span, value); + } + + /// + /// Enumerates the items in the input instance, as pairs of reference/index values. + /// This extension should be used directly within a loop: + /// + /// Span<int> numbers = new[] { 1, 2, 3, 4, 5, 6, 7 }; + /// + /// foreach (var item in numbers.Enumerate()) + /// { + /// // Access the index and value of each item here... + /// int index = item.Index; + /// ref int value = ref item.Value; + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items to enumerate. + /// The source to enumerate. + /// A wrapper type that will handle the reference/index enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanEnumerable Enumerate(this Span span) + { + return new SpanEnumerable(span); + } + + /// + /// Tokenizes the values in the input instance using a specified separator. + /// This extension should be used directly within a loop: + /// + /// Span<char> text = "Hello, world!".ToCharArray(); + /// + /// foreach (var token in text.Tokenize(',')) + /// { + /// // Access the tokens here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of items in the to tokenize. + /// The source to tokenize. + /// The separator item to use. + /// A wrapper type that will handle the tokenization for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SpanTokenizer Tokenize(this Span span, T separator) + where T : IEquatable + { + return new SpanTokenizer(span, separator); + } + + /// + /// Gets a content hash from the input instance using the Djb2 algorithm. + /// + /// The type of items in the input instance. + /// The input instance. + /// The Djb2 value for the input instance. + /// The Djb2 hash is fully deterministic and with no random components. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetDjb2HashCode(this Span span) + where T : notnull + { + return ReadOnlySpanExtensions.GetDjb2HashCode(span); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpinLockExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpinLockExtensions.cs new file mode 100644 index 00000000000..e2d52b61186 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpinLockExtensions.cs @@ -0,0 +1,191 @@ +// 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.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class SpinLockExtensions + { + /// + /// Enters a specified instance and returns a wrapper to use to release the lock. + /// This extension should be used though a block or statement: + /// + /// SpinLock spinLock = new SpinLock(); + /// + /// using (SpinLockExtensions.Enter(&spinLock)) + /// { + /// // Thread-safe code here... + /// } + /// + /// The compiler will take care of releasing the SpinLock when the code goes out of that scope. + /// + /// A pointer to the target to use + /// A wrapper type that will release when its method is called. + /// The returned value shouldn't be used directly: use this extension in a block or statement. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe UnsafeLock Enter(SpinLock* spinLock) + { + return new UnsafeLock(spinLock); + } + + /// + /// A that is used to enter and hold a through a block or statement. + /// + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe ref struct UnsafeLock + { + /// + /// The * pointer to the target value to use. + /// + private readonly SpinLock* spinLock; + + /// + /// A value indicating whether or not the lock is taken by this instance. + /// + private readonly bool lockTaken; + + /// + /// Initializes a new instance of the struct. + /// + /// The target to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UnsafeLock(SpinLock* spinLock) + { + this.spinLock = spinLock; + this.lockTaken = false; + + spinLock->Enter(ref this.lockTaken); + } + + /// + /// Implements the duck-typed method and releases the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + if (this.lockTaken) + { + this.spinLock->Exit(); + } + } + } + +#if NETSTANDARD2_1 + /// + /// Enters a specified instance and returns a wrapper to use to release the lock. + /// This extension should be used though a block or statement: + /// + /// SpinLock spinLock = new SpinLock(); + /// + /// using (spinLock.Enter()) + /// { + /// // Thread-safe code here... + /// } + /// + /// The compiler will take care of releasing the SpinLock when the code goes out of that scope. + /// + /// The target to use + /// A wrapper type that will release when its method is called. + /// The returned value shouldn't be used directly: use this extension in a block or statement. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Lock Enter(ref this SpinLock spinLock) + { + return new Lock(ref spinLock); + } +#else + /// + /// Enters a specified instance and returns a wrapper to use to release the lock. + /// This extension should be used though a block or statement: + /// + /// private SpinLock spinLock = new SpinLock(); + /// + /// public void Foo() + /// { + /// using (SpinLockExtensions.Enter(this, ref spinLock)) + /// { + /// // Thread-safe code here... + /// } + /// } + /// + /// The compiler will take care of releasing the SpinLock when the code goes out of that scope. + /// + /// The owner to create a portable reference for. + /// The target to use (it must be within ). + /// A wrapper type that will release when its method is called. + /// The returned value shouldn't be used directly: use this extension in a block or statement. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Lock Enter(object owner, ref SpinLock spinLock) + { + return new Lock(owner, ref spinLock); + } +#endif + + /// + /// A that is used to enter and hold a through a block or statement. + /// + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + [EditorBrowsable(EditorBrowsableState.Never)] + public ref struct Lock + { + /// + /// The instance pointing to the target value to use. + /// + private readonly Ref spinLock; + + /// + /// A value indicating whether or not the lock is taken by this instance. + /// + private readonly bool lockTaken; + +#if NETSTANDARD2_1 + /// + /// Initializes a new instance of the struct. + /// + /// The target to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Lock(ref SpinLock spinLock) + { + this.spinLock = new Ref(ref spinLock); + this.lockTaken = false; + + spinLock.Enter(ref this.lockTaken); + } +#else + /// + /// Initializes a new instance of the struct. + /// + /// The owner to create a portable reference for. + /// The target to use (it must be within ). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Lock(object owner, ref SpinLock spinLock) + { + this.spinLock = new Ref(owner, ref spinLock); + this.lockTaken = false; + + spinLock.Enter(ref this.lockTaken); + } +#endif + + /// + /// Implements the duck-typed method and releases the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + if (this.lockTaken) + { + this.spinLock.Value.Exit(); + } + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs new file mode 100644 index 00000000000..a695bd5f4c1 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/StringExtensions.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Enumerables; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class StringExtensions + { + /// + /// Returns a reference to the first element within a given , with no bounds checks. + /// + /// The input 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 char DangerousGetReference(this string text) + { + var stringData = Unsafe.As(text); + + return ref stringData.Data; + } + + /// + /// Returns a reference to an element at a specified index within a given , with no bounds checks. + /// + /// 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 char DangerousGetReferenceAt(this string text, int i) + { + var stringData = Unsafe.As(text); + ref var ri = ref Unsafe.Add(ref stringData.Data, i); + + return ref ri; + } + + // Description adapted from CoreCLR: see https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,285. + // CLR strings are laid out in memory as follows: + // [ sync block || pMethodTable || length || string data .. ] + // ^ ^ + // | \-- ref Unsafe.As(text).Data + // \-- string + // The reference to RawStringData.Data points to the first character in the + // string, skipping over the sync block, method table and string length. + [StructLayout(LayoutKind.Explicit)] + private sealed class RawStringData + { +#pragma warning disable CS0649 // Unassigned fields +#pragma warning disable SA1401 // Fields should be private + [FieldOffset(4)] + public char Data; +#pragma warning restore CS0649 +#pragma warning restore SA1401 + } + + /// + /// Counts the number of occurrences of a given character into a target instance. + /// + /// The input instance to read. + /// The character to look for. + /// The number of occurrences of in . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Count(this string text, char c) + { + return text.AsSpan().Count(c); + } + + /// + /// Enumerates the items in the input instance, as pairs of value/index values. + /// This extension should be used directly within a loop: + /// + /// string text = "Hello, world!"; + /// + /// foreach (var item in text.Enumerate()) + /// { + /// // Access the index and value of each item here... + /// int index = item.Index; + /// string value = item.Value; + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The source to enumerate. + /// A wrapper type that will handle the value/index enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpanEnumerable Enumerate(this string text) + { + return new ReadOnlySpanEnumerable(text.AsSpan()); + } + + /// + /// Tokenizes the values in the input instance using a specified separator. + /// This extension should be used directly within a loop: + /// + /// string text = "Hello, world!"; + /// + /// foreach (var token in text.Tokenize(',')) + /// { + /// // Access the tokens here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The source to tokenize. + /// The separator character to use. + /// A wrapper type that will handle the tokenization for . + /// The returned value shouldn't be used directly: use this extension in a loop. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpanTokenizer Tokenize(this string text, char separator) + { + return new ReadOnlySpanTokenizer(text.AsSpan(), separator); + } + + /// + /// Gets a content hash from the input instance using the Djb2 algorithm. + /// + /// The source to enumerate. + /// The Djb2 value for the input instance. + /// The Djb2 hash is fully deterministic and with no random components. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetDjb2HashCode(this string text) + { + return text.AsSpan().GetDjb2HashCode(); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs b/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs new file mode 100644 index 00000000000..24fb97f8e58 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs @@ -0,0 +1,170 @@ +// 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.Contracts; +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to perform bit operations on numeric types. + /// + public static class BitHelper + { + /// + /// Checks whether or not a given bit is set. + /// + /// The input value. + /// The position of the bit to check. + /// Whether or not the n-th bit is set. + /// + /// This method doesn't validate against the valid range. + /// If the parameter is not valid, the result will just be inconsistent. + /// Additionally, no conditional branches are used to retrieve the flag. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlag(uint value, int n) + { + // Read the n-th bit, downcast to byte + byte flag = (byte)((value >> n) & 1); + + /* Reinterpret the byte to avoid the test, setnz and + * movzx instructions (asm x64). This is because the JIT + * compiler is able to optimize this reinterpret-cast as + * a single "and eax, 0x1" instruction, whereas if we had + * compared the previous computed flag against 0, the assembly + * would have had to perform the test, set the non-zero + * flag and then extend the (byte) result to eax. */ + return Unsafe.As(ref flag); + } + + /// + /// Sets a bit to a specified value. + /// + /// The target value. + /// The position of the bit to set or clear. + /// The value to assign to the target bit. + /// + /// Just like , this method doesn't validate + /// and does not contain branching instructions, so it's well suited for use in tight loops as well. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetFlag(ref uint value, int n, bool flag) + { + value = SetFlag(value, n, flag); + } + + /// + /// Sets a bit to a specified value. + /// + /// The input value. + /// The position of the bit to set or clear. + /// The value to assign to the target bit. + /// An value equal to except for the -th bit. + /// + /// Just like , this method doesn't validate + /// and does not contain branching instructions, so it's well suited for use in tight loops as well. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint SetFlag(uint value, int n, bool flag) + { + /* Shift a bit left to the n-th position, negate the + * resulting value and perform an AND with the input value. + * This effectively clears the n-th bit of our input. */ + uint + bit = 1u << n, + not = ~bit, + and = value & not; + + /* Reinterpret the flag as 1 or 0, and cast to uint. + * The flag is first copied to a local variable as taking + * the address of an argument is slower than doing the same + * for a local variable. This is because when referencing + * the argument the JIT compiler will emit code to temporarily + * move the argument to the stack, and then copy it back. + * With a temporary variable instead, the JIT will be able to + * optimize the method to only use CPU registers. */ + bool localFlag = flag; + uint + flag32 = Unsafe.As(ref localFlag), + + /* Finally, we left shift the uint flag to the right position + * and perform an OR with the resulting value of the previous + * operation. This will always guaranteed to work, thanks to the + * initial code clearing that bit before setting it again. */ + shift = flag32 << n, + or = and | shift; + + return or; + } + + /// + /// Checks whether or not a given bit is set. + /// + /// The input value. + /// The position of the bit to check. + /// Whether or not the n-th bit is set. + /// + /// This method doesn't validate against the valid range. + /// If the parameter is not valid, the result will just be inconsistent. + /// Additionally, no conditional branches are used to retrieve the flag. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlag(ulong value, int n) + { + // Same logic as the uint version, see that for more info + byte flag = (byte)((value >> n) & 1); + + return Unsafe.As(ref flag); + } + + /// + /// Sets a bit to a specified value. + /// + /// The target value. + /// The position of the bit to set or clear. + /// The value to assign to the target bit. + /// + /// Just like , this method doesn't validate + /// and does not contain branching instructions, so it's well suited for use in tight loops as well. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetFlag(ref ulong value, int n, bool flag) + { + value = SetFlag(value, n, flag); + } + + /// + /// Sets a bit to a specified value. + /// + /// The input value. + /// The position of the bit to set or clear. + /// The value to assign to the target bit. + /// An value equal to except for the -th bit. + /// + /// Just like , this method doesn't validate + /// and does not contain branching instructions, so it's well suited for use in tight loops as well. + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong SetFlag(ulong value, int n, bool flag) + { + // As with the method above, reuse the same logic as the uint version + ulong + bit = 1ul << n, + not = ~bit, + and = value & not; + bool localFlag = flag; + ulong + flag64 = Unsafe.As(ref localFlag), + shift = flag64 << n, + or = and | shift; + + return or; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs new file mode 100644 index 00000000000..c5419ef0a15 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/HashCode{T}.cs @@ -0,0 +1,412 @@ +// 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.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Combines the hash code of sequences of values into a single hash code. + /// + /// The type of values to hash. + public struct HashCode +#if NETSTANDARD2_1 + where T : notnull +#else + /* .NET Standard 2.0 doesn't have the API to check at runtime whether a + * type satisfies the unmanaged constraint, se we enforce that at compile + * time and only expose the APIs of this class in that case. */ + where T : unmanaged +#endif + { + /// + /// Gets a content hash from the input instance using the xxHash32 algorithm. + /// + /// The input instance + /// The xxHash32 value for the input instance + /// The xxHash32 is only guaranteed to be deterministic within the scope of a single app execution + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Combine(ReadOnlySpan span) + { + int hash = CombineValues(span); + + return HashCode.Combine(hash); + } + + /// + /// Gets a content hash from the input instance. + /// + /// The input instance + /// The hash code for the input instance + /// The returned hash code is not processed through APIs. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515", Justification = "Compiler directive instead of whitespace")] + internal static int CombineValues(ReadOnlySpan span) + { + ref T r0 = ref MemoryMarshal.GetReference(span); + +#if NETSTANDARD2_1 + /* 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 CombineValues(ref r0, span.Length); + } +#endif + // Get the info for the target memory area to process + ref byte rb = ref Unsafe.As(ref r0); + long byteSize = (long)span.Length * Unsafe.SizeOf(); + + // Fast path if the source memory area is not large enough + if (byteSize < BytesProcessor.MinimumSuggestedSize) + { + return CombineValues(ref r0, span.Length); + } + + // Use the fast vectorized overload if the input span can be reinterpreted as a sequence of bytes + return byteSize <= int.MaxValue + ? BytesProcessor.CombineBytes(ref rb, unchecked((int)byteSize)) + : BytesProcessor.CombineBytes(ref rb, byteSize); + } + + /// + /// Gets a content hash from the input memory area. + /// + /// A reference to the start of the memory area. + /// The size of the memory area. + /// The hash code for the input values. + [Pure] + [MethodImpl(MethodImplOptions.NoInlining)] + private static int CombineValues(ref T r0, int length) + { + int + hash = 0, i = 0, + end8 = length - 8; + + // Main loop with 8 unrolled iterations + for (; i <= end8; i += 8) + { + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 0).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 1).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 2).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 3).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 4).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 5).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 6).GetHashCode()); + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i + 7).GetHashCode()); + } + + // Handle the leftover items + for (; i < length; i++) + { + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i).GetHashCode()); + } + + return hash; + } + } + + /// + /// Combines the hash code of sequences of values into a single hash code. + /// + /// + /// This type is just a wrapper for the method, + /// which is not defined within for performance reasons. + /// Because is a generic type, each contained method will be JIT + /// compiled into a different executable, even if it's not directly using the generic type + /// parameters of the declaring type at all. Moving this method into a separate, non-generic + /// type allows the runtime to always reuse a single JIT compilation. + /// + internal static class BytesProcessor + { + /// + /// The minimum suggested size for memory areas to process using the APIs in this class + /// + public const int MinimumSuggestedSize = 512; + + /// + /// Gets a content hash from a given memory area. + /// + /// A reference to the start of the memory area. + /// The size in bytes of the memory area. + /// The hash code for the contents of the source memory area. + [Pure] + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CombineBytes(ref byte r0, long length) + { + /* This method takes a long as the length parameter, which is needed in case + * the target area is a large sequence of items with a byte size greater than + * one. To maximize efficiency, the target memory area is divided into + * contiguous blocks as large as possible, and the SIMD implementation + * is executed on all of them one by one. The partial results + * are accumulated with the usual hash function. */ + int + hash = 0, + runs = unchecked((int)(length / int.MaxValue)), + trailing = unchecked((int)(length - (runs * (long)int.MaxValue))); + + // Process chunks of int.MaxValue consecutive bytes + for (int i = 0; i < runs; i++) + { + int partial = CombineBytes(ref r0, int.MaxValue); + + hash = unchecked((hash * 397) ^ partial); + + r0 = ref Unsafe.Add(ref r0, int.MaxValue); + } + + // Process the leftover elements + int tail = CombineBytes(ref r0, trailing); + + hash = unchecked((hash * 397) ^ tail); + + return hash; + } + + /// + /// Gets a content hash from a given memory area. + /// + /// A reference to the start of the memory area. + /// The size in bytes of the memory area. + /// The hash code for the contents of the source memory area. + [Pure] + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CombineBytes(ref byte r0, int length) + { + int hash = 0, i = 0; + + // Dedicated SIMD branch, if available + if (Vector.IsHardwareAccelerated) + { + /* Test whether the total number of bytes is at least + * equal to the number that can fit in a single SIMD register. + * If that is not the case, skip the entire SIMD branch, which + * also saves the unnecessary computation of partial hash + * values from the accumulation register, and the loading + * of the prime constant in the secondary SIMD register. */ + if (length >= 8 * Vector.Count * sizeof(int)) + { + var vh = Vector.Zero; + var v397 = new Vector(397); + + /* First upper bound for the vectorized path. + * The first loop has sequences of 8 SIMD operations unrolled, + * so assuming that a SIMD register can hold 8 int values at a time, + * it processes 8 * Vector.Count * sizeof(int), which results in + * 128 bytes on SSE registers, 256 on AVX2 and 512 on AVX512 registers. */ + var end256 = length - (8 * Vector.Count * sizeof(int)); + + for (; i <= end256; i += 8 * Vector.Count * sizeof(int)) + { + ref byte ri0 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 0) + i); + var vi0 = Unsafe.ReadUnaligned>(ref ri0); + var vp0 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp0, vi0); + + ref byte ri1 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 1) + i); + var vi1 = Unsafe.ReadUnaligned>(ref ri1); + var vp1 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp1, vi1); + + ref byte ri2 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 2) + i); + var vi2 = Unsafe.ReadUnaligned>(ref ri2); + var vp2 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp2, vi2); + + ref byte ri3 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 3) + i); + var vi3 = Unsafe.ReadUnaligned>(ref ri3); + var vp3 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp3, vi3); + + ref byte ri4 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 4) + i); + var vi4 = Unsafe.ReadUnaligned>(ref ri4); + var vp4 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp4, vi4); + + ref byte ri5 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 5) + i); + var vi5 = Unsafe.ReadUnaligned>(ref ri5); + var vp5 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp5, vi5); + + ref byte ri6 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 6) + i); + var vi6 = Unsafe.ReadUnaligned>(ref ri6); + var vp6 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp6, vi6); + + ref byte ri7 = ref Unsafe.Add(ref r0, (Vector.Count * sizeof(int) * 7) + i); + var vi7 = Unsafe.ReadUnaligned>(ref ri7); + var vp7 = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp7, vi7); + } + + /* Second upper bound for the vectorized path. + * Each iteration processes 16 bytes on SSE, 32 bytes on AVX2 + * and 64 on AVX512 registers. When this point is reached, + * it means that there are at most 127 bytes remaining on SSE, + * or 255 on AVX2, or 511 on AVX512 systems.*/ + var end32 = length - i - (Vector.Count * sizeof(int)); + + for (; i <= end32; i += Vector.Count * sizeof(int)) + { + ref byte ri = ref Unsafe.Add(ref r0, i); + var vi = Unsafe.ReadUnaligned>(ref ri); + var vp = Vector.Multiply(vh, v397); + vh = Vector.Xor(vp, vi); + } + + /* Combine the partial hash values in each position. + * The loop below is automatically unrolled by the JIT. */ + for (var j = 0; j < Vector.Count; j++) + { + hash = unchecked((hash * 397) ^ vh[j]); + } + } + + /* At this point, regardless of whether or not the previous + * branch was taken, there are at most 15 unprocessed bytes + * on SSE systems, 31 on AVX2 systems and 63 on AVX512 systems. */ + } + else + { + // Process groups of 64 bytes at a time + var end64 = length - (8 * sizeof(ulong)); + + for (; i <= end64; i += 8 * sizeof(ulong)) + { + ref byte ri0 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 0) + i); + var value0 = Unsafe.ReadUnaligned(ref ri0); + hash = unchecked((hash * 397) ^ (int)value0 ^ (int)(value0 >> 32)); + + ref byte ri1 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 1) + i); + var value1 = Unsafe.ReadUnaligned(ref ri1); + hash = unchecked((hash * 397) ^ (int)value1 ^ (int)(value1 >> 32)); + + ref byte ri2 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 2) + i); + var value2 = Unsafe.ReadUnaligned(ref ri2); + hash = unchecked((hash * 397) ^ (int)value2 ^ (int)(value2 >> 32)); + + ref byte ri3 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 3) + i); + var value3 = Unsafe.ReadUnaligned(ref ri3); + hash = unchecked((hash * 397) ^ (int)value3 ^ (int)(value3 >> 32)); + + ref byte ri4 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 4) + i); + var value4 = Unsafe.ReadUnaligned(ref ri4); + hash = unchecked((hash * 397) ^ (int)value4 ^ (int)(value4 >> 32)); + + ref byte ri5 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 5) + i); + var value5 = Unsafe.ReadUnaligned(ref ri5); + hash = unchecked((hash * 397) ^ (int)value5 ^ (int)(value5 >> 32)); + + ref byte ri6 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 6) + i); + var value6 = Unsafe.ReadUnaligned(ref ri6); + hash = unchecked((hash * 397) ^ (int)value6 ^ (int)(value6 >> 32)); + + ref byte ri7 = ref Unsafe.Add(ref r0, (sizeof(ulong) * 7) + i); + var value7 = Unsafe.ReadUnaligned(ref ri7); + hash = unchecked((hash * 397) ^ (int)value7 ^ (int)(value7 >> 32)); + } + + /* At this point, there are up to 63 bytes left. + * If there are at least 32, unroll that iteration with + * the same procedure as before, but as uint values. */ + if (length - i >= 8 * sizeof(uint)) + { + ref byte ri0 = ref Unsafe.Add(ref r0, (sizeof(uint) * 0) + i); + var value0 = Unsafe.ReadUnaligned(ref ri0); + hash = unchecked((hash * 397) ^ (int)value0); + + ref byte ri1 = ref Unsafe.Add(ref r0, (sizeof(uint) * 1) + i); + var value1 = Unsafe.ReadUnaligned(ref ri1); + hash = unchecked((hash * 397) ^ (int)value1); + + ref byte ri2 = ref Unsafe.Add(ref r0, (sizeof(uint) * 2) + i); + var value2 = Unsafe.ReadUnaligned(ref ri2); + hash = unchecked((hash * 397) ^ (int)value2); + + ref byte ri3 = ref Unsafe.Add(ref r0, (sizeof(uint) * 3) + i); + var value3 = Unsafe.ReadUnaligned(ref ri3); + hash = unchecked((hash * 397) ^ (int)value3); + + ref byte ri4 = ref Unsafe.Add(ref r0, (sizeof(uint) * 4) + i); + var value4 = Unsafe.ReadUnaligned(ref ri4); + hash = unchecked((hash * 397) ^ (int)value4); + + ref byte ri5 = ref Unsafe.Add(ref r0, (sizeof(uint) * 5) + i); + var value5 = Unsafe.ReadUnaligned(ref ri5); + hash = unchecked((hash * 397) ^ (int)value5); + + ref byte ri6 = ref Unsafe.Add(ref r0, (sizeof(uint) * 6) + i); + var value6 = Unsafe.ReadUnaligned(ref ri6); + hash = unchecked((hash * 397) ^ (int)value6); + + ref byte ri7 = ref Unsafe.Add(ref r0, (sizeof(uint) * 7) + i); + var value7 = Unsafe.ReadUnaligned(ref ri7); + hash = unchecked((hash * 397) ^ (int)value7); + } + + // The non-SIMD path leaves up to 31 unprocessed bytes + } + + /* At this point there might be up to 31 bytes left on both AVX2 systems, + * and on systems with no hardware accelerated SIMD registers. + * That number would go up to 63 on AVX512 systems, in which case it is + * still useful to perform this last loop unrolling. + * The only case where this branch is never taken is on SSE systems, + * but since those are not so common anyway the code is left here for simplicity. + * What follows is the same procedure as before, but with ushort values, + * so that if there are at least 16 bytes available, those + * will all be processed in a single unrolled iteration. */ + if (length - i >= 8 * sizeof(ushort)) + { + ref byte ri0 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 0) + i); + var value0 = Unsafe.ReadUnaligned(ref ri0); + hash = unchecked((hash * 397) ^ value0); + + ref byte ri1 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 1) + i); + var value1 = Unsafe.ReadUnaligned(ref ri1); + hash = unchecked((hash * 397) ^ value1); + + ref byte ri2 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 2) + i); + var value2 = Unsafe.ReadUnaligned(ref ri2); + hash = unchecked((hash * 397) ^ value2); + + ref byte ri3 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 3) + i); + var value3 = Unsafe.ReadUnaligned(ref ri3); + hash = unchecked((hash * 397) ^ value3); + + ref byte ri4 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 4) + i); + var value4 = Unsafe.ReadUnaligned(ref ri4); + hash = unchecked((hash * 397) ^ value4); + + ref byte ri5 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 5) + i); + var value5 = Unsafe.ReadUnaligned(ref ri5); + hash = unchecked((hash * 397) ^ value5); + + ref byte ri6 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 6) + i); + var value6 = Unsafe.ReadUnaligned(ref ri6); + hash = unchecked((hash * 397) ^ value6); + + ref byte ri7 = ref Unsafe.Add(ref r0, (sizeof(ushort) * 7) + i); + var value7 = Unsafe.ReadUnaligned(ref ri7); + hash = unchecked((hash * 397) ^ value7); + } + + // Handle the leftover items + for (; i < length; i++) + { + hash = unchecked((hash * 397) ^ Unsafe.Add(ref r0, i)); + } + + return hash; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs new file mode 100644 index 00000000000..e3c61cb2bd9 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs @@ -0,0 +1,250 @@ +// 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; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to work with parallel code in a highly optimized manner. + /// + public static partial class ParallelHelper + { +#if NETSTANDARD2_1 + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The iteration range. + /// None of the bounds of can start from an end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(Range range) + where TAction : struct, IAction + { + For(range, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The iteration range. + /// + /// 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. + /// + /// None of the bounds of can start from an end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(Range range, int minimumActionsPerThread) + where TAction : struct, IAction + { + For(range, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The iteration range. + /// The instance representing the action to invoke. + /// None of the bounds of can start from an end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(Range range, TAction action) + where TAction : struct, IAction + { + For(range, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The iteration range. + /// 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. + /// + /// None of the bounds of can start from an end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(Range range, TAction action, int minimumActionsPerThread) + where TAction : struct, IAction + { + if (range.Start.IsFromEnd || range.End.IsFromEnd) + { + ThrowArgumentExceptionForRangeIndexFromEnd(nameof(range)); + } + + int + start = range.Start.Value, + end = range.End.Value; + + For(start, end, action, minimumActionsPerThread); + } +#endif + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The starting iteration index. + /// The final iteration index (exclusive). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(int start, int end) + where TAction : struct, IAction + { + For(start, end, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The starting iteration index. + /// The final iteration index (exclusive). + /// + /// 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 For(int start, int end, int minimumActionsPerThread) + where TAction : struct, IAction + { + For(start, end, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The starting iteration index. + /// The final iteration index (exclusive). + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For(int start, int end, in TAction action) + where TAction : struct, IAction + { + For(start, end, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each iteration index. + /// The starting iteration index. + /// The final iteration index (exclusive). + /// 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 For(int start, int end, in TAction action, int minimumActionsPerThread) + where TAction : struct, IAction + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (start > end) + { + ThrowArgumentOutOfRangeExceptionForStartGreaterThanEnd(); + } + + if (start == end) + { + return; + } + + int + count = Math.Abs(start - end), + maxBatches = 1 + ((count - 1) / minimumActionsPerThread), + cores = Environment.ProcessorCount, + numBatches = Math.Min(maxBatches, cores); + + // Skip the parallel invocation when a single batch is needed + if (numBatches == 1) + { + for (int i = start; i < end; i++) + { + Unsafe.AsRef(action).Invoke(i); + } + + return; + } + + int batchSize = 1 + ((count - 1) / numBatches); + + var actionInvoker = new ActionInvoker(start, end, batchSize, action); + + // 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 ActionInvoker + where TAction : struct, IAction + { + private readonly int start; + private readonly int end; + private readonly int batchSize; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ActionInvoker( + int start, + int end, + int batchSize, + in TAction action) + { + this.start = start; + this.end = end; + this.batchSize = batchSize; + this.action = action; + } + + /// + /// 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 + offset = i * this.batchSize, + low = this.start + offset, + high = low + this.batchSize, + stop = Math.Min(high, this.end); + + for (int j = low; j < stop; j++) + { + Unsafe.AsRef(this.action).Invoke(j); + } + } + } + } + + /// + /// A contract for actions being executed with an input index. + /// + /// If the method is small enough, it is highly recommended to mark it with . + public interface IAction + { + /// + /// Executes the action associated with a specific index. + /// + /// The current index for the action to execute. + void Invoke(int i); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs new file mode 100644 index 00000000000..8f82c658887 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs @@ -0,0 +1,348 @@ +// 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.Drawing; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to work with parallel code in a highly optimized manner. + /// + public static partial class ParallelHelper + { +#if NETSTANDARD2_1 + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the iteration range for the outer loop. + /// The value indicating the iteration range for the inner loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Range i, Range j) + where TAction : struct, IAction2D + { + For2D(i, j, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the iteration range for the outer loop. + /// The value indicating the iteration range for the inner loop. + /// + /// 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 For2D(Range i, Range j, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + For2D(i, j, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the iteration range for the outer loop. + /// The value indicating the iteration range for the inner loop. + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Range i, Range j, in TAction action) + where TAction : struct, IAction2D + { + For2D(i, j, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the iteration range for the outer loop. + /// The value indicating the iteration range for the inner loop. + /// 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. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Range i, Range j, in TAction action, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + if (i.Start.IsFromEnd || i.End.IsFromEnd) + { + ThrowArgumentExceptionForRangeIndexFromEnd(nameof(i)); + } + + if (j.Start.IsFromEnd || j.End.IsFromEnd) + { + ThrowArgumentExceptionForRangeIndexFromEnd(nameof(j)); + } + + int + top = i.Start.Value, + bottom = i.End.Value, + left = j.Start.Value, + right = j.End.Value; + + For2D(top, bottom, left, right, action, minimumActionsPerThread); + } +#endif + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the 2D iteration area to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Rectangle area) + where TAction : struct, IAction2D + { + For2D(area.Top, area.Bottom, area.Left, area.Right, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the 2D iteration area to use. + /// + /// 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 For2D(Rectangle area, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + For2D(area.Top, area.Bottom, area.Left, area.Right, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the 2D iteration area to use. + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Rectangle area, in TAction action) + where TAction : struct, IAction2D + { + For2D(area.Top, area.Bottom, area.Left, area.Right, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The value indicating the 2D iteration area to use. + /// 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. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(Rectangle area, in TAction action, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + For2D(area.Top, area.Bottom, area.Left, area.Right, action, minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The starting iteration value for the outer loop. + /// The final iteration value for the outer loop (exclusive). + /// The starting iteration value for the inner loop. + /// The final iteration value for the inner loop (exclusive). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(int top, int bottom, int left, int right) + where TAction : struct, IAction2D + { + For2D(top, bottom, left, right, default(TAction), 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The starting iteration value for the outer loop. + /// The final iteration value for the outer loop (exclusive). + /// The starting iteration value for the inner loop. + /// The final iteration value for the inner loop (exclusive). + /// + /// 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 For2D(int top, int bottom, int left, int right, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + For2D(top, bottom, left, right, default(TAction), minimumActionsPerThread); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The starting iteration value for the outer loop. + /// The final iteration value for the outer loop (exclusive). + /// The starting iteration value for the inner loop. + /// The final iteration value for the inner loop (exclusive). + /// The instance representing the action to invoke. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void For2D(int top, int bottom, int left, int right, in TAction action) + where TAction : struct, IAction2D + { + For2D(top, bottom, left, right, action, 1); + } + + /// + /// Executes a specified action in an optimized parallel loop. + /// + /// The type of action (implementing ) to invoke for each pair of iteration indices. + /// The starting iteration value for the outer loop. + /// The final iteration value for the outer loop (exclusive). + /// The starting iteration value for the inner loop. + /// The final iteration value for the inner loop (exclusive). + /// 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 For2D(int top, int bottom, int left, int right, in TAction action, int minimumActionsPerThread) + where TAction : struct, IAction2D + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (top > bottom) + { + ThrowArgumentOutOfRangeExceptionForTopGreaterThanBottom(); + } + + if (left > right) + { + ThrowArgumentOutOfRangeExceptionForLeftGreaterThanRight(); + } + + // If either side of the target area is empty, no iterations are performed + if (top == bottom || left == right) + { + return; + } + + int + height = Math.Abs(top - bottom), + width = Math.Abs(left - right), + count = height * width, + maxBatches = 1 + ((count - 1) / minimumActionsPerThread), + clipBatches = Math.Min(maxBatches, height), + cores = Environment.ProcessorCount, + numBatches = Math.Min(clipBatches, cores); + + // Skip the parallel invocation when a single batch is needed + if (numBatches == 1) + { + for (int y = top; y < bottom; y++) + { + for (int x = left; x < right; x++) + { + Unsafe.AsRef(action).Invoke(y, x); + } + } + + return; + } + + int batchHeight = 1 + ((height - 1) / numBatches); + + var actionInvoker = new Action2DInvoker(top, bottom, left, right, batchHeight, action); + + // 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 Action2DInvoker + where TAction : struct, IAction2D + { + private readonly int startY; + private readonly int endY; + private readonly int startX; + private readonly int endX; + private readonly int batchHeight; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Action2DInvoker( + int startY, + int endY, + int startX, + int endX, + int batchHeight, + in TAction action) + { + this.startY = startY; + this.endY = endY; + this.startX = startX; + this.endX = endX; + this.batchHeight = batchHeight; + this.action = action; + } + + /// + /// 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 + heightOffset = i * this.batchHeight, + lowY = this.startY + heightOffset, + highY = lowY + this.batchHeight, + stopY = Math.Min(highY, this.endY); + + for (int y = lowY; y < stopY; y++) + { + for (int x = this.startX; x < this.endX; x++) + { + Unsafe.AsRef(this.action).Invoke(y, x); + } + } + } + } + } + + /// + /// A contract for actions being executed with two input indices. + /// + /// If the method is small enough, it is highly recommended to mark it with . + public interface IAction2D + { + /// + /// Executes the action associated with two specified indices. + /// + /// The first index for the action to execute. + /// The second index for the action to execute. + void Invoke(int i, int j); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs new file mode 100644 index 00000000000..d2a9be06eb6 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs @@ -0,0 +1,171 @@ +// 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.Runtime.InteropServices; +using System.Threading.Tasks; + +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(ReadOnlyMemory 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(ReadOnlyMemory 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(ReadOnlyMemory 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(ReadOnlyMemory memory, in TAction action, int minimumActionsPerThread) + where TAction : struct, IInAction + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (memory.IsEmpty) + { + return; + } + + int + maxBatches = 1 + ((memory.Length - 1) / minimumActionsPerThread), + cores = Environment.ProcessorCount, + numBatches = Math.Min(maxBatches, cores); + + // Skip the parallel invocation when a single batch is needed + if (numBatches == 1) + { + foreach (var item in memory.Span) + { + Unsafe.AsRef(action).Invoke(item); + } + + return; + } + + int batchSize = 1 + ((memory.Length - 1) / numBatches); + + var actionInvoker = new InActionInvoker(batchSize, memory, action); + + // 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 InActionInvoker + where TAction : struct, IInAction + { + private readonly int batchSize; + private readonly ReadOnlyMemory memory; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public InActionInvoker( + int batchSize, + ReadOnlyMemory memory, + in TAction action) + { + this.batchSize = batchSize; + this.memory = memory; + this.action = action; + } + + /// + /// 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 + low = i * this.batchSize, + high = low + this.batchSize, + end = Math.Min(high, this.memory.Length); + + ref TItem r0 = ref MemoryMarshal.GetReference(this.memory.Span); + + for (int j = low; j < end; j++) + { + ref TItem rj = ref Unsafe.Add(ref r0, j); + + Unsafe.AsRef(this.action).Invoke(rj); + } + } + } + } + + /// + /// A contract for actions being executed on items of a specific type, with readonly access. + /// + /// The type of items to process. + /// If the method is small enough, it is highly recommended to mark it with . + public interface IInAction + { + /// + /// Executes the action on a specified item. + /// + /// The current item to process. + void Invoke(in T item); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs new file mode 100644 index 00000000000..e6dae6624d8 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs @@ -0,0 +1,171 @@ +// 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.Runtime.InteropServices; +using System.Threading.Tasks; + +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(Memory 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(Memory 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(Memory 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(Memory memory, in TAction action, int minimumActionsPerThread) + where TAction : struct, IRefAction + { + if (minimumActionsPerThread <= 0) + { + ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread(); + } + + if (memory.IsEmpty) + { + return; + } + + int + maxBatches = 1 + ((memory.Length - 1) / minimumActionsPerThread), + cores = Environment.ProcessorCount, + numBatches = Math.Min(maxBatches, cores); + + // Skip the parallel invocation when a single batch is needed + if (numBatches == 1) + { + foreach (ref var item in memory.Span) + { + Unsafe.AsRef(action).Invoke(ref item); + } + + return; + } + + int batchSize = 1 + ((memory.Length - 1) / numBatches); + + var actionInvoker = new RefActionInvoker(batchSize, memory, action); + + // 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 RefActionInvoker + where TAction : struct, IRefAction + { + private readonly int batchSize; + private readonly ReadOnlyMemory memory; + private readonly TAction action; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefActionInvoker( + int batchSize, + ReadOnlyMemory memory, + in TAction action) + { + this.batchSize = batchSize; + this.memory = memory; + this.action = action; + } + + /// + /// 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 + low = i * this.batchSize, + high = low + this.batchSize, + end = Math.Min(high, this.memory.Length); + + ref TItem r0 = ref MemoryMarshal.GetReference(this.memory.Span); + + for (int j = low; j < end; j++) + { + ref TItem rj = ref Unsafe.Add(ref r0, j); + + Unsafe.AsRef(this.action).Invoke(ref rj); + } + } + } + } + + /// + /// A contract for actions being executed on items of a specific type, with side effect. + /// + /// The type of items to process. + /// If the method is small enough, it is highly recommended to mark it with . + public interface IRefAction + { + /// + /// Executes the action on a specified item. + /// + /// The current item to process. + void Invoke(ref T item); + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ThrowExceptions.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ThrowExceptions.cs new file mode 100644 index 00000000000..d13bdfd577e --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ThrowExceptions.cs @@ -0,0 +1,66 @@ +// 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; + +namespace Microsoft.Toolkit.HighPerformance.Helpers +{ + /// + /// Helpers to work with parallel code in a highly optimized manner. + /// + public static partial class ParallelHelper + { + /// + /// Throws an when an invalid parameter is specified for the minimum actions per thread. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread() + { + /* Having the argument name here manually typed is + * not ideal, but this way we save passing that string as + * a parameter, since it's always the same anyway. + * Same goes for the other helper methods below. */ + throw new ArgumentOutOfRangeException( + "minimumActionsPerThread", + "Each thread needs to perform at least one action"); + } + + /// + /// Throws an when an invalid start parameter is specified for 1D loops. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForStartGreaterThanEnd() + { + throw new ArgumentOutOfRangeException("start", "The start parameter must be less than or equal to end"); + } + + /// + /// Throws an when a range has an index starting from an end. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentExceptionForRangeIndexFromEnd(string name) + { + throw new ArgumentException("The bounds of the range can't start from an end", name); + } + + /// + /// Throws an when an invalid top parameter is specified for 2D loops. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForTopGreaterThanBottom() + { + throw new ArgumentOutOfRangeException("top", "The top parameter must be less than or equal to bottom"); + } + + /// + /// Throws an when an invalid left parameter is specified for 2D loops. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForLeftGreaterThanRight() + { + throw new ArgumentOutOfRangeException("left", "The left parameter must be less than or equal to right"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj new file mode 100644 index 00000000000..8c8e89e8b7c --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0;netstandard2.1 + 8.0 + true + Windows Community Toolkit High Performance .NET Standard + + This package includes high performance .NET Standard helpers such as: + - ArrayPoolBufferWriter<T>: an IBufferWriter<T> implementation using pooled arrays, which also supports IMemoryOwner<T>. + - 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. + - String, array, Span<T>, Memory<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. + - Box<T>: a type mapping boxed value types and exposing some utility and high performance methods. + - Ref<T>: a stack-only struct that can store a reference to a value of a specified type. + + UWP Toolkit Windows IncrementalLoadingCollection String Array extensions helpers + + + Full + + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.HighPerformance/NullableReadOnlyRef{T}.cs b/Microsoft.Toolkit.HighPerformance/NullableReadOnlyRef{T}.cs new file mode 100644 index 00000000000..733f2590da0 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/NullableReadOnlyRef{T}.cs @@ -0,0 +1,82 @@ +// 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 NETSTANDARD2_1 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance +{ + /// + /// A that can store an optional readonly reference to a value of a specified type. + /// + /// The type of value to reference. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + public readonly ref struct NullableReadOnlyRef + { + /// + /// The 1-length instance used to track the target value. + /// + private readonly ReadOnlySpan span; + + /// + /// Initializes a new instance of the struct. + /// + /// The readonly reference to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public NullableReadOnlyRef(in T value) + { + ref T r0 = ref Unsafe.AsRef(value); + + span = MemoryMarshal.CreateReadOnlySpan(ref r0, 1); + } + + /// + /// Gets a value indicating whether or not the current instance wraps a valid reference that can be accessed. + /// + public bool HasValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // See comment in NullableRef about this + byte length = unchecked((byte)span.Length); + + return Unsafe.As(ref length); + } + } + + /// + /// Gets the reference represented by the current instance. + /// + /// Thrown if is . + public ref readonly T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (!HasValue) + { + ThrowNullReferenceException(); + } + + return ref MemoryMarshal.GetReference(span); + } + } + + /// + /// Throws a when trying to access for a default instance. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowNullReferenceException() + { + throw new NullReferenceException("The current instance doesn't have a value that can be accessed"); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/NullableRef{T}.cs b/Microsoft.Toolkit.HighPerformance/NullableRef{T}.cs new file mode 100644 index 00000000000..548f1c1194b --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/NullableRef{T}.cs @@ -0,0 +1,86 @@ +// 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 NETSTANDARD2_1 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance +{ + /// + /// A that can store an optional reference to a value of a specified type. + /// + /// The type of value to reference. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + public readonly ref struct NullableRef + { + /// + /// The 1-length instance used to track the target value. + /// + private readonly Span span; + + /// + /// Initializes a new instance of the struct. + /// + /// The reference to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public NullableRef(ref T value) + { + span = MemoryMarshal.CreateSpan(ref value, 1); + } + + /// + /// Gets a value indicating whether or not the current instance wraps a valid reference that can be accessed. + /// + public bool HasValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + /* We know that the span will always have a length of either + * 1 or 0, se instead of using a cmp instruction and setting the + * zero flag to produce our boolean value, we can just cast + * the length to byte without overflow checks (doing a cast will + * also account for the byte endianness of the current system), + * and then reinterpret that value to a bool flag. + * This results in a single movzx instruction on x86-64. */ + byte length = unchecked((byte)span.Length); + + return Unsafe.As(ref length); + } + } + + /// + /// Gets the reference represented by the current instance. + /// + /// Thrown if is . + public ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (!HasValue) + { + ThrowNullReferenceException(); + } + + return ref MemoryMarshal.GetReference(span); + } + } + + /// + /// Throws a when trying to access for a default instance. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowNullReferenceException() + { + throw new NullReferenceException("The current instance doesn't have a value that can be accessed"); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs b/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs new file mode 100644 index 00000000000..443d9964296 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/ReadOnlyRef{T}.cs @@ -0,0 +1,137 @@ +// 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; +#if NETSTANDARD2_1 +using System.Runtime.InteropServices; +#endif + +namespace Microsoft.Toolkit.HighPerformance +{ + /// + /// A that can store a readonly reference to a value of a specified type. + /// + /// The type of value to reference. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + public readonly ref struct ReadOnlyRef + { +#if NETSTANDARD2_1 + /// + /// The 1-length instance used to track the target value. + /// + private readonly ReadOnlySpan span; + + /// + /// Initializes a new instance of the struct. + /// + /// The readonly reference to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRef(in T value) + { + ref T r0 = ref Unsafe.AsRef(value); + + span = MemoryMarshal.CreateReadOnlySpan(ref r0, 1); + } + + /// + /// Gets the readonly reference represented by the current instance. + /// + public ref readonly T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref MemoryMarshal.GetReference(span); + } + + /// + /// Implicitly converts a instance into a one. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlyRef(Ref reference) + { + return new ReadOnlyRef(reference.Value); + } +#else + /// + /// The owner the current instance belongs to + /// + private readonly object owner; + + /// + /// The target offset within the current instance is pointing to + /// + private readonly IntPtr offset; + + /// + /// Initializes a new instance of the struct. + /// + /// The owner to create a portable reference for. + /// The target offset within for the target reference. + /// The parameter is not validated, and it's responsability of the caller to ensure it's valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlyRef(object owner, IntPtr offset) + { + this.owner = owner; + this.offset = offset; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The owner to create a portable reference for. + /// The target reference to point to (it must be within ). + /// The parameter is not validated, and it's responsability of the caller to ensure it's valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRef(object owner, in T value) + { + this.owner = owner; + + ref T valueRef = ref Unsafe.AsRef(value); + var data = Unsafe.As(owner); + ref byte r0 = ref data.Data; + ref byte r1 = ref Unsafe.As(ref valueRef); + + offset = Unsafe.ByteOffset(ref r0, ref r1); + } + + /// + /// Gets the readonly reference represented by the current instance. + /// + public ref readonly T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var data = Unsafe.As(owner); + ref byte r0 = ref data.Data; + ref byte r1 = ref Unsafe.AddByteOffset(ref r0, offset); + + return ref Unsafe.As(ref r1); + } + } + + /// + /// Implicitly converts a instance into a one. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlyRef(Ref reference) + { + return new ReadOnlyRef(reference.Owner, reference.Offset); + } +#endif + + /// + /// Implicitly gets the value from a given instance. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(ReadOnlyRef reference) + { + return reference.Value; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Ref{T}.cs b/Microsoft.Toolkit.HighPerformance/Ref{T}.cs new file mode 100644 index 00000000000..b3b66933cbd --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Ref{T}.cs @@ -0,0 +1,123 @@ +// 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 System.Runtime.InteropServices; + +namespace Microsoft.Toolkit.HighPerformance +{ + /// + /// A that can store a reference to a value of a specified type. + /// + /// The type of value to reference. + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206", Justification = "The type is a ref struct")] + public readonly ref struct Ref + { +#if NETSTANDARD2_1 + /// + /// The 1-length instance used to track the target value. + /// + private readonly Span span; + + /// + /// Initializes a new instance of the struct. + /// + /// The reference to the target value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ref(ref T value) + { + span = MemoryMarshal.CreateSpan(ref value, 1); + } + + /// + /// Gets the reference represented by the current instance. + /// + public ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref MemoryMarshal.GetReference(span); + } +#else + /// + /// The owner the current instance belongs to + /// + internal readonly object Owner; + + /// + /// The target offset within the current instance is pointing to + /// + /// + /// Using an instead of to avoid the int to + /// native int conversion in the generated asm (an extra movsxd on x64). + /// + internal readonly IntPtr Offset; + + /// + /// Initializes a new instance of the struct. + /// + /// The owner to create a portable reference for. + /// The target reference to point to (it must be within ). + /// The parameter is not validated, and it's responsability of the caller to ensure it's valid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ref(object owner, ref T value) + { + this.Owner = owner; + + var data = Unsafe.As(owner); + ref byte r0 = ref data.Data; + ref byte r1 = ref Unsafe.As(ref value); + + Offset = Unsafe.ByteOffset(ref r0, ref r1); + } + + /// + /// Gets the reference represented by the current instance. + /// + public ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var data = Unsafe.As(Owner); + ref byte r0 = ref data.Data; + ref byte r1 = ref Unsafe.AddByteOffset(ref r0, Offset); + + return ref Unsafe.As(ref r1); + } + } +#endif + + /// + /// Implicitly gets the value from a given instance. + /// + /// The input instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(Ref reference) + { + return reference.Value; + } + } + + // Description adapted from CoreCLR: see https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,285. + // CLR objects are laid out in memory as follows: + // [ sync block || pMethodTable || raw data .. ] + // ^ ^ + // | \-- ref Unsafe.As(owner).Data + // \-- object + // The reference to RawObjectData.Data points to the first data byte in the + // target object, skipping over the sync block, method table and string length. + // This type is not nested to avoid creating multiple generic types. + [StructLayout(LayoutKind.Explicit)] + internal sealed class RawObjectData + { +#pragma warning disable CS0649 // Unassigned fields +#pragma warning disable SA1401 // Fields should be private + [FieldOffset(0)] + public byte Data; +#pragma warning restore CS0649 +#pragma warning restore SA1401 + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IMemoryOwnerStream.cs b/Microsoft.Toolkit.HighPerformance/Streams/IMemoryOwnerStream.cs new file mode 100644 index 00000000000..c269aba6819 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/IMemoryOwnerStream.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.IO; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping an of instance. + /// + internal sealed class IMemoryOwnerStream : MemoryStream + { + /// + /// The of instance currently in use. + /// + private readonly IMemoryOwner memory; + + /// + /// Initializes a new instance of the class. + /// + /// The input of instance to use. + public IMemoryOwnerStream(IMemoryOwner memory) + : base(memory.Memory) + { + this.memory = memory; + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + this.memory.Dispose(); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.NETStandard21.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.NETStandard21.cs new file mode 100644 index 00000000000..66e177eb302 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.NETStandard21.cs @@ -0,0 +1,117 @@ +// 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 NETSTANDARD2_1 + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping a or instance. + /// + internal partial class MemoryStream + { + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateDisposed(); + + Span source = this.memory.Span.Slice(this.position); + + this.position += source.Length; + + destination.Write(source); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + try + { + int result = Read(buffer.Span); + + return new ValueTask(result); + } + catch (OperationCanceledException e) + { + return new ValueTask(Task.FromCanceled(e.CancellationToken)); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + try + { + Write(buffer.Span); + + return default; + } + catch (OperationCanceledException e) + { + return new ValueTask(Task.FromCanceled(e.CancellationToken)); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + } + + /// + public override int Read(Span buffer) + { + ValidateDisposed(); + + int + bytesAvailable = this.memory.Length - this.position, + bytesCopied = Math.Min(bytesAvailable, buffer.Length); + + Span source = this.memory.Span.Slice(this.position, bytesCopied); + + source.CopyTo(buffer); + + this.position += bytesCopied; + + return bytesCopied; + } + + /// + public override void Write(ReadOnlySpan buffer) + { + ValidateDisposed(); + ValidateCanWrite(); + + Span destination = this.memory.Span.Slice(this.position); + + if (!buffer.TryCopyTo(destination)) + { + ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.position += buffer.Length; + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs new file mode 100644 index 00000000000..e17c58f55bc --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs @@ -0,0 +1,110 @@ +// 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.IO; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping a or instance. + /// + internal partial class MemoryStream + { + /// + /// Throws an when setting the property. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForPosition() + { + throw new ArgumentOutOfRangeException(nameof(Position), "The value for the property was not in the valid range."); + } + + /// + /// Throws an when an input buffer is . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentNullExceptionForBuffer() + { + throw new ArgumentNullException("buffer", "The buffer is null."); + } + + /// + /// Throws an when the input count is negative. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForOffset() + { + throw new ArgumentOutOfRangeException("offset", "Offset can't be negative."); + } + + /// + /// Throws an when the input count is negative. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRangeExceptionForCount() + { + throw new ArgumentOutOfRangeException("count", "Count can't be negative."); + } + + /// + /// Throws an when the sum of offset and count exceeds the length of the target buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentExceptionForLength() + { + throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer"); + } + + /// + /// Throws a when trying to write on a readonly stream. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowNotSupportedExceptionForCanWrite() + { + throw new NotSupportedException("The current stream doesn't support writing."); + } + + /// + /// Throws an when trying to write too many bytes to the target stream. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentExceptionForEndOfStreamOnWrite() + { + throw new InvalidOperationException("The current stream can't contain the requested input data."); + } + + /// + /// Throws a when trying to set the length of the stream. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowNotSupportedExceptionForSetLength() + { + throw new NotSupportedException("Setting the length is not supported for this stream."); + } + + /// + /// Throws an when using an invalid seek mode. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615", Justification = "Throw method")] + public static long ThrowArgumentExceptionForSeekOrigin() + { + throw new ArgumentException("The input seek mode is not valid.", "origin"); + } + + /// + /// Throws an when using a disposed instance. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(memory), "The current stream has already been disposed"); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs new file mode 100644 index 00000000000..d937704d41c --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs @@ -0,0 +1,85 @@ +// 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.IO; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping a or instance. + /// + internal partial class MemoryStream + { + /// + /// Validates the argument. + /// + /// The new value being set. + /// The maximum length of the target . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidatePosition(long position, int length) + { + if ((ulong)position >= (ulong)length) + { + ThrowArgumentOutOfRangeExceptionForPosition(); + } + } + + /// + /// Validates the or arguments. + /// + /// The target array. + /// The offset within . + /// The number of elements to process within . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidateBuffer(byte[]? buffer, int offset, int count) + { + if (buffer is null) + { + ThrowArgumentNullExceptionForBuffer(); + } + + if (offset < 0) + { + ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (count < 0) + { + ThrowArgumentOutOfRangeExceptionForCount(); + } + + if (offset + count > buffer!.Length) + { + ThrowArgumentExceptionForLength(); + } + } + + /// + /// Validates the property. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateCanWrite() + { + if (!CanWrite) + { + ThrowNotSupportedExceptionForCanWrite(); + } + } + + /// + /// Validates that the current instance hasn't been disposed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateDisposed() + { + if (this.disposed) + { + ThrowObjectDisposedException(); + } + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.cs new file mode 100644 index 00000000000..d70cd024e9a --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.cs @@ -0,0 +1,317 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping a or instance. + /// + /// + /// This type is not marked as so that it can be inherited by + /// , which adds the support for + /// the wrapped buffer. We're not worried about the performance penalty here caused by the JIT + /// not being able to resolve the instruction, as this type is + /// only exposed as a anyway, so the generated code would be the same. + /// + internal partial class MemoryStream : Stream + { + /// + /// Indicates whether was actually a instance. + /// + private readonly bool isReadOnly; + + /// + /// The instance currently in use. + /// + private Memory memory; + + /// + /// The current position within . + /// + private int position; + + /// + /// Indicates whether or not the current instance has been disposed + /// + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The input instance to use. + public MemoryStream(Memory memory) + { + this.memory = memory; + this.position = 0; + this.isReadOnly = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The input instance to use. + public MemoryStream(ReadOnlyMemory memory) + { + this.memory = MemoryMarshal.AsMemory(memory); + this.position = 0; + this.isReadOnly = true; + } + + /// + public override bool CanRead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public override bool CanSeek + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public override bool CanWrite + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.isReadOnly && !this.disposed; + } + + /// + public override long Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ValidateDisposed(); + + return this.memory.Length; + } + } + + /// + public override long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ValidateDisposed(); + + return this.position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + ValidateDisposed(); + ValidatePosition(value, this.memory.Length); + + this.position = unchecked((int)value); + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + CopyTo(destination, bufferSize); + + return Task.CompletedTask; + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + /// + public override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int result = Read(buffer, offset, count); + + return Task.FromResult(result); + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + + return Task.CompletedTask; + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + ValidateDisposed(); + + long index = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.position + offset, + SeekOrigin.End => this.memory.Length + offset, + _ => ThrowArgumentExceptionForSeekOrigin() + }; + + ValidatePosition(index, this.memory.Length); + + this.position = unchecked((int)index); + + return index; + } + + /// + public override void SetLength(long value) + { + ThrowNotSupportedExceptionForSetLength(); + } + + /// + public override int Read(byte[]? buffer, int offset, int count) + { + ValidateDisposed(); + ValidateBuffer(buffer, offset, count); + + int + bytesAvailable = this.memory.Length - this.position, + bytesCopied = Math.Min(bytesAvailable, count); + + Span + source = this.memory.Span.Slice(this.position, bytesCopied), + destination = buffer.AsSpan(offset, bytesCopied); + + source.CopyTo(destination); + + this.position += bytesCopied; + + return bytesCopied; + } + + /// + public override int ReadByte() + { + ValidateDisposed(); + + if (this.position == this.memory.Length) + { + return -1; + } + + return this.memory.Span[this.position++]; + } + + /// + public override void Write(byte[]? buffer, int offset, int count) + { + ValidateDisposed(); + ValidateCanWrite(); + ValidateBuffer(buffer, offset, count); + + Span + source = buffer.AsSpan(offset, count), + destination = this.memory.Span.Slice(this.position); + + if (!source.TryCopyTo(destination)) + { + ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.position += source.Length; + } + + /// + public override void WriteByte(byte value) + { + ValidateDisposed(); + ValidateCanWrite(); + + if (this.position == this.memory.Length) + { + ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.memory.Span[this.position++] = value; + } + + /// + protected override void Dispose(bool disposing) + { + if (this.disposed) + { + return; + } + + this.disposed = true; + this.memory = default; + } + } +} diff --git a/Microsoft.Toolkit.Uwp/Microsoft.Toolkit.Uwp.csproj b/Microsoft.Toolkit.Uwp/Microsoft.Toolkit.Uwp.csproj index 04ac84121fe..7f53af79c61 100644 --- a/Microsoft.Toolkit.Uwp/Microsoft.Toolkit.Uwp.csproj +++ b/Microsoft.Toolkit.Uwp/Microsoft.Toolkit.Uwp.csproj @@ -8,7 +8,6 @@ true - diff --git a/UnitTests/UnitTests.HighPerformance.NetCore/UnitTests.HighPerformance.NetCore.csproj b/UnitTests/UnitTests.HighPerformance.NetCore/UnitTests.HighPerformance.NetCore.csproj new file mode 100644 index 00000000000..9c030bf0816 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.NetCore/UnitTests.HighPerformance.NetCore.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1;netcoreapp3.0 + 8.0 + true + false + + + + + + + + + + + + + + + + + + + + diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs new file mode 100644 index 00000000000..a4943487474 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs @@ -0,0 +1,121 @@ +// 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.IO; +using System.Linq; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Buffers +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_ArrayPoolBufferWriterOfT + { + [TestCategory("ArrayPoolBufferWriterOfT")] + [TestMethod] + public void Test_ArrayPoolBufferWriterOfT_AllocateAndGetMemoryAndSpan() + { + var writer = new ArrayPoolBufferWriter(); + + Assert.AreEqual(writer.Capacity, 256); + Assert.AreEqual(writer.FreeCapacity, 256); + Assert.AreEqual(writer.WrittenCount, 0); + Assert.IsTrue(writer.WrittenMemory.IsEmpty); + Assert.IsTrue(writer.WrittenSpan.IsEmpty); + + Span span = writer.GetSpan(43); + + Assert.IsTrue(span.Length >= 43); + + writer.Advance(43); + + Assert.AreEqual(writer.Capacity, 256); + Assert.AreEqual(writer.FreeCapacity, 256 - 43); + Assert.AreEqual(writer.WrittenCount, 43); + Assert.AreEqual(writer.WrittenMemory.Length, 43); + Assert.AreEqual(writer.WrittenSpan.Length, 43); + + Assert.ThrowsException(() => writer.Advance(-1)); + Assert.ThrowsException(() => writer.GetMemory(-1)); + Assert.ThrowsException(() => writer.Advance(1024)); + + writer.Dispose(); + + Assert.ThrowsException(() => writer.WrittenMemory); + Assert.ThrowsException(() => writer.WrittenSpan.Length); + Assert.ThrowsException(() => writer.Capacity); + Assert.ThrowsException(() => writer.FreeCapacity); + Assert.ThrowsException(() => writer.Clear()); + Assert.ThrowsException(() => writer.Advance(1)); + } + + [TestCategory("ArrayPoolBufferWriterOfT")] + [TestMethod] + public void Test_ArrayPoolBufferWriterOfT_Clear() + { + using var writer = new ArrayPoolBufferWriter(); + + Span span = writer.GetSpan(4).Slice(0, 4); + + byte[] data = { 1, 2, 3, 4 }; + + data.CopyTo(span); + + writer.Advance(4); + + Assert.AreEqual(writer.WrittenCount, 4); + Assert.IsTrue(span.SequenceEqual(data)); + + writer.Clear(); + + Assert.AreEqual(writer.WrittenCount, 0); + Assert.IsTrue(span.ToArray().All(b => b == 0)); + } + + [TestCategory("ArrayPoolBufferWriterOfT")] + [TestMethod] + public void Test_ArrayPoolBufferWriterOfT_MultipleDispose() + { + var writer = new ArrayPoolBufferWriter(); + + writer.Dispose(); + writer.Dispose(); + writer.Dispose(); + writer.Dispose(); + } + + [TestCategory("ArrayPoolBufferWriterOfT")] + [TestMethod] + public void Test_ArrayPoolBufferWriterOfT_AsStream() + { + var writer = new ArrayPoolBufferWriter(); + + Span data = Guid.NewGuid().ToByteArray(); + + data.CopyTo(writer.GetSpan(data.Length)); + + writer.Advance(data.Length); + + Assert.AreEqual(writer.WrittenCount, data.Length); + + Stream stream = writer.AsStream(); + + Assert.AreEqual(stream.Length, data.Length); + + byte[] result = new byte[16]; + + stream.Read(result, 0, result.Length); + + Assert.IsTrue(data.SequenceEqual(result)); + + stream.Dispose(); + + Assert.ThrowsException(() => writer.Capacity); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_MemoryOwner{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_MemoryOwner{T}.cs new file mode 100644 index 00000000000..f99d411fb01 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_MemoryOwner{T}.cs @@ -0,0 +1,89 @@ +// 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 Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Buffers +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_MemoryOwnerOfT + { + [TestCategory("MemoryOwnerOfT")] + [TestMethod] + public void Test_MemoryOwnerOfT_AllocateAndGetMemoryAndSpan() + { + using var buffer = MemoryOwner.Allocate(127); + + Assert.IsTrue(buffer.Length == 127); + Assert.IsTrue(buffer.Memory.Length == 127); + Assert.IsTrue(buffer.Span.Length == 127); + + buffer.Span.Fill(42); + + Assert.IsTrue(buffer.Memory.Span.ToArray().All(i => i == 42)); + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42)); + } + + [TestCategory("MemoryOwnerOfT")] + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public void Test_MemoryOwnerOfT_DisposedMemory() + { + var buffer = MemoryOwner.Allocate(127); + + buffer.Dispose(); + + _ = buffer.Memory; + } + + [TestCategory("MemoryOwnerOfT")] + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public void Test_MemoryOwnerOfT_DisposedSpan() + { + var buffer = MemoryOwner.Allocate(127); + + buffer.Dispose(); + + _ = buffer.Span; + } + + [TestCategory("MemoryOwnerOfT")] + [TestMethod] + public void Test_MemoryOwnerOfT_MultipleDispose() + { + var buffer = MemoryOwner.Allocate(127); + + buffer.Dispose(); + buffer.Dispose(); + buffer.Dispose(); + buffer.Dispose(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_MemoryOwnerOfT_PooledBuffersAndClear() + { + using (var buffer = MemoryOwner.Allocate(127)) + { + buffer.Span.Fill(42); + } + + using (var buffer = MemoryOwner.Allocate(127)) + { + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42)); + } + + using (var buffer = MemoryOwner.Allocate(127, AllocationMode.Clear)) + { + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0)); + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_SpanOwner{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_SpanOwner{T}.cs new file mode 100644 index 00000000000..e39de1f7a79 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_SpanOwner{T}.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Buffers +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_SpanOwnerOfT + { + [TestCategory("SpanOwnerOfT")] + [TestMethod] + public void Test_SpanOwnerOfT_AllocateAndGetMemoryAndSpan() + { + using var buffer = SpanOwner.Allocate(127); + + Assert.IsTrue(buffer.Length == 127); + Assert.IsTrue(buffer.Span.Length == 127); + + buffer.Span.Fill(42); + + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42)); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_SpanOwnerOfT_PooledBuffersAndClear() + { + using (var buffer = SpanOwner.Allocate(127)) + { + buffer.Span.Fill(42); + } + + using (var buffer = SpanOwner.Allocate(127)) + { + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42)); + } + + using (var buffer = SpanOwner.Allocate(127, AllocationMode.Clear)) + { + Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0)); + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs new file mode 100644 index 00000000000..89c8d485e4f --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.2D.cs @@ -0,0 +1,315 @@ +// 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.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + public partial class Test_ArrayExtensions + { + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_DangerousGetReference_Int() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + ref int r0 = ref array.DangerousGetReference(); + ref int r1 = ref array[0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_DangerousGetReference_String() + { + string[,] array = + { + { "a", "bb", "ccc" }, + { "dddd", "eeeee", "ffffff" } + }; + + ref string r0 = ref array.DangerousGetReference(); + ref string r1 = ref array[0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_DangerousGetReferenceAt_Zero() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + ref int r0 = ref array.DangerousGetReferenceAt(0, 0); + ref int r1 = ref array[0, 0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_DangerousGetReferenceAt_Index() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + ref int r0 = ref array.DangerousGetReferenceAt(1, 3); + ref int r1 = ref array[1, 3]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_FillArrayMid() + { + bool[,] test = new bool[4, 5]; + + test.Fill(true, 1, 1, 3, 2); + + var expected = new[,] + { + { false, false, false, false, false }, + { false, true, true, true, false }, + { false, true, true, true, false }, + { false, false, false, false, false }, + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_FillArrayTwice() + { + bool[,] test = new bool[4, 5]; + + test.Fill(true, 0, 0, 1, 2); + test.Fill(true, 1, 3, 2, 2); + + var expected = new[,] + { + { true, false, false, false, false }, + { true, false, false, true, true }, + { false, false, false, true, true }, + { false, false, false, false, false }, + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_FillArrayNegativeSize() + { + 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); + + var expected = new[,] + { + { false, false, false, false, false }, + { false, false, true, true, false }, + { false, false, true, true, false }, + { false, false, true, true, false }, + }; + + CollectionAssert.AreEqual(expected, test); + } + + [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() + { + bool[,] test = new bool[5, 4]; + + test.Fill(true, 3, 2, 3, 3); + + var expected = new[,] + { + { false, false, false, false }, + { false, false, false, false }, + { false, false, false, false }, + { false, false, true, true }, + { false, false, true, true }, + }; + + CollectionAssert.AreEqual(expected, test); + } + + [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 = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + int j = 0; + foreach (ref int value in array.GetRow(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[1, j++])); + } + + CollectionAssert.AreEqual(array.GetRow(1).ToArray(), new[] { 5, 6, 7, 8 }); + + Assert.ThrowsException(() => + { + foreach (var _ in array.GetRow(-1)) { } + }); + + Assert.ThrowsException(() => + { + foreach (var _ in array.GetRow(20)) { } + }); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_GetRow_Empty() + { + int[,] array = new int[0, 0]; + + 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() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + int i = 0; + foreach (ref int value in array.GetColumn(1)) + { + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[i++, 1])); + } + + CollectionAssert.AreEqual(array.GetColumn(1).ToArray(), new[] { 2, 6, 10 }); + + Assert.ThrowsException(() => + { + foreach (var _ in array.GetColumn(-1)) { } + }); + + Assert.ThrowsException(() => + { + foreach (var _ in array.GetColumn(20)) { } + }); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_GetColumn_Empty() + { + int[,] array = new int[0, 0]; + + Assert.ThrowsException(() => array.GetColumn(0).ToArray()); + } + +#if NETCOREAPP3_0 + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_AsSpan_Empty() + { + int[,] array = new int[0, 0]; + + Span span = array.AsSpan(); + + Assert.AreEqual(span.Length, array.Length); + Assert.IsTrue(span.IsEmpty); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_2D_AsSpan_Populated() + { + int[,] array = + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 } + }; + + Span span = array.AsSpan(); + + Assert.AreEqual(span.Length, array.Length); + + ref int r0 = ref array[0, 0]; + ref int r1 = ref span[0]; + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } +#endif + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs new file mode 100644 index 00000000000..3faa4649c56 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayExtensions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public partial class Test_ArrayExtensions + { + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_DangerousGetReference() + { + string[] tokens = "aa,bb,cc,dd,ee,ff,gg,hh,ii".Split(','); + + ref string r0 = ref Unsafe.AsRef(tokens.DangerousGetReference()); + ref string r1 = ref Unsafe.AsRef(tokens[0]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_DangerousGetReferenceAt_Zero() + { + string[] tokens = "aa,bb,cc,dd,ee,ff,gg,hh,ii".Split(','); + + ref string r0 = ref Unsafe.AsRef(tokens.DangerousGetReference()); + ref string r1 = ref Unsafe.AsRef(tokens.DangerousGetReferenceAt(0)); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ArrayExtensions")] + [TestMethod] + public void Test_ArrayExtensions_DangerousGetReferenceAt_Index() + { + string[] tokens = "aa,bb,cc,dd,ee,ff,gg,hh,ii".Split(','); + + ref string r0 = ref Unsafe.AsRef(tokens.DangerousGetReferenceAt(5)); + ref string r1 = ref Unsafe.AsRef(tokens[5]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayPoolExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayPoolExtensions.cs new file mode 100644 index 00000000000..3a728313e1b --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ArrayPoolExtensions.cs @@ -0,0 +1,97 @@ +// 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.Linq; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_ArrayPoolExtensions + { + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Test_ArrayExtensions_InvalidSize() + { + int[] array = null; + + ArrayPool.Shared.Resize(ref array, -1); + } + + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + public void Test_ArrayExtensions_NewArray() + { + int[] array = null; + + ArrayPool.Shared.Resize(ref array, 10); + + Assert.IsNotNull(array); + Assert.IsTrue(array.Length >= 10); + } + + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + public void Test_ArrayExtensions_SameSize() + { + int[] array = ArrayPool.Shared.Rent(10); + int[] backup = array; + + ArrayPool.Shared.Resize(ref array, array.Length); + + Assert.AreSame(array, backup); + } + + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + public void Test_ArrayExtensions_Expand() + { + int[] array = ArrayPool.Shared.Rent(16); + int[] backup = array; + + array.AsSpan().Fill(7); + + ArrayPool.Shared.Resize(ref array, 32); + + Assert.AreNotSame(array, backup); + Assert.IsTrue(array.Length >= 32); + Assert.IsTrue(array.AsSpan(0, 16).ToArray().All(i => i == 7)); + } + + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + public void Test_ArrayExtensions_Shrink() + { + int[] array = ArrayPool.Shared.Rent(32); + int[] backup = array; + + array.AsSpan().Fill(7); + + ArrayPool.Shared.Resize(ref array, 16); + + Assert.AreNotSame(array, backup); + Assert.IsTrue(array.Length >= 16); + Assert.IsTrue(array.AsSpan(0, 16).ToArray().All(i => i == 7)); + } + + [TestCategory("ArrayPoolExtensions")] + [TestMethod] + public void Test_ArrayExtensions_Clear() + { + int[] array = ArrayPool.Shared.Rent(16); + int[] backup = array; + + array.AsSpan().Fill(7); + + ArrayPool.Shared.Resize(ref array, 0, true); + + Assert.AreNotSame(array, backup); + Assert.IsTrue(backup.AsSpan(0, 16).ToArray().All(i => i == 0)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs new file mode 100644 index 00000000000..834922b8831 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_BoolExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_BoolExtensions + { + [TestCategory("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)); + } + + [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)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_HashCodeExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_HashCodeExtensions.cs new file mode 100644 index 00000000000..3d7df0f1619 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_HashCodeExtensions.cs @@ -0,0 +1,116 @@ +// 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.InteropServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_HashCodeExtensions + { + /// + /// Gets the list of counts to test the extension for + /// + private static ReadOnlySpan TestCounts => new[] { 0, 1, 7, 128, 255, 256, short.MaxValue, 245_000 }; + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount8() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount16() + { + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount32() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount64() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat() + { + TestForType(); + } + + /// + /// Performs a test for a specified type. + /// + /// The type to test. + private static void TestForType() + where T : unmanaged, IEquatable + { + foreach (var count in TestCounts) + { + T[] data = CreateRandomData(count); + + HashCode hashCode1 = default; + + hashCode1.Add(data); + + int hash1 = hashCode1.ToHashCode(); + + HashCode hashCode2 = default; + + hashCode2.Add(data); + + int hash2 = hashCode2.ToHashCode(); + + int hash3 = HashCode.Combine(data); + + Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}"); + Assert.AreEqual(hash1, hash3, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash3}"); + } + } + + /// + /// Creates a random array filled with random data. + /// + /// The type of items to put in the array. + /// The number of array items to create. + /// An array of random elements. + [Pure] + private static T[] CreateRandomData(int count) + where T : unmanaged + { + var random = new Random(count); + + T[] data = new T[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_IMemoryOwnerExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_IMemoryOwnerExtensions.cs new file mode 100644 index 00000000000..3900252578b --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_IMemoryOwnerExtensions.cs @@ -0,0 +1,41 @@ +// 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.IO; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_IMemoryOwnerExtensions + { + [TestCategory("IMemoryOwnerExtensions")] + [TestMethod] + public void Test_IMemoryOwnerExtensions_EmptyIMemoryOwnerStream() + { + MemoryOwner buffer = MemoryOwner.Empty; + + Stream stream = buffer.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, buffer.Length); + Assert.IsTrue(stream.CanWrite); + } + + [TestCategory("IMemoryOwnerExtensions")] + [TestMethod] + public void Test_MemoryExtensions_IMemoryOwnerStream() + { + MemoryOwner buffer = MemoryOwner.Allocate(1024); + + Stream stream = buffer.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, buffer.Length); + Assert.IsTrue(stream.CanWrite); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs new file mode 100644 index 00000000000..705a335735d --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs @@ -0,0 +1,41 @@ +// 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.IO; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_MemoryExtensions + { + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_EmptyMemoryStream() + { + Memory memory = default; + + Stream stream = memory.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, memory.Length); + Assert.IsTrue(stream.CanWrite); + } + + [TestCategory("MemoryExtensions")] + [TestMethod] + public void Test_MemoryExtensions_MemoryStream() + { + Memory memory = new byte[1024]; + + Stream stream = memory.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, memory.Length); + Assert.IsTrue(stream.CanWrite); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ObjectExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ObjectExtensions.cs new file mode 100644 index 00000000000..2fa4869a947 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ObjectExtensions.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_ObjectExtensions + { + [TestCategory("ObjectExtensions")] + [TestMethod] + public void Test_BoxOfT_PrimitiveTypes() + { + Test(true); + Test(false); + Test(27); + Test('a'); + Test(4221124); + Test(3.14f); + Test(8394324ul); + Test(184013.234324); + } + + [TestCategory("ObjectExtensions")] + [TestMethod] + public void Test_BoxOfT_OtherTypes() + { + Test(DateTime.Now); + Test(Guid.NewGuid()); + } + + internal struct TestStruct : IEquatable + { + public int Number; + public char Character; + public string Text; + + /// + public bool Equals(TestStruct other) + { + return + this.Number == other.Number && + this.Character == other.Character && + this.Text == other.Text; + } + } + + [TestCategory("ObjectExtensions")] + [TestMethod] + public void TestBoxOfT_CustomStruct() + { + var a = new TestStruct { Number = 42, Character = 'a', Text = "Hello" }; + var b = new TestStruct { Number = 38293, Character = 'z', Text = "World" }; + + Test(a); + Test(b); + } + + /// + /// Tests the extensions type for a given value. + /// + /// The type to test. + /// The initial value. + private static void Test(T value) + where T : struct, IEquatable + { + object obj = value; + + bool success = obj.TryUnbox(out T result); + + Assert.IsTrue(success); + Assert.AreEqual(value, result); + + success = obj.TryUnbox(out decimal test); + + Assert.IsFalse(success); + Assert.AreEqual(test, default); + + result = obj.DangerousUnbox(); + + Assert.AreEqual(value, result); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlyMemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlyMemoryExtensions.cs new file mode 100644 index 00000000000..82d7e7bfc03 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlyMemoryExtensions.cs @@ -0,0 +1,41 @@ +// 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.IO; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_ReadOnlyMemoryExtensions + { + [TestCategory("ReadOnlyMemoryExtensions")] + [TestMethod] + public void Test_ReadOnlyMemoryExtensions_EmptyMemoryStream() + { + ReadOnlyMemory memory = default; + + Stream stream = memory.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, memory.Length); + Assert.IsFalse(stream.CanWrite); + } + + [TestCategory("ReadOnlyMemoryExtensions")] + [TestMethod] + public void Test_ReadOnlyMemoryExtensions_MemoryStream() + { + ReadOnlyMemory memory = new byte[1024]; + + Stream stream = memory.AsStream(); + + Assert.IsNotNull(stream); + Assert.AreEqual(stream.Length, memory.Length); + Assert.IsFalse(stream.CanWrite); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs new file mode 100644 index 00000000000..672ef277e9b --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -0,0 +1,251 @@ +// 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.Diagnostics.Contracts; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + public partial class Test_ReadOnlySpanExtensions + { + /// + /// Gets the list of counts to test the extension for + /// + private static ReadOnlySpan TestCounts => new[] { 0, 1, 7, 128, 255, 256, short.MaxValue, short.MaxValue + 1, 123_938, 1_678_922, 71_890_819 }; + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_RandomCount8() + { + TestForType((byte)123, CreateRandomData); + TestForType((sbyte)123, CreateRandomData); + TestForType(true, CreateRandomData); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_RandomCount16() + { + TestForType((ushort)4712, CreateRandomData); + TestForType((short)4712, CreateRandomData); + TestForType((char)4712, CreateRandomData); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1139", Justification = "Easier to tell types apart at a glance")] + public void Test_ReadOnlySpanExtensions_RandomCount32() + { + TestForType((int)37438941, CreateRandomData); + TestForType((uint)37438941, CreateRandomData); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1139", Justification = "Easier to tell types apart at a glance")] + public void Test_ReadOnlySpanExtensions_RandomCount64() + { + TestForType((long)47128480128401, CreateRandomData); + TestForType((ulong)47128480128401, CreateRandomData); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_RandomCountManaged() + { + var value = new Int(37438941); + + foreach (var count in TestCounts) + { + var random = new Random(count); + + Int[] data = new Int[count]; + + foreach (ref Int item in data.AsSpan()) + { + item = new Int(random.Next()); + } + + // Fill at least 20% of the items with a matching value + int minimum = count / 20; + + for (int i = 0; i < minimum; i++) + { + data[random.Next(0, count)] = value; + } + + int result = data.Count(value); + int expected = CountWithForeach(data, value); + + Assert.AreEqual(result, expected, $"Failed {typeof(Int)} test with count {count}: got {result} instead of {expected}"); + } + } + + // Dummy type to test the managed code path of the API + private sealed class Int : IEquatable + { + private int Value { get; } + + public Int(int value) => Value = value; + + public bool Equals(Int other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Value == other.Value; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || (obj is Int other && Equals(other)); + } + + public override int GetHashCode() + { + return this.Value; + } + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_FilledCount8() + { + TestForType((byte)123, (count, _) => CreateFilledData(count, (byte)123)); + TestForType((sbyte)123, (count, _) => CreateFilledData(count, (sbyte)123)); + TestForType(true, (count, _) => CreateFilledData(count, true)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_FilledCount16() + { + TestForType((ushort)4712, (count, _) => CreateFilledData(count, (ushort)4712)); + TestForType((short)4712, (count, _) => CreateFilledData(count, (short)4712)); + TestForType((char)4712, (count, _) => CreateFilledData(count, (char)4712)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1139", Justification = "Easier to tell types apart at a glance")] + public void Test_ReadOnlySpanExtensions_FilledCount32() + { + TestForType((int)37438941, (count, _) => CreateFilledData(count, (int)37438941)); + TestForType((uint)37438941, (count, _) => CreateFilledData(count, (uint)37438941)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1139", Justification = "Easier to tell types apart at a glance")] + public void Test_ReadOnlySpanExtensions_FilledCount64() + { + TestForType((long)47128480128401, (count, _) => CreateFilledData(count, (long)47128480128401)); + TestForType((ulong)47128480128401, (count, _) => CreateFilledData(count, (ulong)47128480128401)); + } + + /// + /// Performs a test for a specified type. + /// + /// The type to test. + /// The target value to look for. + /// The function to use to create random data. + private static void TestForType(T value, Func provider) + where T : unmanaged, IEquatable + { + foreach (var count in TestCounts) + { + T[] data = provider(count, value); + + int result = data.Count(value); + int expected = CountWithForeach(data, value); + + Assert.AreEqual(result, expected, $"Failed {typeof(T)} test with count {count}: got {result} instead of {expected}"); + } + } + + /// + /// Counts the number of occurrences of a given value into a target instance. + /// + /// The type of items to count. + /// The input instance to read. + /// The value to look for. + /// The number of occurrences of in . + [Pure] + private static int CountWithForeach(ReadOnlySpan span, T value) + where T : IEquatable + { + int count = 0; + + foreach (var item in span) + { + if (item.Equals(value)) + { + count++; + } + } + + return count; + } + + /// + /// Creates a random array filled with random data. + /// + /// The type of items to put in the array. + /// The number of array items to create. + /// The value to look for. + /// An array of random elements. + [Pure] + private static T[] CreateRandomData(int count, T value) + where T : unmanaged + { + var random = new Random(count); + + T[] data = new T[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + // Fill at least 20% of the items with a matching value + int minimum = count / 20; + + for (int i = 0; i < minimum; i++) + { + data[random.Next(0, count)] = value; + } + + return data; + } + + /// + /// Creates a array filled with a given value. + /// + /// The type of items to put in the array. + /// The number of array items to create. + /// The value to use to populate the array. + /// An array of elements. + [Pure] + private static T[] CreateFilledData(int count, T value) + where T : unmanaged + { + T[] data = new T[count]; + + data.AsSpan().Fill(value); + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs new file mode 100644 index 00000000000..d0f719aaaaa --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs @@ -0,0 +1,130 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public partial class Test_ReadOnlySpanExtensions + { + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_DangerousGetReference() + { + ReadOnlySpan data = CreateRandomData(12, default).AsSpan(); + + ref int r0 = ref data.DangerousGetReference(); + ref int r1 = ref Unsafe.AsRef(data[0]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero() + { + ReadOnlySpan data = CreateRandomData(12, default).AsSpan(); + + ref int r0 = ref data.DangerousGetReference(); + ref int r1 = ref data.DangerousGetReferenceAt(0); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Index() + { + ReadOnlySpan data = CreateRandomData(12, default).AsSpan(); + + ref int r0 = ref data.DangerousGetReferenceAt(5); + ref int r1 = ref Unsafe.AsRef(data[5]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "List of value tuple")] + public void Test_ReadOnlySpanExtensions_Enumerate() + { + ReadOnlySpan data = CreateRandomData(12, default).AsSpan(); + + List<(int Index, int Value)> values = new List<(int, int)>(); + + foreach (var item in data.Enumerate()) + { + values.Add(item); + } + + Assert.AreEqual(values.Count, data.Length); + + for (int i = 0; i < data.Length; i++) + { + Assert.AreEqual(data[i], values[i].Value); + Assert.AreEqual(i, values[i].Index); + } + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_Tokenize_Empty() + { + string text = string.Empty; + + var result = new List(); + + foreach (var token in text.AsSpan().Tokenize(',')) + { + result.Add(new string(token.ToArray())); + } + + var tokens = text.Split(','); + + CollectionAssert.AreEqual(result, tokens); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_Tokenize_Csv() + { + string text = "name,surname,city,year,profession,age"; + + var result = new List(); + + foreach (var token in text.AsSpan().Tokenize(',')) + { + result.Add(new string(token.ToArray())); + } + + var tokens = text.Split(','); + + CollectionAssert.AreEqual(result, tokens); + } + + [TestCategory("ReadOnlySpanExtensions")] + [TestMethod] + public void Test_ReadOnlySpanExtensions_Tokenize_CsvWithMissingValues() + { + string text = ",name,,city,,,profession,,age,,"; + + var result = new List(); + + foreach (var token in text.AsSpan().Tokenize(',')) + { + result.Add(new string(token.ToArray())); + } + + var tokens = text.Split(','); + + CollectionAssert.AreEqual(result, tokens); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs new file mode 100644 index 00000000000..be1e3398443 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpanExtensions.cs @@ -0,0 +1,79 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_SpanExtensions + { + [TestCategory("SpanExtensions")] + [TestMethod] + public void Test_SpanExtensions_DangerousGetReference() + { + Span data = new[] { 1, 2, 3, 4, 5, 6, 7 }; + + ref int r0 = ref Unsafe.AsRef(data.DangerousGetReference()); + ref int r1 = ref Unsafe.AsRef(data[0]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("SpanExtensions")] + [TestMethod] + public void Test_SpanExtensions_DangerousGetReferenceAt_Zero() + { + Span data = new[] { 1, 2, 3, 4, 5, 6, 7 }; + + ref int r0 = ref Unsafe.AsRef(data.DangerousGetReference()); + ref int r1 = ref Unsafe.AsRef(data.DangerousGetReferenceAt(0)); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("SpanExtensions")] + [TestMethod] + public void Test_SpanExtensions_DangerousGetReferenceAt_Index() + { + Span data = new[] { 1, 2, 3, 4, 5, 6, 7 }; + + ref int r0 = ref Unsafe.AsRef(data.DangerousGetReferenceAt(5)); + ref int r1 = ref Unsafe.AsRef(data[5]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("SpanExtensions")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "List of value tuple")] + public void Test_SpanExtensions_Enumerate() + { + Span data = new[] { 1, 2, 3, 4, 5, 6, 7 }; + + List<(int Index, int Value)> values = new List<(int, int)>(); + + foreach (var item in data.Enumerate()) + { + values.Add((item.Index, item.Value)); + + item.Value = item.Index * 10; + } + + Assert.AreEqual(values.Count, data.Length); + + for (int i = 0; i < data.Length; i++) + { + Assert.AreEqual(data[i], i * 10); + Assert.AreEqual(i, values[i].Index); + Assert.AreEqual(i + 1, values[i].Value); + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpinLockExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpinLockExtensions.cs new file mode 100644 index 00000000000..bda99403074 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_SpinLockExtensions.cs @@ -0,0 +1,74 @@ +// 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.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_SpinLockExtensions + { + [TestCategory("SpinLockExtensions")] + [TestMethod] + public unsafe void Test_ArrayExtensions_Pointer() + { + SpinLock spinLock = default; + SpinLock* p = &spinLock; + + int sum = 0; + + Parallel.For(0, 1000, i => + { + for (int j = 0; j < 10; j++) + { + using (SpinLockExtensions.Enter(p)) + { + sum++; + } + } + }); + + Assert.AreEqual(sum, 1000 * 10); + } + + [TestCategory("SpinLockExtensions")] + [TestMethod] + public void Test_ArrayExtensions_Ref() + { + var spinLockOwner = new SpinLockOwner(); + + int sum = 0; + + Parallel.For(0, 1000, i => + { + for (int j = 0; j < 10; j++) + { +#if NETCOREAPP2_1 || WINDOWS_UWP + using (SpinLockExtensions.Enter(spinLockOwner, ref spinLockOwner.Lock)) +#else + using (spinLockOwner.Lock.Enter()) +#endif + { + sum++; + } + } + }); + + Assert.AreEqual(sum, 1000 * 10); + } + + /// + /// A dummy model that owns a object. + /// + private sealed class SpinLockOwner + { + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401", Justification = "Quick ref access for tests")] + public SpinLock Lock; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_StringExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_StringExtensions.cs new file mode 100644 index 00000000000..a4403375426 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_StringExtensions.cs @@ -0,0 +1,51 @@ +// 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 Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_StringExtensions + { + [TestCategory("StringExtensions")] + [TestMethod] + public void Test_StringExtensions_DangerousGetReference() + { + string text = "Hello, world!"; + + ref char r0 = ref text.DangerousGetReference(); + ref char r1 = ref Unsafe.AsRef(text.AsSpan()[0]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("StringExtensions")] + [TestMethod] + public void Test_StringExtensions_DangerousGetReferenceAt_Zero() + { + string text = "Hello, world!"; + + ref char r0 = ref text.DangerousGetReference(); + ref char r1 = ref text.DangerousGetReferenceAt(0); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + + [TestCategory("StringExtensions")] + [TestMethod] + public void Test_StringExtensions_DangerousGetReferenceAt_Index() + { + string text = "Hello, world!"; + + ref char r0 = ref text.DangerousGetReferenceAt(5); + ref char r1 = ref Unsafe.AsRef(text.AsSpan()[5]); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_BitHelper.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_BitHelper.cs new file mode 100644 index 00000000000..f9a5f2e3c1d --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_BitHelper.cs @@ -0,0 +1,67 @@ +// 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.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Extensions +{ + [TestClass] + public class Test_BitHelper + { + [TestCategory("BitHelper")] + [TestMethod] + public void Test_BitHelper_HasFlag_UInt32() + { + uint value = 0b10_1001110101_0110010010_1100100010; + + Assert.IsFalse(BitHelper.HasFlag(value, 0)); + Assert.IsTrue(BitHelper.HasFlag(value, 1)); + Assert.IsFalse(BitHelper.HasFlag(value, 2)); + Assert.IsTrue(BitHelper.HasFlag(value, 9)); + Assert.IsFalse(BitHelper.HasFlag(value, 10)); + Assert.IsFalse(BitHelper.HasFlag(value, 30)); + Assert.IsTrue(BitHelper.HasFlag(value, 31)); + } + + [TestCategory("BitHelper")] + [TestMethod] + public void Test_BitHelper_SetFlag_UInt32() + { + Assert.AreEqual(0b1u, BitHelper.SetFlag(0u, 0, true)); + Assert.AreEqual(4u, BitHelper.SetFlag(4u, 1, false)); + Assert.AreEqual(0b110u, BitHelper.SetFlag(2u, 2, true)); + Assert.AreEqual(unchecked((uint)int.MinValue), BitHelper.SetFlag(0u, 31, true)); + } + + [TestCategory("BitHelper")] + [TestMethod] + public void Test_UInt64Extensions_HasFlag() + { + ulong value = 0b10_1001110101_0110010010_1100100010; + + value |= 1ul << 63; + + Assert.IsFalse(BitHelper.HasFlag(value, 0)); + Assert.IsTrue(BitHelper.HasFlag(value, 1)); + Assert.IsFalse(BitHelper.HasFlag(value, 2)); + Assert.IsTrue(BitHelper.HasFlag(value, 9)); + Assert.IsFalse(BitHelper.HasFlag(value, 10)); + Assert.IsFalse(BitHelper.HasFlag(value, 30)); + Assert.IsTrue(BitHelper.HasFlag(value, 31)); + Assert.IsTrue(BitHelper.HasFlag(value, 63)); + } + + [TestCategory("BitHelper")] + [TestMethod] + public void Test_UInt64Extensions_SetFlag() + { + Assert.AreEqual(0b1ul, BitHelper.SetFlag(0u, 0, true)); + Assert.AreEqual(4ul, BitHelper.SetFlag(4u, 1, false)); + Assert.AreEqual(0b110ul, BitHelper.SetFlag(2u, 2, true)); + Assert.AreEqual(1ul << 31, BitHelper.SetFlag(0ul, 31, true)); + Assert.AreEqual(1ul << 63, BitHelper.SetFlag(0ul, 63, true)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs new file mode 100644 index 00000000000..8dd6c0e2cfa --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs @@ -0,0 +1,129 @@ +// 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.Diagnostics.Contracts; +using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_HashCodeOfT + { + /// + /// Gets the list of counts to test the extension for + /// + private static ReadOnlySpan TestCounts => new[] { 0, 1, 7, 128, 255, 256, short.MaxValue, short.MaxValue + 1, 123_938, 1_678_922, 71_890_819 }; + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount8() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount16() + { + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount32() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorSupportedTypes_TestRepeatCount64() + { + TestForType(); + TestForType(); + TestForType(); + } + + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat() + { + TestForType(); + } + +#if NETCOREAPP3_0 + [TestCategory("HashCodeOfT")] + [TestMethod] + public void Test_HashCodeOfT_ManagedType_TestRepeat() + { + var random = new Random(); + + foreach (var count in TestCounts.Slice(0, 8)) + { + string[] data = new string[count]; + + foreach (ref string text in data.AsSpan()) + { + text = random.NextDouble().ToString("E"); + } + + int hash1 = HashCode.Combine(data); + int hash2 = HashCode.Combine(data); + + Assert.AreEqual(hash1, hash2, $"Failed {typeof(string)} test with count {count}: got {hash1} and then {hash2}"); + } + } +#endif + + /// + /// Performs a test for a specified type. + /// + /// The type to test. + private static void TestForType() + where T : unmanaged, IEquatable + { + foreach (var count in TestCounts) + { + T[] data = CreateRandomData(count); + + int hash1 = HashCode.Combine(data); + int hash2 = HashCode.Combine(data); + + Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}"); + } + } + + /// + /// Creates a random array filled with random data. + /// + /// The type of items to put in the array. + /// The number of array items to create. + /// An array of random elements. + [Pure] + private static T[] CreateRandomData(int count) + where T : unmanaged + { + var random = new Random(count); + + T[] data = new T[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs new file mode 100644 index 00000000000..89f753a4163 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs @@ -0,0 +1,99 @@ +// 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 Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + [TestClass] + public partial class Test_ParallelHelper + { + /// + /// Gets the list of counts to test the For (1D) extensions for + /// + private static ReadOnlySpan TestForCounts => new[] { 0, 1, 7, 128, 255, 256, short.MaxValue, short.MaxValue + 1, 123_938, 1_678_922, 71_890_819 }; + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ForWithIndices() + { + foreach (int count in TestForCounts) + { + int[] data = new int[count]; + + ParallelHelper.For(0, data.Length, new Assigner(data)); + + foreach (var item in data.Enumerate()) + { + if (item.Index != item.Value) + { + Assert.Fail($"Invalid item at position {item.Index}, value was {item.Value}"); + } + } + } + } + +#if NETCOREAPP3_0 + [TestCategory("ParallelHelper")] + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void Test_ParallelHelper_ForInvalidRange_FromEnd() + { + ParallelHelper.For(..^1); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void Test_ParallelHelper_ForInvalidRange_RangeAll() + { + ParallelHelper.For(..); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ForWithRanges() + { + foreach (int count in TestForCounts) + { + int[] data = new int[count]; + + ParallelHelper.For(..data.Length, new Assigner(data)); + + foreach (var item in data.Enumerate()) + { + if (item.Index != item.Value) + { + Assert.Fail($"Invalid item at position {item.Index}, value was {item.Value}"); + } + } + } + } +#endif + + /// + /// A type implementing to initialize an array + /// + private readonly struct Assigner : IAction + { + private readonly int[] array; + + public Assigner(int[] array) => this.array = array; + + /// + public void Invoke(int i) + { + if (this.array[i] != 0) + { + throw new InvalidOperationException($"Invalid target position {i}, was {this.array[i]} instead of 0"); + } + + this.array[i] = i; + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs new file mode 100644 index 00000000000..0a5b2949be1 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs @@ -0,0 +1,113 @@ +// 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.Drawing; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + /// + /// Gets the list of counts to test the For2D extensions for + /// + private static ReadOnlySpan TestFor2DSizes => new[] + { + new Size(0, 0), + new Size(0, 1), + new Size(1, 1), + new Size(3, 3), + new Size(1024, 1024), + new Size(512, 2175), + new Size(4039, 11231) + }; + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_For2DWithIndices() + { + foreach (var size in TestFor2DSizes) + { + int[,] data = new int[size.Height, size.Width]; + + ParallelHelper.For2D(0, size.Height, 0, size.Width, new Assigner2D(data)); + + for (int i = 0; i < size.Height; i++) + { + for (int j = 0; j < size.Width; j++) + { + if (data[i, j] != unchecked(i * 397 ^ j)) + { + Assert.Fail($"Invalid item at position [{i},{j}], value was {data[i, j]} instead of {unchecked(i * 397 ^ j)}"); + } + } + } + } + } + +#if NETCOREAPP3_0 + [TestCategory("ParallelHelper")] + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void Test_ParallelHelper_For2DInvalidRange_FromEnd() + { + ParallelHelper.For2D(..^1, ..4); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void Test_ParallelHelper_For2DInvalidRange_RangeAll() + { + ParallelHelper.For2D(..5, ..); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_For2DWithRanges() + { + foreach (var size in TestFor2DSizes) + { + int[,] data = new int[size.Height, size.Width]; + + ParallelHelper.For2D(..size.Height, ..size.Width, new Assigner2D(data)); + + for (int i = 0; i < size.Height; i++) + { + for (int j = 0; j < size.Width; j++) + { + if (data[i, j] != unchecked(i * 397 ^ j)) + { + Assert.Fail($"Invalid item at position [{i},{j}], value was {data[i, j]} instead of {unchecked(i * 397 ^ j)}"); + } + } + } + } + } +#endif + + /// + /// A type implementing to initialize a 2D array + /// + private readonly struct Assigner2D : IAction2D + { + private readonly int[,] array; + + public Assigner2D(int[,] array) => this.array = array; + + /// + public void Invoke(int i, int j) + { + if (this.array[i, j] != 0) + { + throw new InvalidOperationException($"Invalid target position [{i},{j}], was {this.array[i, j]} instead of 0"); + } + + this.array[i, j] = unchecked(i * 397 ^ j); + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs new file mode 100644 index 00000000000..b53a97b7042 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.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 System.Threading; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + [TestCategory("ParallelHelper")] + [TestMethod] + public unsafe void Test_ParallelHelper_ForEach_In() + { + foreach (int count in TestForCounts) + { + int[] data = CreateRandomData(count); + + int sum = 0; + + ParallelHelper.ForEach(data.AsMemory(), new Summer(&sum)); + + int expected = 0; + + foreach (int n in data) + { + expected += n; + } + + Assert.AreEqual(sum, expected, $"The sum doesn't match, was {sum} instead of {expected}"); + } + } + + /// + /// A type implementing to sum array elements. + /// + private readonly unsafe struct Summer : IInAction + { + private readonly int* ptr; + + public Summer(int* ptr) => this.ptr = ptr; + + /// + public void Invoke(in int i) => Interlocked.Add(ref Unsafe.AsRef(this.ptr), i); + } + + /// + /// Creates a random array filled with random numbers. + /// + /// The number of array items to create. + /// An array of random elements. + [Pure] + private static int[] CreateRandomData(int count) + { + var random = new Random(count); + + int[] data = new int[count]; + + foreach (ref int n in data.AsSpan()) + { + n = random.Next(0, byte.MaxValue); + } + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs new file mode 100644 index 00000000000..cccfbb7ffec --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs @@ -0,0 +1,52 @@ +// 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 Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ForEach_Ref() + { + foreach (int count in TestForCounts) + { + int[] data = CreateRandomData(count); + int[] copy = data.AsSpan().ToArray(); + + foreach (ref int n in copy.AsSpan()) + { + n = unchecked(n * 397); + } + + ParallelHelper.ForEach(data.AsMemory(), new Multiplier(397)); + + for (int i = 0; i < data.Length; i++) + { + if (data[i] != copy[i]) + { + Assert.Fail($"Item #{i} was not a match, was {data[i]} instead of {copy[i]}"); + } + } + } + } + + /// + /// A type implementing to multiply array elements. + /// + private readonly struct Multiplier : IRefAction + { + private readonly int factor; + + public Multiplier(int factor) => this.factor = factor; + + /// + public void Invoke(ref int i) => i = unchecked(i * this.factor); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ThrowExceptions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ThrowExceptions.cs new file mode 100644 index 00000000000..181761af135 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ThrowExceptions.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.Linq; +using Microsoft.Toolkit.HighPerformance.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; + +namespace UnitTests.HighPerformance.Helpers +{ + public partial class Test_ParallelHelper + { + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ParameterName_ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread() + { + try + { + ParallelHelper.For(0, 1, -1); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(ParallelHelper).GetMethods() + where + method.Name == nameof(ParallelHelper.For) && + method.IsGenericMethodDefinition + let typeParams = method.GetGenericArguments() + let normalParams = method.GetParameters() + where + typeParams.Length == 1 && + normalParams.Length == 3 && + normalParams.All(p => p.ParameterType == typeof(int)) + select normalParams[2].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ParameterName_ThrowArgumentOutOfRangeExceptionForStartGreaterThanEnd() + { + try + { + ParallelHelper.For(1, 0); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(ParallelHelper).GetMethods() + where + method.Name == nameof(ParallelHelper.For) && + method.IsGenericMethodDefinition + let typeParams = method.GetGenericArguments() + let normalParams = method.GetParameters() + where + typeParams.Length == 1 && + normalParams.Length == 2 && + normalParams.All(p => p.ParameterType == typeof(int)) + select normalParams[0].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ParameterName_ThrowArgumentOutOfRangeExceptionForTopGreaterThanBottom() + { + try + { + ParallelHelper.For2D(1, 0, 0, 1); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(ParallelHelper).GetMethods() + where + method.Name == nameof(ParallelHelper.For2D) && + method.IsGenericMethodDefinition + let typeParams = method.GetGenericArguments() + let normalParams = method.GetParameters() + where + typeParams.Length == 1 && + normalParams.Length == 4 && + normalParams.All(p => p.ParameterType == typeof(int)) + select normalParams[0].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("ParallelHelper")] + [TestMethod] + public void Test_ParallelHelper_ParameterName_ThrowArgumentOutOfRangeExceptionForLeftGreaterThanRight() + { + try + { + ParallelHelper.For2D(0, 1, 1, 0); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(ParallelHelper).GetMethods() + where + method.Name == nameof(ParallelHelper.For2D) && + method.IsGenericMethodDefinition + let typeParams = method.GetGenericArguments() + let normalParams = method.GetParameters() + where + typeParams.Length == 1 && + normalParams.Length == 4 && + normalParams.All(p => p.ParameterType == typeof(int)) + select normalParams[2].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + /// + /// A dummy type implementing + /// + private readonly struct DummyAction : IAction + { + /// + public void Invoke(int i) + { + } + } + + /// + /// A dummy type implementing + /// + private readonly struct DummyAction2D : IAction2D + { + /// + public void Invoke(int i, int j) + { + } + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IMemoryOwnerStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IMemoryOwnerStream.cs new file mode 100644 index 00000000000..583976da817 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IMemoryOwnerStream.cs @@ -0,0 +1,35 @@ +// 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.IO; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Streams +{ + [TestClass] + public class Test_IMemoryOwnerStream + { + [TestCategory("IMemoryOwnerStream")] + [TestMethod] + public void Test_IMemoryOwnerStream_Lifecycle() + { + MemoryOwner buffer = MemoryOwner.Allocate(100); + + Stream stream = buffer.AsStream(); + + Assert.IsTrue(stream.CanRead); + Assert.IsTrue(stream.CanSeek); + Assert.IsTrue(stream.CanWrite); + Assert.AreEqual(stream.Length, buffer.Length); + Assert.AreEqual(stream.Position, 0); + + stream.Dispose(); + + Assert.ThrowsException(() => buffer.Memory); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.ThrowExceptions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.ThrowExceptions.cs new file mode 100644 index 00000000000..621bef47fbc --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.ThrowExceptions.cs @@ -0,0 +1,182 @@ +// 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.IO; +using System.Linq; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Streams +{ + public partial class Test_MemoryStream + { + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentExceptionForPosition() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Position = -1; + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + Assert.AreEqual(e.ParamName, nameof(Stream.Position)); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentExceptionForSeekOrigin() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Seek(0, (SeekOrigin)int.MinValue); + } + catch (ArgumentException e) when (e.GetType() == typeof(ArgumentException)) + { + var method = stream.GetType().GetMethod(nameof(Stream.Seek)); + var name = method!.GetParameters()[1].Name; + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentNullExceptionForNullBuffer() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Write(null, 0, 10); + } + catch (ArgumentNullException e) when (e.GetType() == typeof(ArgumentNullException)) + { + var name = ( + from method in typeof(Stream).GetMethods() + where method.Name == nameof(Stream.Write) + let normalParams = method.GetParameters() + where + normalParams.Length == 3 && + normalParams[0].ParameterType == typeof(byte[]) && + normalParams[1].ParameterType == typeof(int) && + normalParams[2].ParameterType == typeof(int) + select normalParams[0].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentOutOfRangeExceptionForNegativeOffset() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Write(new byte[1], -1, 10); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(Stream).GetMethods() + where method.Name == nameof(Stream.Write) + let normalParams = method.GetParameters() + where + normalParams.Length == 3 && + normalParams[0].ParameterType == typeof(byte[]) && + normalParams[1].ParameterType == typeof(int) && + normalParams[2].ParameterType == typeof(int) + select normalParams[1].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentOutOfRangeExceptionForNegativeCount() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Write(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException e) when (e.GetType() == typeof(ArgumentOutOfRangeException)) + { + var name = ( + from method in typeof(Stream).GetMethods() + where method.Name == nameof(Stream.Write) + let normalParams = method.GetParameters() + where + normalParams.Length == 3 && + normalParams[0].ParameterType == typeof(byte[]) && + normalParams[1].ParameterType == typeof(int) && + normalParams[2].ParameterType == typeof(int) + select normalParams[2].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ParameterName_ThrowArgumentExceptionForExceededBufferSize() + { + var stream = new byte[10].AsMemory().AsStream(); + + try + { + stream.Write(new byte[1], 0, 10); + } + catch (ArgumentException e) when (e.GetType() == typeof(ArgumentException)) + { + var name = ( + from method in typeof(Stream).GetMethods() + where method.Name == nameof(Stream.Write) + let normalParams = method.GetParameters() + where + normalParams.Length == 3 && + normalParams[0].ParameterType == typeof(byte[]) && + normalParams[1].ParameterType == typeof(int) && + normalParams[2].ParameterType == typeof(int) + select normalParams[0].Name).Single(); + + Assert.AreEqual(e.ParamName, name); + + return; + } + + Assert.Fail("Failed to raise correct exception"); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs new file mode 100644 index 00000000000..b7c68c9a81e --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs @@ -0,0 +1,252 @@ +// 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.Diagnostics.Contracts; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Streams +{ + [TestClass] + public partial class Test_MemoryStream + { + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_Lifecycle() + { + Memory memory = new byte[100]; + + Stream stream = memory.AsStream(); + + Assert.IsTrue(stream.CanRead); + Assert.IsTrue(stream.CanSeek); + Assert.IsTrue(stream.CanWrite); + Assert.AreEqual(stream.Length, memory.Length); + Assert.AreEqual(stream.Position, 0); + + stream.Dispose(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + Assert.ThrowsException(() => stream.Length); + Assert.ThrowsException(() => stream.Position); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_Seek() + { + Stream stream = new byte[100].AsMemory().AsStream(); + + Assert.AreEqual(stream.Position, 0); + + stream.Position = 42; + + Assert.AreEqual(stream.Position, 42); + + Assert.ThrowsException(() => stream.Position = -1); + Assert.ThrowsException(() => stream.Position = 120); + + stream.Seek(0, SeekOrigin.Begin); + + Assert.ThrowsException(() => stream.Seek(-1, SeekOrigin.Begin)); + Assert.ThrowsException(() => stream.Seek(120, SeekOrigin.Begin)); + + Assert.AreEqual(stream.Position, 0); + + stream.Seek(-1, SeekOrigin.End); + + Assert.ThrowsException(() => stream.Seek(20, SeekOrigin.End)); + Assert.ThrowsException(() => stream.Seek(-120, SeekOrigin.End)); + + Assert.AreEqual(stream.Position, stream.Length - 1); + + stream.Seek(42, SeekOrigin.Begin); + stream.Seek(20, SeekOrigin.Current); + stream.Seek(-30, SeekOrigin.Current); + + Assert.ThrowsException(() => stream.Seek(-64, SeekOrigin.Current)); + Assert.ThrowsException(() => stream.Seek(80, SeekOrigin.Current)); + + Assert.AreEqual(stream.Position, 32); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ReadWrite_Array() + { + Stream stream = new byte[100].AsMemory().AsStream(); + + byte[] data = CreateRandomData(64); + + stream.Write(data, 0, data.Length); + + Assert.AreEqual(stream.Position, data.Length); + + stream.Position = 0; + + byte[] result = new byte[data.Length]; + + int bytesRead = stream.Read(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.AsSpan().SequenceEqual(result)); + + Assert.ThrowsException(() => stream.Write(null, 0, 10)); + Assert.ThrowsException(() => stream.Write(data, -1, 10)); + Assert.ThrowsException(() => stream.Write(data, 200, 10)); + Assert.ThrowsException(() => stream.Write(data, 0, -24)); + Assert.ThrowsException(() => stream.Write(data, 0, 200)); + + stream.Dispose(); + + Assert.ThrowsException(() => stream.Write(data, 0, data.Length)); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public async Task Test_MemoryStream_ReadWriteAsync_Array() + { + Stream stream = new byte[100].AsMemory().AsStream(); + + byte[] data = CreateRandomData(64); + + await stream.WriteAsync(data, 0, data.Length); + + Assert.AreEqual(stream.Position, data.Length); + + stream.Position = 0; + + byte[] result = new byte[data.Length]; + + int bytesRead = await stream.ReadAsync(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.AsSpan().SequenceEqual(result)); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(null, 0, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, -1, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 200, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, -24)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, 200)); + + stream.Dispose(); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, data.Length)); + } + + [TestCategory("MemoryStream")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1500", Justification = "Array initialization")] + public void Test_MemoryStream_ReadWriteByte() + { + Stream stream = new byte[4].AsMemory().AsStream(); + + ReadOnlySpan data = stackalloc byte[] { 1, 128, 255, 32 }; + + foreach (var item in data.Enumerate()) + { + Assert.AreEqual(stream.Position, item.Index); + + stream.WriteByte(item.Value); + } + + Assert.AreEqual(stream.Position, data.Length); + + stream.Position = 0; + + Span result = stackalloc byte[4]; + + foreach (ref byte value in result) + { + value = checked((byte)stream.ReadByte()); + } + + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.SequenceEqual(result)); + + Assert.ThrowsException(() => stream.WriteByte(128)); + + int exitCode = stream.ReadByte(); + + Assert.AreEqual(exitCode, -1); + } + +#if NETCOREAPP2_1 || NETCOREAPP3_0 + [TestCategory("MemoryStream")] + [TestMethod] + public void Test_MemoryStream_ReadWrite_Span() + { + Stream stream = new byte[100].AsMemory().AsStream(); + + Memory data = CreateRandomData(64); + + stream.Write(data.Span); + + Assert.AreEqual(stream.Position, data.Length); + + stream.Position = 0; + + Span result = new byte[data.Length]; + + int bytesRead = stream.Read(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + } + + [TestCategory("MemoryStream")] + [TestMethod] + public async Task Test_MemoryStream_ReadWriteAsync_Memory() + { + Stream stream = new byte[100].AsMemory().AsStream(); + + Memory data = CreateRandomData(64); + + await stream.WriteAsync(data); + + Assert.AreEqual(stream.Position, data.Length); + + stream.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = await stream.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } +#endif + + /// + /// Creates a random array filled with random data. + /// + /// The number of array items to create. + /// The returned random array. + [Pure] + private static byte[] CreateRandomData(int count) + { + var random = new Random(DateTime.Now.Ticks.GetHashCode()); + + byte[] data = new byte[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + return data; + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Test_Box{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Test_Box{T}.cs new file mode 100644 index 00000000000..81f93784c82 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Test_Box{T}.cs @@ -0,0 +1,104 @@ +// 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 Microsoft.Toolkit.HighPerformance; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_BoxOfT + { + [TestCategory("BoxOfT")] + [TestMethod] + public void Test_BoxOfT_PrimitiveTypes() + { + Test(true, false); + Test(27, 254); + Test('a', '$'); + Test(4221124, 1241241); + Test(3.14f, 2342.222f); + Test(8394324ul, 1343431241ul); + Test(184013.234324, 14124.23423); + } + + [TestCategory("BoxOfT")] + [TestMethod] + public void Test_BoxOfT_OtherTypes() + { + Test(DateTime.Now, DateTime.FromBinary(278091429014)); + Test(Guid.NewGuid(), Guid.NewGuid()); + } + + internal struct TestStruct : IEquatable + { + public int Number; + public char Character; + public string Text; + + /// + public bool Equals(TestStruct other) + { + return + this.Number == other.Number && + this.Character == other.Character && + this.Text == other.Text; + } + } + + [TestCategory("BoxOfT")] + [TestMethod] + public void TestBoxOfT_CustomStruct() + { + var a = new TestStruct { Number = 42, Character = 'a', Text = "Hello" }; + var b = new TestStruct { Number = 38293, Character = 'z', Text = "World" }; + + Test(a, b); + } + + /// + /// Tests the type for a given pair of values. + /// + /// The type to test. + /// The initial value. + /// The new value to assign and test. + private static void Test(T value, T test) + where T : struct, IEquatable + { + Box box = value; + + Assert.AreEqual(box.GetReference(), value); + Assert.AreEqual(box.ToString(), value.ToString()); + Assert.AreEqual(box.GetHashCode(), value.GetHashCode()); + + object obj = value; + + bool success = Box.TryGetFrom(obj, out box); + + Assert.IsTrue(success); + Assert.IsTrue(ReferenceEquals(obj, box)); + Assert.IsNotNull(box); + Assert.AreEqual(box.GetReference(), value); + Assert.AreEqual(box.ToString(), value.ToString()); + Assert.AreEqual(box.GetHashCode(), value.GetHashCode()); + + box = Box.DangerousGetFrom(obj); + + Assert.IsTrue(ReferenceEquals(obj, box)); + Assert.AreEqual(box.GetReference(), value); + Assert.AreEqual(box.ToString(), value.ToString()); + Assert.AreEqual(box.GetHashCode(), value.GetHashCode()); + + box.GetReference() = test; + + Assert.AreEqual(box.GetReference(), test); + Assert.AreEqual(box.ToString(), test.ToString()); + Assert.AreEqual(box.GetHashCode(), test.GetHashCode()); + Assert.AreEqual(obj, test); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableReadOnlyRef{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableReadOnlyRef{T}.cs new file mode 100644 index 00000000000..ef72e305f80 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableReadOnlyRef{T}.cs @@ -0,0 +1,51 @@ +// 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 NETCOREAPP3_0 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_NullableReadOnlyRefOfT + { + [TestCategory("NullableReadOnlyRefOfT")] + [TestMethod] + public void Test_NullableReadOnlyRefOfT_CreateNullableReadOnlyRefOfT_Ok() + { + int value = 1; + var reference = new NullableReadOnlyRef(value); + + Assert.IsTrue(reference.HasValue); + Assert.IsTrue(Unsafe.AreSame(ref value, ref Unsafe.AsRef(reference.Value))); + } + + [TestCategory("NullableReadOnlyRefOfT")] + [TestMethod] + public void Test_NullableReadOnlyRefOfT_CreateNullableReadOnlyRefOfT_Null() + { + NullableReadOnlyRef reference = default; + + Assert.IsFalse(reference.HasValue); + } + + [TestCategory("NullableReadOnlyRefOfT")] + [TestMethod] + [ExpectedException(typeof(NullReferenceException))] + public void Test_NullableReadOnlyRefOfT_CreateNullableReadOnlyRefOfT_Null_Exception() + { + NullableReadOnlyRef reference = default; + + _ = reference.Value; + } + } +} + +#endif \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableRef{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableRef{T}.cs new file mode 100644 index 00000000000..f07dd9d6f80 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Test_NullableRef{T}.cs @@ -0,0 +1,55 @@ +// 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 NETCOREAPP3_0 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_NullableRefOfT + { + [TestCategory("NullableRefOfT")] + [TestMethod] + public void Test_RefOfT_CreateNullableRefOfT_Ok() + { + int value = 1; + var reference = new NullableRef(ref value); + + Assert.IsTrue(reference.HasValue); + Assert.IsTrue(Unsafe.AreSame(ref value, ref reference.Value)); + + reference.Value++; + + Assert.AreEqual(value, 2); + } + + [TestCategory("NullableRefOfT")] + [TestMethod] + public void Test_RefOfT_CreateNullableRefOfT_Null() + { + NullableRef reference = default; + + Assert.IsFalse(reference.HasValue); + } + + [TestCategory("NullableRefOfT")] + [TestMethod] + [ExpectedException(typeof(NullReferenceException))] + public void Test_RefOfT_CreateNullableRefOfT_Null_Exception() + { + NullableRef reference = default; + + _ = reference.Value; + } + } +} + +#endif \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Test_ReadOnlyRef{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Test_ReadOnlyRef{T}.cs new file mode 100644 index 00000000000..76df1a23e41 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Test_ReadOnlyRef{T}.cs @@ -0,0 +1,45 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_ReadOnlyRefOfT + { + [TestCategory("ReadOnlyRefOfT")] + [TestMethod] +#if NETCOREAPP2_1 || WINDOWS_UWP + public void Test_RefOfT_CreateRefOfT() + { + var model = new ReadOnlyFieldOwner(); + var reference = new ReadOnlyRef(model, model.Value); + + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(model.Value), ref Unsafe.AsRef(reference.Value))); + } + + /// + /// A dummy model that owns an field. + /// + private sealed class ReadOnlyFieldOwner + { + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401", Justification = "Ref readonly access for tests")] + public readonly int Value = 1; + } +#else + public void Test_RefOfT_CreateRefOfT() + { + int value = 1; + var reference = new ReadOnlyRef(value); + + Assert.IsTrue(Unsafe.AreSame(ref value, ref Unsafe.AsRef(reference.Value))); + } +#endif + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Test_Ref{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Test_Ref{T}.cs new file mode 100644 index 00000000000..00a30c89bec --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Test_Ref{T}.cs @@ -0,0 +1,53 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance +{ + [TestClass] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")] + public class Test_RefOfT + { + [TestCategory("RefOfT")] + [TestMethod] +#if NETCOREAPP2_1 || WINDOWS_UWP + public void Test_RefOfT_CreateRefOfT() + { + var model = new FieldOwner { Value = 1 }; + var reference = new Ref(model, ref model.Value); + + Assert.IsTrue(Unsafe.AreSame(ref model.Value, ref reference.Value)); + + reference.Value++; + + Assert.AreEqual(model.Value, 2); + } + + /// + /// A dummy model that owns an field. + /// + private sealed class FieldOwner + { + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401", Justification = "Quick ref access for tests")] + public int Value; + } +#else + public void Test_RefOfT_CreateRefOfT() + { + int value = 1; + var reference = new Ref(ref value); + + Assert.IsTrue(Unsafe.AreSame(ref value, ref reference.Value)); + + reference.Value++; + + Assert.AreEqual(value, 2); + } +#endif + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems new file mode 100644 index 00000000000..c3b6bccd1c3 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems @@ -0,0 +1,45 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 9b3a94a6-0d29-4523-880b-6938e2efeef7 + + + UnitTests.HighPerformance.Shared + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.shproj b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.shproj new file mode 100644 index 00000000000..cc5d62aef89 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.shproj @@ -0,0 +1,13 @@ + + + + 9b3a94a6-0d29-4523-880b-6938e2efeef7 + 14.0 + + + + + + + + diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/LockScreenLogo.scale-200.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000000..735f57adb5d Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/LockScreenLogo.scale-200.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/SplashScreen.scale-200.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000000..023e7f1feda Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/SplashScreen.scale-200.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square150x150Logo.scale-200.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000000..af49fec1a54 Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square150x150Logo.scale-200.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.scale-200.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000000..ce342a2ec8a Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.scale-200.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000000..f6c02ce97e0 Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/StoreLogo.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/StoreLogo.png new file mode 100644 index 00000000000..7385b56c0e4 Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/StoreLogo.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Assets/Wide310x150Logo.scale-200.png b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000000..288995b397f Binary files /dev/null and b/UnitTests/UnitTests.HighPerformance.UWP/Assets/Wide310x150Logo.scale-200.png differ diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Package.appxmanifest b/UnitTests/UnitTests.HighPerformance.UWP/Package.appxmanifest new file mode 100644 index 00000000000..96031f29ad1 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + UnitTests.HighPerformance.UWP + Microsoft + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Properties/AssemblyInfo.cs b/UnitTests/UnitTests.HighPerformance.UWP/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..d2fa8156284 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +// 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.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UnitTests.HighPerformance.UWP")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UnitTests.HighPerformance.UWP")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyMetadata("TargetPlatform", "UAP")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.UWP/Properties/Default.rd.xml b/UnitTests/UnitTests.HighPerformance.UWP/Properties/Default.rd.xml new file mode 100644 index 00000000000..996a8392ada --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/Properties/Default.rd.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml b/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml new file mode 100644 index 00000000000..7db299100ec --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml @@ -0,0 +1,6 @@ + + + diff --git a/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml.cs b/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml.cs new file mode 100644 index 00000000000..1bde3279655 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/UnitTestApp.xaml.cs @@ -0,0 +1,53 @@ +// 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 Windows.ApplicationModel.Activation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace UnitTests.HighPerformance.UWP +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public sealed partial class App : Application + { + /// + /// Initializes a new instance of the class. + /// + public App() + { + this.InitializeComponent(); + } + + /// + protected override void OnLaunched(LaunchActivatedEventArgs e) + { +#if DEBUG + if (System.Diagnostics.Debugger.IsAttached) + { + this.DebugSettings.EnableFrameRateCounter = true; + } +#endif + + Frame rootFrame = Window.Current.Content as Frame; + + if (rootFrame == null) + { + // Create a Frame to act as the navigation context and navigate to the first page + rootFrame = new Frame(); + + // Place the frame in the current Window + Window.Current.Content = rootFrame; + } + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI(); + + // Ensure the current window is active + Window.Current.Activate(); + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(e.Arguments); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.UWP/UnitTests.HighPerformance.UWP.csproj b/UnitTests/UnitTests.HighPerformance.UWP/UnitTests.HighPerformance.UWP.csproj new file mode 100644 index 00000000000..9457baf56ba --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.UWP/UnitTests.HighPerformance.UWP.csproj @@ -0,0 +1,184 @@ + + + + + Debug + x86 + {5524523E-DB0F-41F7-A0D4-43128422A342} + AppContainerExe + Properties + UnitTests.HighPerformance.UWP + UnitTests.HighPerformance.UWP + en-US + UAP + 10.0.17763.0 + 10.0.16299.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + $(VisualStudioVersion) + false + true + 8.0 + + + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + true + + + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM64 + false + prompt + true + true + + + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM64 + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + PackageReference + + + + + + + + UnitTestApp.xaml + + + + + MSBuild:Compile + Designer + + + + + Designer + + + + + + + + + + + + + + + 6.2.9 + + + 1.4.0 + + + 1.4.0 + + + 4.7.1 + + + + + {7e30d48c-4cd8-47be-b557-10a20391dcc4} + Microsoft.Toolkit.HighPerformance + + + + + 14.0 + + + + \ No newline at end of file diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln index 2a562708db3..a7a3f016cb5 100644 --- a/Windows Community Toolkit.sln +++ b/Windows Community Toolkit.sln @@ -90,10 +90,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GazeInputTest", "GazeInputT EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Uwp.UI.Media", "Microsoft.Toolkit.Uwp.UI.Media\Microsoft.Toolkit.Uwp.UI.Media.csproj", "{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.HighPerformance", "Microsoft.Toolkit.HighPerformance\Microsoft.Toolkit.HighPerformance.csproj", "{7E30D48C-4CD8-47BE-B557-10A20391DCC4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.HighPerformance.NetCore", "UnitTests\UnitTests.HighPerformance.NetCore\UnitTests.HighPerformance.NetCore.csproj", "{D9BDBC68-3D0A-47FC-9C88-0BF769101644}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "UnitTests.HighPerformance.Shared", "UnitTests\UnitTests.HighPerformance.Shared\UnitTests.HighPerformance.Shared.shproj", "{9B3A94A6-0D29-4523-880B-6938E2EFEEF7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HighPerformance", "HighPerformance", "{262CDB74-CF21-47AC-8DD9-CBC33C73B7CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.HighPerformance.UWP", "UnitTests\UnitTests.HighPerformance.UWP\UnitTests.HighPerformance.UWP.csproj", "{5524523E-DB0F-41F7-A0D4-43128422A342}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + UnitTests\UnitTests.HighPerformance.Shared\UnitTests.HighPerformance.Shared.projitems*{5524523e-db0f-41f7-a0d4-43128422a342}*SharedItemsImports = 4 UnitTests\UnitTests.Notifications.Shared\UnitTests.Notifications.Shared.projitems*{982cc826-aacd-4855-9075-430bb6ce40a9}*SharedItemsImports = 13 + UnitTests\UnitTests.HighPerformance.Shared\UnitTests.HighPerformance.Shared.projitems*{9b3a94a6-0d29-4523-880b-6938e2efeef7}*SharedItemsImports = 13 UnitTests\UnitTests.Notifications.Shared\UnitTests.Notifications.Shared.projitems*{bab1caf4-c400-4a7f-a987-c576de63cffd}*SharedItemsImports = 4 + UnitTests\UnitTests.HighPerformance.Shared\UnitTests.HighPerformance.Shared.projitems*{d9bdbc68-3d0a-47fc-9c88-0bf769101644}*SharedItemsImports = 5 UnitTests\UnitTests.Notifications.Shared\UnitTests.Notifications.Shared.projitems*{efa96b3c-857e-4659-b942-6bef7719f4ca}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -786,13 +799,9 @@ Global {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Debug|x86.ActiveCfg = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Debug|x86.Build.0 = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|Any CPU.ActiveCfg = Debug|Any CPU - {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|Any CPU.Build.0 = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|ARM.ActiveCfg = Debug|Any CPU - {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|ARM.Build.0 = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|ARM64.ActiveCfg = Debug|Any CPU - {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|ARM64.Build.0 = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|x64.ActiveCfg = Debug|Any CPU - {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|x64.Build.0 = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Native|x86.ActiveCfg = Debug|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|Any CPU.Build.0 = Release|Any CPU @@ -804,6 +813,89 @@ Global {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x64.Build.0 = Release|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x86.ActiveCfg = Release|Any CPU {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x86.Build.0 = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|ARM.Build.0 = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|ARM64.Build.0 = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|x64.Build.0 = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|x86.Build.0 = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Native|Any CPU.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Native|ARM.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Native|ARM64.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Native|x64.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Native|x86.ActiveCfg = Debug|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|Any CPU.Build.0 = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|ARM.ActiveCfg = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|ARM.Build.0 = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|ARM64.ActiveCfg = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|ARM64.Build.0 = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|x64.ActiveCfg = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|x64.Build.0 = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|x86.ActiveCfg = Release|Any CPU + {7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Release|x86.Build.0 = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|ARM.Build.0 = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|ARM64.Build.0 = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|x64.Build.0 = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Debug|x86.Build.0 = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Native|Any CPU.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Native|ARM.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Native|ARM64.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Native|x64.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Native|x86.ActiveCfg = Debug|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|Any CPU.Build.0 = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|ARM.ActiveCfg = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|ARM.Build.0 = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|ARM64.ActiveCfg = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|ARM64.Build.0 = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|x64.ActiveCfg = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|x64.Build.0 = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|x86.ActiveCfg = Release|Any CPU + {D9BDBC68-3D0A-47FC-9C88-0BF769101644}.Release|x86.Build.0 = Release|Any CPU + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|Any CPU.ActiveCfg = Debug|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|Any CPU.Build.0 = Debug|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM.ActiveCfg = Debug|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM.Build.0 = Debug|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM.Deploy.0 = Debug|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM64.Build.0 = Debug|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x64.ActiveCfg = Debug|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x64.Build.0 = Debug|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x64.Deploy.0 = Debug|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x86.ActiveCfg = Debug|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x86.Build.0 = Debug|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Debug|x86.Deploy.0 = Debug|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Native|Any CPU.ActiveCfg = Release|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Native|ARM.ActiveCfg = Release|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Native|ARM64.ActiveCfg = Release|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Native|x64.ActiveCfg = Release|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Native|x86.ActiveCfg = Release|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|Any CPU.ActiveCfg = Release|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|Any CPU.Build.0 = Release|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM.ActiveCfg = Release|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM.Build.0 = Release|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM.Deploy.0 = Release|ARM + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM64.ActiveCfg = Release|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM64.Build.0 = Release|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|ARM64.Deploy.0 = Release|ARM64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x64.ActiveCfg = Release|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x64.Build.0 = Release|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x64.Deploy.0 = Release|x64 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x86.ActiveCfg = Release|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x86.Build.0 = Release|x86 + {5524523E-DB0F-41F7-A0D4-43128422A342}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -827,6 +919,10 @@ Global {262BB7CE-EF42-4BF7-B90C-107E6CBB57FF} = {096ECFD7-7035-4487-9C87-81DCE9389620} {A122EA02-4DE7-413D-BFBF-AF7DFC668DD6} = {B30036C4-D514-4E5B-A323-587A061772CE} {75F9EE44-3EFA-47BC-AEDD-351B9834A0AF} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC} + {D9BDBC68-3D0A-47FC-9C88-0BF769101644} = {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF} + {9B3A94A6-0D29-4523-880B-6938E2EFEEF7} = {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF} + {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF} = {B30036C4-D514-4E5B-A323-587A061772CE} + {5524523E-DB0F-41F7-A0D4-43128422A342} = {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345}