Skip to content

Reduced memory usage in HighPerformance tests #3408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.

using System;
using System.Buffers;
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> : MemoryManager<T>
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;
}

/// <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;

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
IntPtr ptr = this.ptr;

if (ptr == IntPtr.Zero)
{
return;
}

this.ptr = IntPtr.Zero;

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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
using System.Runtime.InteropServices;
using Microsoft.Toolkit.HighPerformance.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTests.HighPerformance.Shared.Buffers.Internals;

#nullable enable

namespace UnitTests.HighPerformance.Extensions
{
Expand Down Expand Up @@ -168,15 +171,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.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}");
}
Expand Down Expand Up @@ -214,24 +217,26 @@ 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.GetSpan()))
{
n = (byte)random.Next(0, byte.MaxValue);
}

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

Span<T> span = data.GetSpan();

for (int i = 0; i < minimum; i++)
{
data[random.Next(0, count)] = value;
span[random.Next(0, count)] = value;
}

return data;
Expand All @@ -245,12 +250,12 @@ 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.GetSpan().Fill(value);

return data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.GetSpan();

ref int r0 = ref data.DangerousGetReference();
ref int r1 = ref Unsafe.AsRef(data[0]);
Expand All @@ -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.GetSpan();

ref int r0 = ref data.DangerousGetReference();
ref int r1 = ref data.DangerousGetReferenceAt(0);
Expand All @@ -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.GetSpan();

ref int r0 = ref data.DangerousGetReferenceAt(5);
ref int r1 = ref Unsafe.AsRef(data[5]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -67,19 +68,23 @@ public void Test_HashCodeOfT_VectorUnsupportedTypes_TestRepeat()
[TestMethod]
public void Test_HashCodeOfT_ManagedType_TestRepeat()
{
var random = new Random();
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]];

foreach (var count in TestCounts.Slice(0, 8))
var random = new Random();
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}");
}
Expand All @@ -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.GetSpan());
int hash2 = HashCode<T>.Combine(data.GetSpan());

Assert.AreEqual(hash1, hash2, $"Failed {typeof(T)} test with count {count}: got {hash1} and then {hash2}");
}
Expand All @@ -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.GetSpan()))
{
n = (byte)random.Next(0, byte.MaxValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);

data.GetSpan().Clear();

ParallelHelper.For(..data.Length, new Assigner(data));
ParallelHelper.For(..data.Length, new Assigner(data.Length, data.Ptr));

foreach (var item in data.Enumerate())
foreach (var item in data.GetSpan().Enumerate())
{
if (item.Index != item.Value)
{
Expand All @@ -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 IndexOutOfRangeException($"The target position was out of range, was {i} and should've been in [0, {this.length})");
}

if (this.ptr[i] != 0)
{
throw new InvalidOperationException($"Invalid target position {i}, was {this.array[i]} instead of 0");
throw new InvalidOperationException($"Invalid target position {i}, was {this.ptr[i]} instead of 0");
}

this.array[i] = i;
this.ptr[i] = i;
}
}
}
Expand Down
Loading