Skip to content

Commit

Permalink
Update SpanReader
Browse files Browse the repository at this point in the history
Flesh out SpanReader and tune for performance.
  • Loading branch information
JeremyKuhne committed Jan 22, 2024
1 parent f472eda commit 6d94953
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/thirtytwo/Support/SpanExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static IEnumerable<string> Split(this ReadOnlySpan<char> span, char delim
{
List<string> strings = [];
SpanReader<char> reader = new(span);
while (reader.TryReadTo(out var next, delimiter))
while (reader.TryReadTo(delimiter, out var next))
{
if (includeEmptyStrings || !next.IsEmpty)
{
Expand Down
191 changes: 181 additions & 10 deletions src/thirtytwo/Support/SpanReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Windows.Support;

/// <summary>
/// Simple span reader. Follows <see cref="SequenceReader{T}"/>.
/// Fast stack based <see cref="ReadOnlySpan{T}"/> reader.
/// </summary>
public ref struct SpanReader<T>(ReadOnlySpan<T> span) where T : unmanaged, IEquatable<T>
/// <remarks>
/// <para>
/// Inspired by <see cref="SequenceReader{T}"/> patterns.
/// </para>
/// </remarks>
public unsafe ref struct SpanReader<T>(ReadOnlySpan<T> span) where T : unmanaged, IEquatable<T>
{
private ReadOnlySpan<T> _unread = span;
public ReadOnlySpan<T> Span { get; } = span;
public int Index { get; private set; }
public readonly ReadOnlySpan<T> UnreadSpan => _unread;

public readonly ReadOnlySpan<T> Remaining => Span[Index..];
/// <summary>
/// Try to read everything up to the given <paramref name="delimiter"/>. Advances the reader past the
/// <paramref name="delimiter"/> if found.
/// </summary>
/// <inheritdoc cref="TryReadTo(T, bool, out ReadOnlySpan{T})"/>
public bool TryReadTo(T delimiter, out ReadOnlySpan<T> span) =>
TryReadTo(delimiter, advancePastDelimiter: true, out span);

/// <summary>
/// Try to read everything up to the given <paramref name="delimiter"/>.
Expand All @@ -22,23 +36,180 @@ public ref struct SpanReader<T>(ReadOnlySpan<T> span) where T : unmanaged, IEqua
/// <param name="delimiter">The delimiter to look for.</param>
/// <param name="advancePastDelimiter"><see langword="true"/> to move past the <paramref name="delimiter"/> if found.</param>
/// <returns><see langword="true"/> if the <paramref name="delimiter"/> was found.</returns>
public bool TryReadTo(out ReadOnlySpan<T> span, T delimiter, bool advancePastDelimiter = true)
public bool TryReadTo(T delimiter, bool advancePastDelimiter, out ReadOnlySpan<T> span)
{
bool found = false;
ReadOnlySpan<T> remaining = Remaining;
int index = remaining.IndexOf(delimiter);
int index = _unread.IndexOf(delimiter);
span = default;

if (index != -1)
{
span = index == 0 ? default : remaining[..index];
Index += index + (advancePastDelimiter ? 1 : 0);
found = true;
if (index > 0)
{
span = _unread;
UncheckedSliceTo(ref span, index);
if (advancePastDelimiter)
{
index++;
}

UncheckedSlice(ref _unread, index, _unread.Length - index);
}
}

return found;
}

/// <summary>
/// Try to read the next value.
/// </summary>
public bool TryRead(out T value)
{
bool success;

if (_unread.IsEmpty)
{
value = default;
success = false;
}
else
{
success = true;
value = _unread[0];
UnsafeAdvance(1);
}

return success;
}

/// <summary>
/// Try to read a span of the given <paramref name="count"/>.
/// </summary>
public bool TryRead(int count, out ReadOnlySpan<T> span)
{
bool success;

if (count > _unread.Length)
{
span = default;
success = false;
}
else
{
success = true;
span = _unread[..count];
UnsafeAdvance(count);
}

return found;
return success;
}

/// <summary>
/// Try to read a value of the given type. The size of the value must be evenly divisible by the size of
/// <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is just a straight copy of bits. If <typeparamref name="TValue"/> has methods that depend on
/// specific field value constraints this could be unsafe.
/// </para>
/// <para>
/// The compiler will often optimize away the struct copy if you only read from the value.
/// </para>
/// </remarks>
public bool TryRead<TValue>(out TValue value) where TValue : unmanaged
{
if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0)
{
throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}.");
}

bool success;

if (sizeof(TValue) > _unread.Length * sizeof(T))
{
value = default;
success = false;
}
else
{
success = true;
value = Unsafe.ReadUnaligned<TValue>(ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(_unread)));
UnsafeAdvance(sizeof(TValue) / sizeof(T));
}

return success;
}

/// <summary>
/// Try to read a span of values of the given type. The size of the value must be evenly divisible by the size of
/// <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// <para>
/// This effectively does a <see cref="MemoryMarshal.Cast{TFrom, TTo}(ReadOnlySpan{TFrom})"/> and the same
/// caveats apply about safety.
/// </para>
/// </remarks>
public bool TryRead<TValue>(int count, out ReadOnlySpan<TValue> value) where TValue : unmanaged
{
if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0)
{
throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}.");
}

bool success;

if (sizeof(TValue) * count > _unread.Length * sizeof(T))
{
value = default;
success = false;
}
else
{
success = true;
value = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, TValue>(ref MemoryMarshal.GetReference(_unread)), count);
UnsafeAdvance((sizeof(TValue) / sizeof(T)) * count);
}

return success;
}

/// <summary>
/// Advance the reader by the given <paramref name="count"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count) => _unread = _unread[count..];

/// <summary>
/// Rewind the reader by the given <paramref name="count"/>.
/// </summary>
public void Rewind(int count) => _unread = Span[(Span.Length - _unread.Length - count)..];

/// <summary>
/// Reset the reader to the beginning of the span.
/// </summary>
public void Reset() => _unread = Span;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UnsafeAdvance(int count)
{
Debug.Assert((uint)count <= (uint)_unread.Length);
UncheckedSlice(ref _unread, count, _unread.Length - count);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UncheckedSliceTo(ref ReadOnlySpan<T> span, int length)
{
Debug.Assert((uint)length <= (uint)span.Length);
span = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(span), length);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UncheckedSlice(ref ReadOnlySpan<T> span, int start, int length)
{
Debug.Assert((uint)start <= (uint)span.Length && (uint)length <= (uint)(span.Length - start));
span = MemoryMarshal.CreateReadOnlySpan<T>(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)start), length);
}
}
158 changes: 158 additions & 0 deletions src/thirtytwo_tests/Support/SpanReaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Drawing;

namespace Windows.Support;

public class SpanReaderTests
{
[Fact]
public void SpanReader_TryReadTo_SkipDelimiter()
{
ReadOnlySpan<byte> span = new byte[] { 1, 2, 3, 4, 5 };
SpanReader<byte> reader = new(span);

reader.TryReadTo(3, out var read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([1, 2]);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([4, 5]);

reader.TryReadTo(5, out read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([4]);
reader.UnreadSpan.ToArray().Should().BeEmpty();
}

[Fact]
public void SpanReader_TryReadTo_DontSkipDelimiter()
{
ReadOnlySpan<byte> span = new byte[] { 1, 2, 3, 4, 5 };
SpanReader<byte> reader = new(span);

reader.TryReadTo(3, advancePastDelimiter: false, out var read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([1, 2]);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]);

reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([3, 4]);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);

reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue();
read.ToArray().Should().BeEmpty();
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);
}

[Fact]
public void SpanReader_Advance()
{
ReadOnlySpan<byte> span = new byte[] { 1, 2, 3, 4, 5 };
SpanReader<byte> reader = new(span);

reader.Advance(2);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]);

reader.Advance(2);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);

reader.Advance(1);
reader.UnreadSpan.ToArray().Should().BeEmpty();

try
{
reader.Advance(1);
Assert.Fail($"Expected {nameof(ArgumentOutOfRangeException)}");
}
catch (ArgumentOutOfRangeException)
{
// Expected
}
}

[Fact]
public void SpanReader_Rewind()
{
ReadOnlySpan<byte> span = new byte[] { 1, 2, 3, 4, 5 };
SpanReader<byte> reader = new(span);

reader.Advance(2);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]);

reader.Rewind(1);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([2, 3, 4, 5]);

try
{
reader.Rewind(2);
Assert.Fail($"Expected {nameof(ArgumentOutOfRangeException)}");
}
catch (ArgumentOutOfRangeException)
{
// Expected
}

reader.Rewind(1);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 5]);
}

[Fact]
public void SpanReader_TryRead_ReadPoints()
{
ReadOnlySpan<uint> span = new uint[] { 1, 2, 3, 4, 5 };
SpanReader<uint> reader = new(span);

reader.TryRead(out Point value).Should().BeTrue();
value.Should().Be(new Point(1, 2));

reader.TryRead(out value).Should().BeTrue();
value.Should().Be(new Point(3, 4));

reader.TryRead(out value).Should().BeFalse();
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);
}

[Fact]
public void SpanReader_TryRead_ReadPointCounts()
{
ReadOnlySpan<uint> span = new uint[] { 1, 2, 3, 4, 5 };
SpanReader<uint> reader = new(span);

reader.TryRead(2, out ReadOnlySpan<Point> value).Should().BeTrue();
value.ToArray().Should().BeEquivalentTo([new Point(1, 2), new Point(3, 4)]);

// This fails to compile as the span is read only, as expected.
// value[0].X = 0;

reader.TryRead(2, out value).Should().BeFalse();
value.ToArray().Should().BeEmpty();
}

[Theory]
[InlineData(0, 1)]
[InlineData(1, 1)]
[InlineData(2, 2)]
[InlineData(3, 2)]
public void SpanReader_TryRead_ReadPointFCounts_NotEnoughBuffer(int bufferSize, int readCount)
{
ReadOnlySpan<float> span = new float[bufferSize];
SpanReader<float> reader = new(span);

reader.TryRead<PointF>(readCount, out _).Should().BeFalse();
}

[Fact]
public void SpanReader_TryRead_Count()
{
ReadOnlySpan<uint> span = new uint[] { 1, 2, 3, 4, 5 };
SpanReader<uint> reader = new(span);

reader.TryRead(2, out var read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([1, 2]);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]);

reader.TryRead(2, out read).Should().BeTrue();
read.ToArray().Should().BeEquivalentTo([3, 4]);
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);

reader.TryRead(2, out read).Should().BeFalse();
reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]);
}
}

0 comments on commit 6d94953

Please sign in to comment.