From 3b1671404af675cfb75a681cd70f47820e9ffbf8 Mon Sep 17 00:00:00 2001 From: Maryam Ariyan Date: Fri, 22 Mar 2019 14:22:55 -0700 Subject: [PATCH 1/4] Add ReadOnlySequence.FirstSpan Fixes: #33029 --- src/System.Memory/ref/System.Memory.cs | 1 + .../Buffers/ReadOnlySequence.Helpers.cs | 78 +++++++++++++++++++ .../src/System/Buffers/ReadOnlySequence.cs | 5 ++ .../ReadOnlySequenceTests.Common.cs | 35 +++++++++ .../ReadOnlySequenceTests.Default.cs | 1 + .../ReadOnlySequenceTests.Empty.cs | 1 + 6 files changed, 121 insertions(+) diff --git a/src/System.Memory/ref/System.Memory.cs b/src/System.Memory/ref/System.Memory.cs index e0a05cbabd0a..46af74f20413 100644 --- a/src/System.Memory/ref/System.Memory.cs +++ b/src/System.Memory/ref/System.Memory.cs @@ -160,6 +160,7 @@ public readonly partial struct ReadOnlySequence public ReadOnlySequence(T[] array, int start, int length) { throw null; } public System.SequencePosition End { get { throw null; } } public System.ReadOnlyMemory First { get { throw null; } } + public System.ReadOnlySpan FirstSpan { get { throw null; } } public bool IsEmpty { get { throw null; } } public bool IsSingleSegment { get { throw null; } } public long Length { get { throw null; } } diff --git a/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs b/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs index 8c396df232f3..9be3d7678209 100644 --- a/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs +++ b/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs @@ -154,6 +154,84 @@ private ReadOnlyMemory GetFirstBufferSlow(object startObject, bool isMultiSeg } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlySpan GetFirstSpan() + { + object startObject = _startObject; + + if (startObject == null) + return default; + + int startIndex = _startInteger; + int endIndex = _endInteger; + + bool isMultiSegment = startObject != _endObject; + + // The highest bit of startIndex and endIndex are used to infer the sequence type + // The code below is structured this way for performance reasons and is equivalent to the following: + // SequenceType type = GetSequenceType(); + // if (type == SequenceType.MultiSegment) { ... } + // else if (type == SequenceType.Array) { ... } + // else if (type == SequenceType.String){ ... } + // else if (type == SequenceType.MemoryManager) { ... } + + // Highest bit of startIndex: A = startIndex >> 31 + // Highest bit of endIndex: B = endIndex >> 31 + + // A == 0 && B == 0 means SequenceType.MultiSegment + // Equivalent to startIndex >= 0 && endIndex >= 0 + if ((startIndex | endIndex) >= 0) + { + ReadOnlySpan span = ((ReadOnlySequenceSegment)startObject).Memory.Span; + if (isMultiSegment) + { + return span.Slice(startIndex); + } + return span.Slice(startIndex, endIndex - startIndex); + } + else + { + return GetFirstSpanFast(startObject, isMultiSegment); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private ReadOnlySpan GetFirstSpanFast(object startObject, bool isMultiSegment) + { + if (isMultiSegment) + ThrowHelper.ThrowInvalidOperationException_EndPositionNotReached(); + + int startIndex = _startInteger; + int endIndex = _endInteger; + + Debug.Assert(startIndex < 0 || endIndex < 0); + + // A == 0 && B == 1 means SequenceType.Array + if (startIndex >= 0) + { + Debug.Assert(endIndex < 0); + ReadOnlySpan span = (T[])startObject; + return span.Slice(startIndex, (endIndex & ReadOnlySequence.IndexBitMask) - startIndex); + } + else + { + // The type == char check here is redundant. However, we still have it to allow + // the JIT to see when that the code is unreachable and eliminate it. + // A == 1 && B == 1 means SequenceType.String + if (typeof(T) == typeof(char) && endIndex < 0) + { + var memory = (ReadOnlyMemory)(object)((string)startObject).AsMemory(); + // No need to remove the FlagBitMask since (endIndex - startIndex) == (endIndex & ReadOnlySequence.IndexBitMask) - (startIndex & ReadOnlySequence.IndexBitMask) + return memory.Span.Slice(startIndex & ReadOnlySequence.IndexBitMask, endIndex - startIndex); + } + else // endIndex >= 0, A == 1 && B == 0 means SequenceType.MemoryManager + { + startIndex &= ReadOnlySequence.IndexBitMask; + return ((MemoryManager)startObject).Memory.Span.Slice(startIndex, endIndex - startIndex); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal SequencePosition Seek(long offset, ExceptionArgument exceptionArgument = ExceptionArgument.offset) { diff --git a/src/System.Memory/src/System/Buffers/ReadOnlySequence.cs b/src/System.Memory/src/System/Buffers/ReadOnlySequence.cs index 319189fa23dd..1ade0921255a 100644 --- a/src/System.Memory/src/System/Buffers/ReadOnlySequence.cs +++ b/src/System.Memory/src/System/Buffers/ReadOnlySequence.cs @@ -51,6 +51,11 @@ public bool IsSingleSegment /// public ReadOnlyMemory First => GetFirstBuffer(); + /// + /// Gets from the first segment. + /// + public ReadOnlySpan FirstSpan => GetFirstSpan(); + /// /// A position to the start of the . /// diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs index 685388b48ee4..74a22ff3fa06 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.MemoryTests; using System.Text; using Xunit; @@ -126,6 +127,7 @@ public void CanGetFirst() for (int i = 100; i > 0; i--) { Assert.Equal(i, buffer.First.Length); + Assert.Equal(i, buffer.FirstSpan.Length); buffer = buffer.Slice(1); length--; Assert.Equal(length, buffer.Length); @@ -137,6 +139,7 @@ public void CanGetFirst() for (int i = 200; i > 0; i--) { Assert.Equal(i, buffer.First.Length); + Assert.Equal(i, buffer.FirstSpan.Length); buffer = buffer.Slice(1); length--; Assert.Equal(length, buffer.Length); @@ -144,6 +147,38 @@ public void CanGetFirst() Assert.Equal(0, buffer.Length); Assert.Equal(0, buffer.First.Length); + Assert.Equal(0, buffer.FirstSpan.Length); + } + + [Fact] + public void AsString_CanGetFirst() + { + const string SampleString = "12345"; + ReadOnlySequence buffer = new ReadOnlySequence(SampleString.AsMemory()); + Assert.Equal(SampleString.Length, buffer.First.Length); + Assert.Equal(SampleString.Length, buffer.FirstSpan.Length); + } + + private readonly int[] _sampleIntArray = { 1, 2, 3, 4, 5 }; + + [Fact] + public void AsArray_CanGetFirst() + { + ReadOnlyMemory memory = new ReadOnlyMemory(_sampleIntArray); + ReadOnlySequence buffer = new ReadOnlySequence(memory); + Assert.Equal(_sampleIntArray.Length, buffer.First.Length); + Assert.Equal(_sampleIntArray.Length, buffer.FirstSpan.Length); + } + + [Fact] + public void AsMemoryManager_CanGetFirst() + { + MemoryManager manager = new CustomMemoryForTest(_sampleIntArray); + ReadOnlyMemory memoryFromManager = ((ReadOnlyMemory)manager.Memory); + + ReadOnlySequence buffer = new ReadOnlySequence(memoryFromManager); + Assert.Equal(_sampleIntArray.Length, buffer.First.Length); + Assert.Equal(_sampleIntArray.Length, buffer.FirstSpan.Length); } #endregion diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Default.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Default.cs index 2a9efc393746..b873c3ffa7c3 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Default.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Default.cs @@ -21,6 +21,7 @@ public void Default_Constructor() Assert.True(buffer.IsSingleSegment); Assert.Equal(0, buffer.Length); Assert.True(buffer.First.IsEmpty); + Assert.True(buffer.FirstSpan.IsEmpty); Assert.Equal($"System.Buffers.ReadOnlySequence<{typeof(byte).Name}>[0]", buffer.ToString()); } diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Empty.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Empty.cs index 962bec799e15..a4ad8f5fdb7e 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Empty.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Empty.cs @@ -22,6 +22,7 @@ public void Empty_Constructor() Assert.True(buffer.IsSingleSegment); Assert.Equal(0, buffer.Length); Assert.True(buffer.First.IsEmpty); + Assert.True(buffer.FirstSpan.IsEmpty); Assert.Equal($"System.Buffers.ReadOnlySequence<{typeof(byte).Name}>[0]", buffer.ToString()); } From 0cd234d5e48edbb4d5ef54e10e3a4c9e0fdad4bc Mon Sep 17 00:00:00 2001 From: Maryam Ariyan Date: Thu, 28 Mar 2019 12:20:50 -0700 Subject: [PATCH 2/4] Rename to GetFirstSpanSlow --- .../src/System/Buffers/ReadOnlySequence.Helpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs b/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs index 9be3d7678209..4612016a0b43 100644 --- a/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs +++ b/src/System.Memory/src/System/Buffers/ReadOnlySequence.Helpers.cs @@ -191,12 +191,12 @@ private ReadOnlySpan GetFirstSpan() } else { - return GetFirstSpanFast(startObject, isMultiSegment); + return GetFirstSpanSlow(startObject, isMultiSegment); } } [MethodImpl(MethodImplOptions.NoInlining)] - private ReadOnlySpan GetFirstSpanFast(object startObject, bool isMultiSegment) + private ReadOnlySpan GetFirstSpanSlow(object startObject, bool isMultiSegment) { if (isMultiSegment) ThrowHelper.ThrowInvalidOperationException_EndPositionNotReached(); From c1dbbd9d9362d36ca04f43a934a59304f96e733e Mon Sep 17 00:00:00 2001 From: Maryam Ariyan Date: Thu, 28 Mar 2019 16:48:51 -0700 Subject: [PATCH 3/4] Cleanup tests --- .../ReadOnlySequenceTests.Common.char.cs | 8 +++ .../ReadOnlySequenceTests.Common.cs | 66 ++++++++----------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.char.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.char.cs index f42d140297ae..ef450f336d44 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.char.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.char.cs @@ -80,5 +80,13 @@ public void HelloWorldAcrossTwoBlocks() Assert.Equal("Hello", new string(helloBytes)); Assert.Equal(" World", new string(worldBytes)); } + + [Fact] + public void AsString_CanGetFirst() + { + const string SampleString = "12345"; + var buffer = new ReadOnlySequence(SampleString.AsMemory()); + VerifyCanGetFirst(buffer, expectedSize: 5); + } } } diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs index 74a22ff3fa06..a9ad857740f0 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs @@ -109,8 +109,26 @@ public void CheckEndReachableDoesNotCrossPastEnd() #region First + private readonly T[] _sampleArray = Enumerable.Repeat(default(T), 5).ToArray(); + [Fact] - public void CanGetFirst() + public void AsArray_CanGetFirst() + { + ReadOnlyMemory memory = new ReadOnlyMemory(_sampleArray); + VerifyCanGetFirst(new ReadOnlySequence(memory), expectedSize: _sampleArray.Length); + } + + [Fact] + public void AsMemoryManager_CanGetFirst() + { + MemoryManager manager = new CustomMemoryForTest(_sampleArray); + ReadOnlyMemory memoryFromManager = ((ReadOnlyMemory)manager.Memory); + + VerifyCanGetFirst(new ReadOnlySequence(memoryFromManager), expectedSize: _sampleArray.Length); + } + + [Fact] + public void AsMultiSegment_CanGetFirst() { var bufferSegment1 = new BufferSegment(new T[100]); BufferSegment bufferSegment2 = bufferSegment1.Append(new T[100]); @@ -118,10 +136,9 @@ public void CanGetFirst() BufferSegment bufferSegment4 = bufferSegment3.Append(new T[200]); var buffer = new ReadOnlySequence(bufferSegment1, 0, bufferSegment4, 200); - + // Verify first 3 segments Assert.Equal(500, buffer.Length); int length = 500; - for (int s = 0; s < 3; s++) { for (int i = 100; i > 0; i--) @@ -133,10 +150,16 @@ public void CanGetFirst() Assert.Equal(length, buffer.Length); } } + // Verify last segment + VerifyCanGetFirst(buffer, expectedSize: 200); + } + + protected void VerifyCanGetFirst(ReadOnlySequence buffer, int expectedSize) + { + Assert.Equal(expectedSize, buffer.Length); + int length = expectedSize; - Assert.Equal(200, buffer.Length); - - for (int i = 200; i > 0; i--) + for (int i = length; i > 0; i--) { Assert.Equal(i, buffer.First.Length); Assert.Equal(i, buffer.FirstSpan.Length); @@ -150,37 +173,6 @@ public void CanGetFirst() Assert.Equal(0, buffer.FirstSpan.Length); } - [Fact] - public void AsString_CanGetFirst() - { - const string SampleString = "12345"; - ReadOnlySequence buffer = new ReadOnlySequence(SampleString.AsMemory()); - Assert.Equal(SampleString.Length, buffer.First.Length); - Assert.Equal(SampleString.Length, buffer.FirstSpan.Length); - } - - private readonly int[] _sampleIntArray = { 1, 2, 3, 4, 5 }; - - [Fact] - public void AsArray_CanGetFirst() - { - ReadOnlyMemory memory = new ReadOnlyMemory(_sampleIntArray); - ReadOnlySequence buffer = new ReadOnlySequence(memory); - Assert.Equal(_sampleIntArray.Length, buffer.First.Length); - Assert.Equal(_sampleIntArray.Length, buffer.FirstSpan.Length); - } - - [Fact] - public void AsMemoryManager_CanGetFirst() - { - MemoryManager manager = new CustomMemoryForTest(_sampleIntArray); - ReadOnlyMemory memoryFromManager = ((ReadOnlyMemory)manager.Memory); - - ReadOnlySequence buffer = new ReadOnlySequence(memoryFromManager); - Assert.Equal(_sampleIntArray.Length, buffer.First.Length); - Assert.Equal(_sampleIntArray.Length, buffer.FirstSpan.Length); - } - #endregion #region EmptySegments From 668349bab596d93d7b1ed0594bb90cdba0f46cf8 Mon Sep 17 00:00:00 2001 From: Maryam Ariyan Date: Thu, 28 Mar 2019 21:40:12 -0700 Subject: [PATCH 4/4] nit: removing _sampleArray --- .../ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs index a9ad857740f0..e274546cd362 100644 --- a/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs +++ b/src/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceTests.Common.cs @@ -109,22 +109,20 @@ public void CheckEndReachableDoesNotCrossPastEnd() #region First - private readonly T[] _sampleArray = Enumerable.Repeat(default(T), 5).ToArray(); - [Fact] public void AsArray_CanGetFirst() { - ReadOnlyMemory memory = new ReadOnlyMemory(_sampleArray); - VerifyCanGetFirst(new ReadOnlySequence(memory), expectedSize: _sampleArray.Length); + var memory = new ReadOnlyMemory(new T[5]); + VerifyCanGetFirst(new ReadOnlySequence(memory), expectedSize: 5); } [Fact] public void AsMemoryManager_CanGetFirst() { - MemoryManager manager = new CustomMemoryForTest(_sampleArray); + MemoryManager manager = new CustomMemoryForTest(new T[5]); ReadOnlyMemory memoryFromManager = ((ReadOnlyMemory)manager.Memory); - VerifyCanGetFirst(new ReadOnlySequence(memoryFromManager), expectedSize: _sampleArray.Length); + VerifyCanGetFirst(new ReadOnlySequence(memoryFromManager), expectedSize: 5); } [Fact]