From d2f0bf7898f057ec6dc9f8c06c216cf9ed81e7e4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Mon, 3 Aug 2020 23:38:52 +0200 Subject: [PATCH 1/8] Reduced memory usage in Count tests --- .../Test_ReadOnlySpanExtensions.Count.cs | 76 ++++++++++++++++--- .../Extensions/Test_ReadOnlySpanExtensions.cs | 12 ++- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs index 3966aac..a3fd7fd 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -3,12 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; +#nullable enable + namespace UnitTests.HighPerformance.Extensions { public partial class Test_ReadOnlySpanExtensions @@ -168,15 +172,15 @@ public void Test_ReadOnlySpanExtensions_FilledCount64() /// <typeparam name="T">The type to test.</typeparam> /// <param name="value">The target value to look for.</param> /// <param name="provider">The function to use to create random data.</param> - private static void TestForType<T>(T value, Func<int, T, T[]> provider) + private static void TestForType<T>(T value, Func<int, T, UnmanagedSpanOwner<T>> provider) where T : unmanaged, IEquatable<T> { foreach (var count in TestCounts) { - T[] data = provider(count, value); + using UnmanagedSpanOwner<T> data = provider(count, value); - int result = data.Count(value); - int expected = CountWithForeach(data, value); + int result = data.Span.Count(value); + int expected = CountWithForeach(data.Span, value); Assert.AreEqual(result, expected, $"Failed {typeof(T)} test with count {count}: got {result} instead of {expected}"); } @@ -214,14 +218,14 @@ private static int CountWithForeach<T>(ReadOnlySpan<T> span, T value) /// <param name="value">The value to look for.</param> /// <returns>An array of random <typeparamref name="T"/> elements.</returns> [Pure] - private static T[] CreateRandomData<T>(int count, T value) + private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count, T value) where T : unmanaged { var random = new Random(count); - T[] data = new T[count]; + UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + foreach (ref byte n in MemoryMarshal.AsBytes(data.Span)) { n = (byte)random.Next(0, byte.MaxValue); } @@ -229,9 +233,11 @@ private static T[] CreateRandomData<T>(int count, T value) // Fill at least 20% of the items with a matching value int minimum = count / 20; + Span<T> span = data.Span; + for (int i = 0; i < minimum; i++) { - data[random.Next(0, count)] = value; + span[random.Next(0, count)] = value; } return data; @@ -245,14 +251,62 @@ private static T[] CreateRandomData<T>(int count, T value) /// <param name="value">The value to use to populate the array.</param> /// <returns>An array of <typeparamref name="T"/> elements.</returns> [Pure] - private static T[] CreateFilledData<T>(int count, T value) + private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value) where T : unmanaged { - T[] data = new T[count]; + UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - data.AsSpan().Fill(value); + data.Span.Fill(value); return data; } + + /// <summary> + /// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory. + /// </summary> + /// <typeparam name="T">The type of items to store in the rented buffers.</typeparam> + private sealed class UnmanagedSpanOwner<T> : IDisposable + where T : unmanaged + { + /// <summary> + /// The size of the current instance + /// </summary> + private readonly int length; + + /// <summary> + /// The underlying <see cref="byte"/> array. + /// </summary> + private byte[]? array; + + /// <summary> + /// Initializes a new instance of the <see cref="UnmanagedSpanOwner{T}"/> class. + /// </summary> + /// <param name="size">The size of the buffer to rent.</param> + public UnmanagedSpanOwner(int size) + { + this.array = ArrayPool<byte>.Shared.Rent(size * Unsafe.SizeOf<T>()); + this.length = size; + } + + /// <inheritdoc/> + public void Dispose() + { + byte[]? array = this.array; + + if (array is null) + { + return; + } + + this.array = null; + + ArrayPool<byte>.Shared.Return(array); + } + + /// <summary> + /// Gets the <see cref="Memory{T}"/> for the current instance. + /// </summary> + public Span<T> Span => this.array.AsSpan().Cast<byte, T>().Slice(0, this.length); + } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs index d3d2010..c431e49 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs @@ -18,7 +18,9 @@ public partial class Test_ReadOnlySpanExtensions [TestMethod] public void Test_ReadOnlySpanExtensions_DangerousGetReference() { - ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan(); + using var owner = CreateRandomData<int>(12, default); + + ReadOnlySpan<int> data = owner.Span; ref int r0 = ref data.DangerousGetReference(); ref int r1 = ref Unsafe.AsRef(data[0]); @@ -30,7 +32,9 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReference() [TestMethod] public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero() { - ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan(); + using var owner = CreateRandomData<int>(12, default); + + ReadOnlySpan<int> data = owner.Span; ref int r0 = ref data.DangerousGetReference(); ref int r1 = ref data.DangerousGetReferenceAt(0); @@ -42,7 +46,9 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero() [TestMethod] public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Index() { - ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan(); + using var owner = CreateRandomData<int>(12, default); + + ReadOnlySpan<int> data = owner.Span; ref int r0 = ref data.DangerousGetReferenceAt(5); ref int r1 = ref Unsafe.AsRef(data[5]); From 917b96acbbc439fa5b58eb2d72ac5ddf3f3c18d1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 01:59:32 +0200 Subject: [PATCH 2/8] Attempt to fix CI with manual memory management --- .../Test_ReadOnlySpanExtensions.Count.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs index a3fd7fd..4be89dd 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; @@ -265,7 +264,7 @@ private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value) /// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory. /// </summary> /// <typeparam name="T">The type of items to store in the rented buffers.</typeparam> - private sealed class UnmanagedSpanOwner<T> : IDisposable + private sealed unsafe class UnmanagedSpanOwner<T> : IDisposable where T : unmanaged { /// <summary> @@ -274,9 +273,9 @@ private sealed class UnmanagedSpanOwner<T> : IDisposable private readonly int length; /// <summary> - /// The underlying <see cref="byte"/> array. + /// The pointer to the underlying <see cref="byte"/> array. /// </summary> - private byte[]? array; + private IntPtr ptr; /// <summary> /// Initializes a new instance of the <see cref="UnmanagedSpanOwner{T}"/> class. @@ -284,29 +283,29 @@ private sealed class UnmanagedSpanOwner<T> : IDisposable /// <param name="size">The size of the buffer to rent.</param> public UnmanagedSpanOwner(int size) { - this.array = ArrayPool<byte>.Shared.Rent(size * Unsafe.SizeOf<T>()); + this.ptr = Marshal.AllocHGlobal(size * Unsafe.SizeOf<T>()); this.length = size; } /// <inheritdoc/> public void Dispose() { - byte[]? array = this.array; + IntPtr ptr = this.ptr; - if (array is null) + if (ptr == IntPtr.Zero) { return; } - this.array = null; + this.ptr = IntPtr.Zero; - ArrayPool<byte>.Shared.Return(array); + Marshal.FreeHGlobal(ptr); } /// <summary> /// Gets the <see cref="Memory{T}"/> for the current instance. /// </summary> - public Span<T> Span => this.array.AsSpan().Cast<byte, T>().Slice(0, this.length); + public Span<T> Span => new Span<T>((void*)this.ptr, this.length); } } } From db7f64f1340985edc9054f50d29cc0bfa31389ba Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 13:08:28 +0200 Subject: [PATCH 3/8] Minor code refactoring --- .../Buffers/Internals/UnmanagedSpanOwner.cs | 54 +++++++++++++++++++ .../Test_ReadOnlySpanExtensions.Count.cs | 50 +---------------- ...UnitTests.HighPerformance.Shared.projitems | 1 + 3 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs new file mode 100644 index 0000000..f821205 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace UnitTests.HighPerformance.Shared.Buffers.Internals +{ + /// <summary> + /// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory. + /// </summary> + /// <typeparam name="T">The type of items to store in the rented buffers.</typeparam> + internal sealed unsafe class UnmanagedSpanOwner<T> : IDisposable + where T : unmanaged + { + /// <summary> + /// The size of the current instance + /// </summary> + private readonly int length; + + /// <summary> + /// The pointer to the underlying <see cref="byte"/> array. + /// </summary> + private IntPtr ptr; + + /// <summary> + /// Initializes a new instance of the <see cref="UnmanagedSpanOwner{T}"/> class. + /// </summary> + /// <param name="size">The size of the buffer to rent.</param> + public UnmanagedSpanOwner(int size) + { + this.ptr = Marshal.AllocHGlobal(size * Unsafe.SizeOf<T>()); + this.length = size; + } + + /// <inheritdoc/> + public void Dispose() + { + IntPtr ptr = this.ptr; + + if (ptr == IntPtr.Zero) + { + return; + } + + this.ptr = IntPtr.Zero; + + Marshal.FreeHGlobal(ptr); + } + + /// <summary> + /// Gets the <see cref="Memory{T}"/> for the current instance. + /// </summary> + public Span<T> Span => new Span<T>((void*)this.ptr, this.length); + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs index 4be89dd..28d5948 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -5,10 +5,10 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; #nullable enable @@ -259,53 +259,5 @@ private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value) return data; } - - /// <summary> - /// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory. - /// </summary> - /// <typeparam name="T">The type of items to store in the rented buffers.</typeparam> - private sealed unsafe class UnmanagedSpanOwner<T> : IDisposable - where T : unmanaged - { - /// <summary> - /// The size of the current instance - /// </summary> - private readonly int length; - - /// <summary> - /// The pointer to the underlying <see cref="byte"/> array. - /// </summary> - private IntPtr ptr; - - /// <summary> - /// Initializes a new instance of the <see cref="UnmanagedSpanOwner{T}"/> class. - /// </summary> - /// <param name="size">The size of the buffer to rent.</param> - public UnmanagedSpanOwner(int size) - { - this.ptr = Marshal.AllocHGlobal(size * Unsafe.SizeOf<T>()); - this.length = size; - } - - /// <inheritdoc/> - public void Dispose() - { - IntPtr ptr = this.ptr; - - if (ptr == IntPtr.Zero) - { - return; - } - - this.ptr = IntPtr.Zero; - - Marshal.FreeHGlobal(ptr); - } - - /// <summary> - /// Gets the <see cref="Memory{T}"/> for the current instance. - /// </summary> - public Span<T> Span => new Span<T>((void*)this.ptr, this.length); - } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems index 06b7503..9539bf3 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems @@ -9,6 +9,7 @@ <Import_RootNamespace>UnitTests.HighPerformance.Shared</Import_RootNamespace> </PropertyGroup> <ItemGroup> + <Compile Include="$(MSBuildThisFileDirectory)Buffers\Internals\UnmanagedSpanOwner.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_MemoryBufferWriter{T}.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_ArrayPoolBufferWriter{T}.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_MemoryOwner{T}.cs" /> From 9af5262458675d3af0a381721aa3da9c77cb4c1b Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 13:16:39 +0200 Subject: [PATCH 4/8] Reduced memory usage in hash code tests --- .../Helpers/Test_HashCode{T}.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs index c82ad26..36817d4 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; namespace UnitTests.HighPerformance.Helpers { @@ -67,19 +68,23 @@ public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat() [TestMethod] public void Test_HashCodeOfT_ManagedType_TestRepeat() { + var localTestCounts = TestCounts.Slice(0, 8); + + // Only rent a single array of the maximum necessary size, to save space + string[] data = new string[localTestCounts[localTestCounts.Length - 1]]; + var random = new Random(); - - foreach (var count in TestCounts.Slice(0, 8)) + foreach (ref string text in data.AsSpan()) { - string[] data = new string[count]; + text = random.NextDouble().ToString("E"); + } - foreach (ref string text in data.AsSpan()) - { - text = random.NextDouble().ToString("E"); - } + foreach (var count in localTestCounts) + { + Span<string> iterationData = data.AsSpan().Slice(0, count); - int hash1 = HashCode<string>.Combine(data); - int hash2 = HashCode<string>.Combine(data); + int hash1 = HashCode<string>.Combine(iterationData); + int hash2 = HashCode<string>.Combine(iterationData); Assert.AreEqual(hash1, hash2, $"Failed {typeof(string)} test with count {count}: got {hash1} and then {hash2}"); } @@ -95,10 +100,10 @@ private static void TestForType<T>() { foreach (var count in TestCounts) { - T[] data = CreateRandomData<T>(count); + using UnmanagedSpanOwner<T> data = CreateRandomData<T>(count); - int hash1 = HashCode<T>.Combine(data); - int hash2 = HashCode<T>.Combine(data); + int hash1 = HashCode<T>.Combine(data.Span); + int hash2 = HashCode<T>.Combine(data.Span); Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}"); } @@ -111,14 +116,14 @@ private static void TestForType<T>() /// <param name="count">The number of array items to create.</param> /// <returns>An array of random <typeparamref name="T"/> elements.</returns> [Pure] - private static T[] CreateRandomData<T>(int count) + private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count) where T : unmanaged { var random = new Random(count); - T[] data = new T[count]; + UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + foreach (ref byte n in MemoryMarshal.AsBytes(data.Span)) { n = (byte)random.Next(0, byte.MaxValue); } From 9ace1401d5a7608e6df4e564005b711f5e0bafaa Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 13:34:19 +0200 Subject: [PATCH 5/8] Added missing file header --- .../Buffers/Internals/UnmanagedSpanOwner.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs index f821205..7decb59 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -31,6 +35,21 @@ public UnmanagedSpanOwner(int size) this.length = size; } + /// <summary> + /// Gets the length of the buffer in use. + /// </summary> + public int Length => this.length; + + /// <summary> + /// Gets a pointer to the start of the buffer in use. + /// </summary> + public T* Ptr => (T*)this.ptr; + + /// <summary> + /// Gets the <see cref="Memory{T}"/> for the current instance. + /// </summary> + public Span<T> Span => new Span<T>((void*)this.ptr, this.length); + /// <inheritdoc/> public void Dispose() { @@ -45,10 +64,5 @@ public void Dispose() Marshal.FreeHGlobal(ptr); } - - /// <summary> - /// Gets the <see cref="Memory{T}"/> for the current instance. - /// </summary> - public Span<T> Span => new Span<T>((void*)this.ptr, this.length); } } From 9dc5481b473cde69a893e41a9d819c1a4524251d Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 13:46:24 +0200 Subject: [PATCH 6/8] Code refactoring to support Memory<T> tests --- .../Buffers/Internals/UnmanagedSpanOwner.cs | 28 ++++++++--- .../Test_ReadOnlySpanExtensions.Count.cs | 10 ++-- .../Helpers/Test_HashCode{T}.cs | 6 +-- .../Helpers/Test_ParallelHelper.For.cs | 43 ++++++++++------ .../Helpers/Test_ParallelHelper.For2D.cs | 50 +++++++++++++------ 5 files changed, 92 insertions(+), 45 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs index 7decb59..70f87a1 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Internals/UnmanagedSpanOwner.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -12,7 +13,7 @@ namespace UnitTests.HighPerformance.Shared.Buffers.Internals /// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory. /// </summary> /// <typeparam name="T">The type of items to store in the rented buffers.</typeparam> - internal sealed unsafe class UnmanagedSpanOwner<T> : IDisposable + internal sealed unsafe class UnmanagedSpanOwner<T> : MemoryManager<T> where T : unmanaged { /// <summary> @@ -45,13 +46,8 @@ public UnmanagedSpanOwner(int size) /// </summary> public T* Ptr => (T*)this.ptr; - /// <summary> - /// Gets the <see cref="Memory{T}"/> for the current instance. - /// </summary> - public Span<T> Span => new Span<T>((void*)this.ptr, this.length); - /// <inheritdoc/> - public void Dispose() + protected override void Dispose(bool disposing) { IntPtr ptr = this.ptr; @@ -64,5 +60,23 @@ public void Dispose() Marshal.FreeHGlobal(ptr); } + + /// <inheritdoc/> + public override Span<T> GetSpan() + { + return new Span<T>((void*)this.ptr, this.length); + } + + /// <inheritdoc/> + public override MemoryHandle Pin(int elementIndex = 0) + { + throw new NotImplementedException(); + } + + /// <inheritdoc/> + public override void Unpin() + { + throw new NotImplementedException(); + } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs index 28d5948..10f1591 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs @@ -178,8 +178,8 @@ private static void TestForType<T>(T value, Func<int, T, UnmanagedSpanOwner<T>> { using UnmanagedSpanOwner<T> data = provider(count, value); - int result = data.Span.Count(value); - int expected = CountWithForeach(data.Span, value); + int result = data.GetSpan().Count(value); + int expected = CountWithForeach(data.GetSpan(), value); Assert.AreEqual(result, expected, $"Failed {typeof(T)} test with count {count}: got {result} instead of {expected}"); } @@ -224,7 +224,7 @@ private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count, T value) UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - foreach (ref byte n in MemoryMarshal.AsBytes(data.Span)) + foreach (ref byte n in MemoryMarshal.AsBytes(data.GetSpan())) { n = (byte)random.Next(0, byte.MaxValue); } @@ -232,7 +232,7 @@ private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count, T value) // Fill at least 20% of the items with a matching value int minimum = count / 20; - Span<T> span = data.Span; + Span<T> span = data.GetSpan(); for (int i = 0; i < minimum; i++) { @@ -255,7 +255,7 @@ private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value) { UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - data.Span.Fill(value); + data.GetSpan().Fill(value); return data; } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs index 36817d4..8682848 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs @@ -102,8 +102,8 @@ private static void TestForType<T>() { using UnmanagedSpanOwner<T> data = CreateRandomData<T>(count); - int hash1 = HashCode<T>.Combine(data.Span); - int hash2 = HashCode<T>.Combine(data.Span); + int hash1 = HashCode<T>.Combine(data.GetSpan()); + int hash2 = HashCode<T>.Combine(data.GetSpan()); Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}"); } @@ -123,7 +123,7 @@ private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count) UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count); - foreach (ref byte n in MemoryMarshal.AsBytes(data.Span)) + foreach (ref byte n in MemoryMarshal.AsBytes(data.GetSpan())) { n = (byte)random.Next(0, byte.MaxValue); } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs index 2fac17d..f1d2b98 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs @@ -6,6 +6,7 @@ using Microsoft.Toolkit.HighPerformance.Extensions; using Microsoft.Toolkit.HighPerformance.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; namespace UnitTests.HighPerformance.Helpers { @@ -19,15 +20,17 @@ public partial class Test_ParallelHelper [TestCategory("ParallelHelper")] [TestMethod] - public void Test_ParallelHelper_ForWithIndices() + public unsafe void Test_ParallelHelper_ForWithIndices() { foreach (int count in TestForCounts) { - int[] data = new int[count]; + using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(count); - ParallelHelper.For(0, data.Length, new Assigner(data)); + data.GetSpan().Clear(); - foreach (var item in data.Enumerate()) + ParallelHelper.For(0, data.Length, new Assigner(data.Length, data.Ptr)); + + foreach (var item in data.GetSpan().Enumerate()) { if (item.Index != item.Value) { @@ -56,15 +59,17 @@ public void Test_ParallelHelper_ForInvalidRange_RangeAll() [TestCategory("ParallelHelper")] [TestMethod] - public void Test_ParallelHelper_ForWithRanges() + public unsafe void Test_ParallelHelper_ForWithRanges() { foreach (int count in TestForCounts) { - int[] data = new int[count]; + using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(count); - ParallelHelper.For(..data.Length, new Assigner(data)); + data.GetSpan().Clear(); - foreach (var item in data.Enumerate()) + ParallelHelper.For(..data.Length, new Assigner(data.Length, data.Ptr)); + + foreach (var item in data.GetSpan().Enumerate()) { if (item.Index != item.Value) { @@ -78,21 +83,31 @@ public void Test_ParallelHelper_ForWithRanges() /// <summary> /// A type implementing <see cref="IAction"/> to initialize an array /// </summary> - private readonly struct Assigner : IAction + private readonly unsafe struct Assigner : IAction { - private readonly int[] array; + private readonly int length; + private readonly int* ptr; - public Assigner(int[] array) => this.array = array; + public Assigner(int length, int* ptr) + { + this.length = length; + this.ptr = ptr; + } /// <inheritdoc/> public void Invoke(int i) { - if (this.array[i] != 0) + if ((uint)i >= (uint)this.length) { - throw new InvalidOperationException($"Invalid target position {i}, was {this.array[i]} instead of 0"); + throw new IndexOutOfRangeException($"The target position was out of range, was {i} and should've been in [0, {this.length})"); } - this.array[i] = i; + if (this.ptr[i] != 0) + { + throw new InvalidOperationException($"Invalid target position {i}, was {this.ptr[i]} instead of 0"); + } + + this.ptr[i] = i; } } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs index 8fccecf..e038e2c 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For2D.cs @@ -6,6 +6,7 @@ using System.Drawing; using Microsoft.Toolkit.HighPerformance.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; namespace UnitTests.HighPerformance.Helpers { @@ -27,21 +28,23 @@ public partial class Test_ParallelHelper [TestCategory("ParallelHelper")] [TestMethod] - public void Test_ParallelHelper_For2DWithIndices() + public unsafe void Test_ParallelHelper_For2DWithIndices() { foreach (var size in TestFor2DSizes) { - int[,] data = new int[size.Height, size.Width]; + using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(size.Height * size.Width); - ParallelHelper.For2D(0, size.Height, 0, size.Width, new Assigner2D(data)); + data.GetSpan().Clear(); + + ParallelHelper.For2D(0, size.Height, 0, size.Width, new Assigner2D(size.Height, size.Width, data.Ptr)); for (int i = 0; i < size.Height; i++) { for (int j = 0; j < size.Width; j++) { - if (data[i, j] != unchecked(i * 397 ^ j)) + if (data.Ptr[(i * size.Width) + j] != unchecked(i * 397 ^ j)) { - Assert.Fail($"Invalid item at position [{i},{j}], value was {data[i, j]} instead of {unchecked(i * 397 ^ j)}"); + Assert.Fail($"Invalid item at position [{i},{j}], value was {data.Ptr[(i * size.Width) + j]} instead of {unchecked(i * 397 ^ j)}"); } } } @@ -67,21 +70,23 @@ public void Test_ParallelHelper_For2DInvalidRange_RangeAll() [TestCategory("ParallelHelper")] [TestMethod] - public void Test_ParallelHelper_For2DWithRanges() + public unsafe void Test_ParallelHelper_For2DWithRanges() { foreach (var size in TestFor2DSizes) { - int[,] data = new int[size.Height, size.Width]; + using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(size.Height * size.Width); - ParallelHelper.For2D(..size.Height, ..size.Width, new Assigner2D(data)); + data.GetSpan().Clear(); + + ParallelHelper.For2D(..size.Height, ..size.Width, new Assigner2D(size.Height, size.Width, data.Ptr)); for (int i = 0; i < size.Height; i++) { for (int j = 0; j < size.Width; j++) { - if (data[i, j] != unchecked(i * 397 ^ j)) + if (data.Ptr[(i * size.Width) + j] != unchecked(i * 397 ^ j)) { - Assert.Fail($"Invalid item at position [{i},{j}], value was {data[i, j]} instead of {unchecked(i * 397 ^ j)}"); + Assert.Fail($"Invalid item at position [{i},{j}], value was {data.Ptr[(i * size.Width) + j]} instead of {unchecked(i * 397 ^ j)}"); } } } @@ -92,21 +97,34 @@ public void Test_ParallelHelper_For2DWithRanges() /// <summary> /// A type implementing <see cref="IAction"/> to initialize a 2D array /// </summary> - private readonly struct Assigner2D : IAction2D + private readonly unsafe struct Assigner2D : IAction2D { - private readonly int[,] array; + private readonly int height; + private readonly int width; + private readonly int* ptr; - public Assigner2D(int[,] array) => this.array = array; + public Assigner2D(int height, int width, int* ptr) + { + this.height = height; + this.width = width; + this.ptr = ptr; + } /// <inheritdoc/> public void Invoke(int i, int j) { - if (this.array[i, j] != 0) + if ((uint)i >= (uint)this.height || + (uint)j >= (uint)this.width) { - throw new InvalidOperationException($"Invalid target position [{i},{j}], was {this.array[i, j]} instead of 0"); + throw new IndexOutOfRangeException($"The target position was invalid, was [{i}, {j}], should've been in [0, {this.height}] and [0, {this.width}]"); } - this.array[i, j] = unchecked(i * 397 ^ j); + if (this.ptr[(i * this.width) + j] != 0) + { + throw new InvalidOperationException($"Invalid target position [{i},{j}], was {this.ptr[(i * this.width) + j]} instead of 0"); + } + + this.ptr[(i * this.width) + j] = unchecked(i * 397 ^ j); } } } From 8cf39a14d7be7e702358da3a3b0b68a0fa1b94af Mon Sep 17 00:00:00 2001 From: Sergio Pedri <sergio0694@live.com> Date: Tue, 4 Aug 2020 13:51:49 +0200 Subject: [PATCH 7/8] Switched to unmanaged memory in all parallel tests --- .../Extensions/Test_ReadOnlySpanExtensions.cs | 6 +++--- .../Helpers/Test_ParallelHelper.ForEach.In.cs | 13 +++++++------ .../Helpers/Test_ParallelHelper.ForEach.Ref.cs | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs index c431e49..80b6b4d 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs @@ -20,7 +20,7 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReference() { using var owner = CreateRandomData<int>(12, default); - ReadOnlySpan<int> data = owner.Span; + ReadOnlySpan<int> data = owner.GetSpan(); ref int r0 = ref data.DangerousGetReference(); ref int r1 = ref Unsafe.AsRef(data[0]); @@ -34,7 +34,7 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero() { using var owner = CreateRandomData<int>(12, default); - ReadOnlySpan<int> data = owner.Span; + ReadOnlySpan<int> data = owner.GetSpan(); ref int r0 = ref data.DangerousGetReference(); ref int r1 = ref data.DangerousGetReferenceAt(0); @@ -48,7 +48,7 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Index() { using var owner = CreateRandomData<int>(12, default); - ReadOnlySpan<int> data = owner.Span; + ReadOnlySpan<int> data = owner.GetSpan(); ref int r0 = ref data.DangerousGetReferenceAt(5); ref int r1 = ref Unsafe.AsRef(data[5]); diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs index b53a97b..b7d49e5 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.In.cs @@ -8,6 +8,7 @@ using System.Threading; using Microsoft.Toolkit.HighPerformance.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; namespace UnitTests.HighPerformance.Helpers { @@ -19,15 +20,15 @@ public unsafe void Test_ParallelHelper_ForEach_In() { foreach (int count in TestForCounts) { - int[] data = CreateRandomData(count); + using UnmanagedSpanOwner<int> data = CreateRandomData(count); int sum = 0; - ParallelHelper.ForEach<int, Summer>(data.AsMemory(), new Summer(&sum)); + ParallelHelper.ForEach<int, Summer>(data.Memory, new Summer(&sum)); int expected = 0; - foreach (int n in data) + foreach (int n in data.GetSpan()) { expected += n; } @@ -55,13 +56,13 @@ public unsafe void Test_ParallelHelper_ForEach_In() /// <param name="count">The number of array items to create.</param> /// <returns>An array of random <see cref="int"/> elements.</returns> [Pure] - private static int[] CreateRandomData(int count) + private static UnmanagedSpanOwner<int> CreateRandomData(int count) { var random = new Random(count); - int[] data = new int[count]; + UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(count); - foreach (ref int n in data.AsSpan()) + foreach (ref int n in data.GetSpan()) { n = random.Next(0, byte.MaxValue); } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs index cccfbb7..0571fae 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.ForEach.Ref.cs @@ -5,6 +5,7 @@ using System; using Microsoft.Toolkit.HighPerformance.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTests.HighPerformance.Shared.Buffers.Internals; namespace UnitTests.HighPerformance.Helpers { @@ -16,21 +17,26 @@ public void Test_ParallelHelper_ForEach_Ref() { foreach (int count in TestForCounts) { - int[] data = CreateRandomData(count); - int[] copy = data.AsSpan().ToArray(); + using UnmanagedSpanOwner<int> data = CreateRandomData(count); + using UnmanagedSpanOwner<int> copy = new UnmanagedSpanOwner<int>(count); - foreach (ref int n in copy.AsSpan()) + data.GetSpan().CopyTo(copy.GetSpan()); + + foreach (ref int n in copy.GetSpan()) { n = unchecked(n * 397); } - ParallelHelper.ForEach(data.AsMemory(), new Multiplier(397)); + ParallelHelper.ForEach(data.Memory, new Multiplier(397)); + + Span<int> dataSpan = data.GetSpan(); + Span<int> copySpan = copy.GetSpan(); for (int i = 0; i < data.Length; i++) { - if (data[i] != copy[i]) + if (dataSpan[i] != copySpan[i]) { - Assert.Fail($"Item #{i} was not a match, was {data[i]} instead of {copy[i]}"); + Assert.Fail($"Item #{i} was not a match, was {dataSpan[i]} instead of {copySpan[i]}"); } } } From 6f4b9c9f9fa750afa826a05c45488101704bffd0 Mon Sep 17 00:00:00 2001 From: Kyaa Dost <35208324+Kyaa-dost@users.noreply.github.com> Date: Wed, 5 Aug 2020 17:25:36 -0700 Subject: [PATCH 8/8] Update Microsoft.Toolkit.HighPerformance.csproj --- .../Microsoft.Toolkit.HighPerformance.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj index f1d961a..a0d6bcb 100644 --- a/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj +++ b/Microsoft.Toolkit.HighPerformance/Microsoft.Toolkit.HighPerformance.csproj @@ -78,9 +78,9 @@ <!-- NETCORE_RUNTIME: to avoid issues with APIs that assume a specific memory layout, we define a .NET Core runtime constant to indicate the either .NET Core 2.1 or .NET Core 3.1. These are - runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays + runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays, and 2D arrays). We can use this constant to make sure that APIs that are exclusively available - for .NET Standard targets do not make any assumtpion of any internals of the runtime being + for .NET Standard targets do not make any assumption of any internals of the runtime being actually used by the consumers. --> <DefineConstants>NETSTANDARD2_1_OR_GREATER;SPAN_RUNTIME_SUPPORT;NETCORE_RUNTIME</DefineConstants> </PropertyGroup>