Skip to content
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

Added [Memory|Span]Owner<T>.DangerousGetArray #3530

Merged
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
44 changes: 43 additions & 1 deletion Microsoft.Toolkit.HighPerformance/Buffers/MemoryOwner{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
#if NETCORE_RUNTIME
using System.Runtime.InteropServices;
#endif
using Microsoft.Toolkit.HighPerformance.Buffers.Views;
using Microsoft.Toolkit.HighPerformance.Extensions;

Expand Down Expand Up @@ -180,7 +183,22 @@ public Span<T> Span
ThrowObjectDisposedException();
}

#if NETCORE_RUNTIME
ref T r0 = ref array!.DangerousGetReferenceAt(this.start);

// On .NET Core runtimes, we can manually create a span from the starting reference to
// skip the argument validations, which include an explicit null check, covariance check
// for the array and the actual validation for the starting offset and target length. We
// only do this on .NET Core as we can leverage the runtime-specific array layout to get
// a fast access to the initial element, which makes this trick worth it. Otherwise, on
// runtimes where we would need to at least access a static field to retrieve the base
// byte offset within an SZ array object, we can get better performance by just using the
// default Span<T> constructor and paying the cost of the extra conditional branches,
// especially if T is a value type, in which case the covariance check is JIT removed.
return MemoryMarshal.CreateSpan(ref r0, this.length);
#else
return new Span<T>(array!, this.start, this.length);
#endif
}
}

Expand Down Expand Up @@ -208,6 +226,31 @@ public Span<T> Span
return ref array!.DangerousGetReferenceAt(this.start);
}

/// <summary>
/// Gets an <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.
/// </summary>
/// <returns>An <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.</returns>
/// <exception cref="ObjectDisposedException">Thrown when the buffer in use has already been disposed.</exception>
/// <remarks>
/// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution.
/// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's
/// not used after the current <see cref="MemoryOwner{T}"/> instance is disposed. Doing so is considered undefined behavior,
/// as the same array might be in use within another <see cref="MemoryOwner{T}"/> instance.
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySegment<T> DangerousGetArray()
{
T[]? array = this.array;

if (array is null)
{
ThrowObjectDisposedException();
}

return new ArraySegment<T>(array!, this.start, this.length);
}

/// <summary>
/// Slices the buffer currently in use and returns a new <see cref="MemoryOwner{T}"/> instance.
/// </summary>
Expand All @@ -222,7 +265,6 @@ public Span<T> Span
/// 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 <see cref="MemoryOwner{T}"/> instance.
/// </remarks>
[Pure]
public MemoryOwner<T> Slice(int start, int length)
{
T[]? array = this.array;
Expand Down
31 changes: 30 additions & 1 deletion Microsoft.Toolkit.HighPerformance/Buffers/SpanOwner{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
#if NETCORE_RUNTIME
using System.Runtime.InteropServices;
#endif
using Microsoft.Toolkit.HighPerformance.Buffers.Views;
using Microsoft.Toolkit.HighPerformance.Extensions;

Expand Down Expand Up @@ -143,7 +146,16 @@ public int Length
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Span<T>(this.array, 0, this.length);
get
{
#if NETCORE_RUNTIME
ref T r0 = ref array!.DangerousGetReference();

return MemoryMarshal.CreateSpan(ref r0, this.length);
#else
return new Span<T>(this.array, 0, this.length);
#endif
}
}

/// <summary>
Expand All @@ -157,6 +169,23 @@ public Span<T> Span
return ref this.array.DangerousGetReference();
}

/// <summary>
/// Gets an <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.
/// </summary>
/// <returns>An <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.</returns>
/// <remarks>
/// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution.
/// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's
/// not used after the current <see cref="SpanOwner{T}"/> instance is disposed. Doing so is considered undefined behavior,
/// as the same array might be in use within another <see cref="SpanOwner{T}"/> instance.
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySegment<T> DangerousGetArray()
{
return new ArraySegment<T>(array!, 0, this.length);
}

/// <summary>
/// Implements the duck-typed <see cref="IDisposable.Dispose"/> method.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.Linq;
using Microsoft.Toolkit.HighPerformance.Buffers;
Expand Down Expand Up @@ -105,7 +104,7 @@ public void Test_MemoryOwnerOfT_MultipleDispose()
// by accident doesn't cause issues, and just does nothing.
}

[TestCategory("HashCodeOfT")]
[TestCategory("MemoryOwnerOfT")]
[TestMethod]
public void Test_MemoryOwnerOfT_PooledBuffersAndClear()
{
Expand All @@ -124,5 +123,44 @@ public void Test_MemoryOwnerOfT_PooledBuffersAndClear()
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0));
}
}

[TestCategory("MemoryOwnerOfT")]
[TestMethod]
public void Test_MemoryOwnerOfT_AllocateAndGetArray()
{
var buffer = MemoryOwner<int>.Allocate(127);

// Here we allocate a MemoryOwner<T> instance with a requested size of 127, which means it
// internally requests an array of size 127 from ArrayPool<T>.Shared. We then get the array
// segment, so we need to verify that (since buffer is not disposed) the returned array is
// not null, is of size >= the requested one (since ArrayPool<T> by definition returns an
// array that is at least of the requested size), and that the offset and count properties
// match our input values (same length, and offset at 0 since the buffer was not sliced).
var segment = buffer.DangerousGetArray();

Assert.IsNotNull(segment.Array);
Assert.IsTrue(segment.Array.Length >= buffer.Length);
Assert.AreEqual(segment.Offset, 0);
Assert.AreEqual(segment.Count, buffer.Length);

var second = buffer.Slice(10, 80);

// The original buffer instance is disposed here, because calling Slice transfers
// the ownership of the internal buffer to the new instance (this is documented in
// XML docs for the MemoryOwner<T>.Slice method).
Assert.ThrowsException<ObjectDisposedException>(() => buffer.DangerousGetArray());

segment = second.DangerousGetArray();

// Same as before, but we now also verify the initial offset != 0, as we used Slice
Assert.IsNotNull(segment.Array);
Assert.IsTrue(segment.Array.Length >= second.Length);
Assert.AreEqual(segment.Offset, 10);
Assert.AreEqual(segment.Count, second.Length);

second.Dispose();

Assert.ThrowsException<ObjectDisposedException>(() => second.DangerousGetArray());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void Test_SpanOwnerOfT_InvalidRequestedSize()
Assert.Fail("You shouldn't be here");
}

[TestCategory("HashCodeOfT")]
[TestCategory("SpanOwnerOfT")]
[TestMethod]
public void Test_SpanOwnerOfT_PooledBuffersAndClear()
{
Expand All @@ -79,5 +79,23 @@ public void Test_SpanOwnerOfT_PooledBuffersAndClear()
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0));
}
}

[TestCategory("SpanOwnerOfT")]
[TestMethod]
public void Test_SpanOwnerOfT_AllocateAndGetArray()
{
using var buffer = SpanOwner<int>.Allocate(127);

var segment = buffer.DangerousGetArray();

// See comments in the MemoryOwner<T> tests about this. The main difference
// here is that we don't do the disposed checks, as SpanOwner<T> is optimized
// with the assumption that usages after dispose are undefined behavior. This
// is all documented in the XML docs for the SpanOwner<T> type.
Assert.IsNotNull(segment.Array);
Assert.IsTrue(segment.Array.Length >= buffer.Length);
Assert.AreEqual(segment.Offset, 0);
Assert.AreEqual(segment.Count, buffer.Length);
}
}
}