Skip to content

Commit e4da174

Browse files
authored
Merge pull request #3408 from Sergio0694/bugfix/ci-memory-usage
Reduced memory usage in HighPerformance tests
2 parents bdc6240 + 6551f9e commit e4da174

9 files changed

+210
-71
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Buffers;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
9+
10+
namespace UnitTests.HighPerformance.Shared.Buffers.Internals
11+
{
12+
/// <summary>
13+
/// An owner for a buffer of an unmanaged type, recycling <see cref="byte"/> arrays to save memory.
14+
/// </summary>
15+
/// <typeparam name="T">The type of items to store in the rented buffers.</typeparam>
16+
internal sealed unsafe class UnmanagedSpanOwner<T> : MemoryManager<T>
17+
where T : unmanaged
18+
{
19+
/// <summary>
20+
/// The size of the current instance
21+
/// </summary>
22+
private readonly int length;
23+
24+
/// <summary>
25+
/// The pointer to the underlying <see cref="byte"/> array.
26+
/// </summary>
27+
private IntPtr ptr;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="UnmanagedSpanOwner{T}"/> class.
31+
/// </summary>
32+
/// <param name="size">The size of the buffer to rent.</param>
33+
public UnmanagedSpanOwner(int size)
34+
{
35+
this.ptr = Marshal.AllocHGlobal(size * Unsafe.SizeOf<T>());
36+
this.length = size;
37+
}
38+
39+
/// <summary>
40+
/// Gets the length of the buffer in use.
41+
/// </summary>
42+
public int Length => this.length;
43+
44+
/// <summary>
45+
/// Gets a pointer to the start of the buffer in use.
46+
/// </summary>
47+
public T* Ptr => (T*)this.ptr;
48+
49+
/// <inheritdoc/>
50+
protected override void Dispose(bool disposing)
51+
{
52+
IntPtr ptr = this.ptr;
53+
54+
if (ptr == IntPtr.Zero)
55+
{
56+
return;
57+
}
58+
59+
this.ptr = IntPtr.Zero;
60+
61+
Marshal.FreeHGlobal(ptr);
62+
}
63+
64+
/// <inheritdoc/>
65+
public override Span<T> GetSpan()
66+
{
67+
return new Span<T>((void*)this.ptr, this.length);
68+
}
69+
70+
/// <inheritdoc/>
71+
public override MemoryHandle Pin(int elementIndex = 0)
72+
{
73+
throw new NotImplementedException();
74+
}
75+
76+
/// <inheritdoc/>
77+
public override void Unpin()
78+
{
79+
throw new NotImplementedException();
80+
}
81+
}
82+
}

UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.Count.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using System.Runtime.InteropServices;
99
using Microsoft.Toolkit.HighPerformance.Extensions;
1010
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using UnitTests.HighPerformance.Shared.Buffers.Internals;
12+
13+
#nullable enable
1114

1215
namespace UnitTests.HighPerformance.Extensions
1316
{
@@ -168,15 +171,15 @@ public void Test_ReadOnlySpanExtensions_FilledCount64()
168171
/// <typeparam name="T">The type to test.</typeparam>
169172
/// <param name="value">The target value to look for.</param>
170173
/// <param name="provider">The function to use to create random data.</param>
171-
private static void TestForType<T>(T value, Func<int, T, T[]> provider)
174+
private static void TestForType<T>(T value, Func<int, T, UnmanagedSpanOwner<T>> provider)
172175
where T : unmanaged, IEquatable<T>
173176
{
174177
foreach (var count in TestCounts)
175178
{
176-
T[] data = provider(count, value);
179+
using UnmanagedSpanOwner<T> data = provider(count, value);
177180

178-
int result = data.Count(value);
179-
int expected = CountWithForeach(data, value);
181+
int result = data.GetSpan().Count(value);
182+
int expected = CountWithForeach(data.GetSpan(), value);
180183

181184
Assert.AreEqual(result, expected, $"Failed {typeof(T)} test with count {count}: got {result} instead of {expected}");
182185
}
@@ -214,24 +217,26 @@ private static int CountWithForeach<T>(ReadOnlySpan<T> span, T value)
214217
/// <param name="value">The value to look for.</param>
215218
/// <returns>An array of random <typeparamref name="T"/> elements.</returns>
216219
[Pure]
217-
private static T[] CreateRandomData<T>(int count, T value)
220+
private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count, T value)
218221
where T : unmanaged
219222
{
220223
var random = new Random(count);
221224

222-
T[] data = new T[count];
225+
UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count);
223226

224-
foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan()))
227+
foreach (ref byte n in MemoryMarshal.AsBytes(data.GetSpan()))
225228
{
226229
n = (byte)random.Next(0, byte.MaxValue);
227230
}
228231

229232
// Fill at least 20% of the items with a matching value
230233
int minimum = count / 20;
231234

235+
Span<T> span = data.GetSpan();
236+
232237
for (int i = 0; i < minimum; i++)
233238
{
234-
data[random.Next(0, count)] = value;
239+
span[random.Next(0, count)] = value;
235240
}
236241

237242
return data;
@@ -245,12 +250,12 @@ private static T[] CreateRandomData<T>(int count, T value)
245250
/// <param name="value">The value to use to populate the array.</param>
246251
/// <returns>An array of <typeparamref name="T"/> elements.</returns>
247252
[Pure]
248-
private static T[] CreateFilledData<T>(int count, T value)
253+
private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value)
249254
where T : unmanaged
250255
{
251-
T[] data = new T[count];
256+
UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count);
252257

253-
data.AsSpan().Fill(value);
258+
data.GetSpan().Fill(value);
254259

255260
return data;
256261
}

UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_ReadOnlySpanExtensions.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ public partial class Test_ReadOnlySpanExtensions
1818
[TestMethod]
1919
public void Test_ReadOnlySpanExtensions_DangerousGetReference()
2020
{
21-
ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan();
21+
using var owner = CreateRandomData<int>(12, default);
22+
23+
ReadOnlySpan<int> data = owner.GetSpan();
2224

2325
ref int r0 = ref data.DangerousGetReference();
2426
ref int r1 = ref Unsafe.AsRef(data[0]);
@@ -30,7 +32,9 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReference()
3032
[TestMethod]
3133
public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero()
3234
{
33-
ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan();
35+
using var owner = CreateRandomData<int>(12, default);
36+
37+
ReadOnlySpan<int> data = owner.GetSpan();
3438

3539
ref int r0 = ref data.DangerousGetReference();
3640
ref int r1 = ref data.DangerousGetReferenceAt(0);
@@ -42,7 +46,9 @@ public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Zero()
4246
[TestMethod]
4347
public void Test_ReadOnlySpanExtensions_DangerousGetReferenceAt_Index()
4448
{
45-
ReadOnlySpan<int> data = CreateRandomData<int>(12, default).AsSpan();
49+
using var owner = CreateRandomData<int>(12, default);
50+
51+
ReadOnlySpan<int> data = owner.GetSpan();
4652

4753
ref int r0 = ref data.DangerousGetReferenceAt(5);
4854
ref int r1 = ref Unsafe.AsRef(data[5]);

UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_HashCode{T}.cs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Runtime.InteropServices;
99
using Microsoft.Toolkit.HighPerformance.Helpers;
1010
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using UnitTests.HighPerformance.Shared.Buffers.Internals;
1112

1213
namespace UnitTests.HighPerformance.Helpers
1314
{
@@ -67,19 +68,23 @@ public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat()
6768
[TestMethod]
6869
public void Test_HashCodeOfT_ManagedType_TestRepeat()
6970
{
70-
var random = new Random();
71+
var localTestCounts = TestCounts.Slice(0, 8);
72+
73+
// Only rent a single array of the maximum necessary size, to save space
74+
string[] data = new string[localTestCounts[localTestCounts.Length - 1]];
7175

72-
foreach (var count in TestCounts.Slice(0, 8))
76+
var random = new Random();
77+
foreach (ref string text in data.AsSpan())
7378
{
74-
string[] data = new string[count];
79+
text = random.NextDouble().ToString("E");
80+
}
7581

76-
foreach (ref string text in data.AsSpan())
77-
{
78-
text = random.NextDouble().ToString("E");
79-
}
82+
foreach (var count in localTestCounts)
83+
{
84+
Span<string> iterationData = data.AsSpan().Slice(0, count);
8085

81-
int hash1 = HashCode<string>.Combine(data);
82-
int hash2 = HashCode<string>.Combine(data);
86+
int hash1 = HashCode<string>.Combine(iterationData);
87+
int hash2 = HashCode<string>.Combine(iterationData);
8388

8489
Assert.AreEqual(hash1, hash2, $"Failed {typeof(string)} test with count {count}: got {hash1} and then {hash2}");
8590
}
@@ -95,10 +100,10 @@ private static void TestForType<T>()
95100
{
96101
foreach (var count in TestCounts)
97102
{
98-
T[] data = CreateRandomData<T>(count);
103+
using UnmanagedSpanOwner<T> data = CreateRandomData<T>(count);
99104

100-
int hash1 = HashCode<T>.Combine(data);
101-
int hash2 = HashCode<T>.Combine(data);
105+
int hash1 = HashCode<T>.Combine(data.GetSpan());
106+
int hash2 = HashCode<T>.Combine(data.GetSpan());
102107

103108
Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}");
104109
}
@@ -111,14 +116,14 @@ private static void TestForType<T>()
111116
/// <param name="count">The number of array items to create.</param>
112117
/// <returns>An array of random <typeparamref name="T"/> elements.</returns>
113118
[Pure]
114-
private static T[] CreateRandomData<T>(int count)
119+
private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count)
115120
where T : unmanaged
116121
{
117122
var random = new Random(count);
118123

119-
T[] data = new T[count];
124+
UnmanagedSpanOwner<T> data = new UnmanagedSpanOwner<T>(count);
120125

121-
foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan()))
126+
foreach (ref byte n in MemoryMarshal.AsBytes(data.GetSpan()))
122127
{
123128
n = (byte)random.Next(0, byte.MaxValue);
124129
}

UnitTests/UnitTests.HighPerformance.Shared/Helpers/Test_ParallelHelper.For.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Toolkit.HighPerformance.Extensions;
77
using Microsoft.Toolkit.HighPerformance.Helpers;
88
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
using UnitTests.HighPerformance.Shared.Buffers.Internals;
910

1011
namespace UnitTests.HighPerformance.Helpers
1112
{
@@ -19,15 +20,17 @@ public partial class Test_ParallelHelper
1920

2021
[TestCategory("ParallelHelper")]
2122
[TestMethod]
22-
public void Test_ParallelHelper_ForWithIndices()
23+
public unsafe void Test_ParallelHelper_ForWithIndices()
2324
{
2425
foreach (int count in TestForCounts)
2526
{
26-
int[] data = new int[count];
27+
using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(count);
2728

28-
ParallelHelper.For(0, data.Length, new Assigner(data));
29+
data.GetSpan().Clear();
2930

30-
foreach (var item in data.Enumerate())
31+
ParallelHelper.For(0, data.Length, new Assigner(data.Length, data.Ptr));
32+
33+
foreach (var item in data.GetSpan().Enumerate())
3134
{
3235
if (item.Index != item.Value)
3336
{
@@ -56,15 +59,17 @@ public void Test_ParallelHelper_ForInvalidRange_RangeAll()
5659

5760
[TestCategory("ParallelHelper")]
5861
[TestMethod]
59-
public void Test_ParallelHelper_ForWithRanges()
62+
public unsafe void Test_ParallelHelper_ForWithRanges()
6063
{
6164
foreach (int count in TestForCounts)
6265
{
63-
int[] data = new int[count];
66+
using UnmanagedSpanOwner<int> data = new UnmanagedSpanOwner<int>(count);
67+
68+
data.GetSpan().Clear();
6469

65-
ParallelHelper.For(..data.Length, new Assigner(data));
70+
ParallelHelper.For(..data.Length, new Assigner(data.Length, data.Ptr));
6671

67-
foreach (var item in data.Enumerate())
72+
foreach (var item in data.GetSpan().Enumerate())
6873
{
6974
if (item.Index != item.Value)
7075
{
@@ -78,21 +83,31 @@ public void Test_ParallelHelper_ForWithRanges()
7883
/// <summary>
7984
/// A type implementing <see cref="IAction"/> to initialize an array
8085
/// </summary>
81-
private readonly struct Assigner : IAction
86+
private readonly unsafe struct Assigner : IAction
8287
{
83-
private readonly int[] array;
88+
private readonly int length;
89+
private readonly int* ptr;
8490

85-
public Assigner(int[] array) => this.array = array;
91+
public Assigner(int length, int* ptr)
92+
{
93+
this.length = length;
94+
this.ptr = ptr;
95+
}
8696

8797
/// <inheritdoc/>
8898
public void Invoke(int i)
8999
{
90-
if (this.array[i] != 0)
100+
if ((uint)i >= (uint)this.length)
101+
{
102+
throw new IndexOutOfRangeException($"The target position was out of range, was {i} and should've been in [0, {this.length})");
103+
}
104+
105+
if (this.ptr[i] != 0)
91106
{
92-
throw new InvalidOperationException($"Invalid target position {i}, was {this.array[i]} instead of 0");
107+
throw new InvalidOperationException($"Invalid target position {i}, was {this.ptr[i]} instead of 0");
93108
}
94109

95-
this.array[i] = i;
110+
this.ptr[i] = i;
96111
}
97112
}
98113
}

0 commit comments

Comments
 (0)