Simplified OwnedBuffer Pinning Support#1527
Conversation
|
Before After |
6d22339 to
fda80d6
Compare
fda80d6 to
99216a6
Compare
davidfowl
left a comment
There was a problem hiding this comment.
I see less and less value in OwnedBuffer the more changes we make to it. What are the semantics of Retain/Release? If I call Retain twice and then Release what behavior should I expect? If it's basically the reference counting behavior then lets call it what it is instead of being abstract. Trying to make it too abstract will break systems that rely on reference counting for correctness (aka pipelines)
|
The contract is: if Retain is called more than Release, Buffer.Span will succeed. This requires reference counting for unmanaged buffers and pooled arrays. It does not require reference counting for array based buffers. Also, this change does not seem to have anything to do with your concern. It literally just optimizes the Buffer.Pin and Buffer.Span code paths, plus fixes a bunch of bugs. Can we resolve the concern offline (i.e. not as part of this PR)? |
|
|
||
| #endregion | ||
|
|
||
| protected static unsafe void* Add(void* pointer, int offset) |
There was a problem hiding this comment.
Does this Add method belong here?
There was a problem hiding this comment.
It's used in many subtypes. I would like to move it somewhere, but I am not sure where.
There was a problem hiding this comment.
What about using Unsafe.Add? We use it in Primitive Formatters a lot.
There was a problem hiding this comment.
Unsafe.Add takes ref T. I need byte*
There was a problem hiding this comment.
Unsafe.AsPointer(Unsafe.Add(ref Unsafe.AsRef(myPointer), offset))
There was a problem hiding this comment.
Might not be exactly that since I'm going from memory. But then you don't have to do the sizeof math and it all gets inlined anyways.
There was a problem hiding this comment.
This is code that implementers of OwnedBuffers will have to write. I would prefer this be simpler than going back and forth between references and pointers. Let me leave the PR as-is and we can think separately what's the best way to provide convenient API for this scenario. I filed issue #1529
There was a problem hiding this comment.
Sounds good to me. S.R.CS.Unsafe has both void* and ref T overloads for most methods. void* overloads for Add and Subtract are missing for no good reason.
| return true; | ||
| } | ||
|
|
||
| public unsafe bool TryGetArray(out ArraySegment<T> buffer) |
There was a problem hiding this comment.
Does this method still need to be unsafe?
| public abstract int Length { get; } | ||
|
|
||
| public abstract Span<T> Span { get; } | ||
| public abstract Span<T> AsSpan(int index, int length); |
There was a problem hiding this comment.
Do we need this overload? Why not just use AsSpan().Slice(index, length)?
There was a problem hiding this comment.
This is faster. The method is virtual, so the same optimizations we were able to do in Span probably will not work here. Moreover, it allows subclasses to materialize only part of the buffer representing this range.
There was a problem hiding this comment.
This is faster. The method is virtual, so the same optimizations we were able to do in Span probably will not work here.
Does this mean that the IndexOf that takes an index/count vs Slice analysis doesn't apply to this? If so, and it is faster, then it is good to keep.
Moreover, it allows subclasses to materialize only part of the buffer representing this range.
Why exactly can't the subtypes just slice the span?
There was a problem hiding this comment.
Does this mean that the IndexOf that takes an index/count vs Slice analysis doesn't apply to this? If so, and it is faster, then it is good to keep.
It does not apply because IndexOf and Slice on Span are non-virtual, can be inlined, and code optimized out.
Why exactly can't the subtypes just slice the span?
Let's say the buffer wraps memory mapped file. AsSpan(0, 10) needs to read only first ten bytes of the file. Span.Slice(0, 10) would first have to read the whole file and then create a span of just first ten bytes.
There was a problem hiding this comment.
Span.Slice(0, 10) would first have to read the whole file and then create a span of just first ten bytes.
I don't completely understand this. The cost of creating a span isn't proportional to the input buffer size it is wrapping, correct? It is a constant cost operation. So why does Span.Slice(0, 10) read the whole file whereas AsSpan(0, 10) does not?
There was a problem hiding this comment.
If you buffer is in memory, then you are right. But OwnedBuffer is an abstraction and not all it's data have to be in memory.
|
|
||
| namespace System.Buffers | ||
| { | ||
| public class OwnedArray<T> : OwnedBuffer<T> |
There was a problem hiding this comment.
Is the only change here moving OwnedArrray.cs from System/Buffers/Internals to System/Buffers or are there other changes as well?
Edit: Nevermind, I see the changes.
| _array = array; | ||
| } | ||
|
|
||
| public OwnedArray(ArraySegment<T> segment) |
There was a problem hiding this comment.
What is the motivation to remove the OwnedArray constructor that takes an ArraySegment?
There was a problem hiding this comment.
It requires fields for offset and length to be stored in OwnedArray.
| return _array; | ||
| } | ||
| if (IsDisposed) BufferPrimitivesThrowHelper.ThrowObjectDisposedException(nameof(OwnedBuffer)); | ||
| return _array.Slice(index, length); |
There was a problem hiding this comment.
_array.AsSpan().Slice(index, length); instead?
There was a problem hiding this comment.
Yeah, I was wondering why this Slice method is still here. I will fix.
| public void* PinnedPointer => _pointer; | ||
| public void* PinnedPointer { | ||
| get { | ||
| if (_pointer == null) throw new InvalidOperationException(); |
There was a problem hiding this comment.
|
|
||
| public override void Retain() | ||
| { | ||
| if (IsDisposed) throw new InvalidOperationException(); |
There was a problem hiding this comment.
Use BufferPrimitivesThrowHelper?
|
|
||
| public OwnedArray(T[] array) | ||
| { | ||
| if (array == null) throw new ArgumentNullException(nameof(array)); |
There was a problem hiding this comment.
Use BufferPrimitivesThrowHelper?
| return new Span<byte>(Slab.Array, _offset, _length); | ||
| } | ||
| if (IsDisposed) PipelinesThrowHelper.ThrowObjectDisposedException(nameof(MemoryPoolBlock)); | ||
| if (length > _length - index) throw new ArgumentOutOfRangeException(); |
There was a problem hiding this comment.
Is this check redundant? https://github.com/dotnet/corefx/blob/master/src/System.Memory/src/System/Span.cs#L91
There was a problem hiding this comment.
It's not. The slab array is very large and the span ctor will often work
There was a problem hiding this comment.
Should it then be if (length > _length - (_offset + index))?
There was a problem hiding this comment.
No, because _length is not the length of the whole buffer, but rather the length of the section represented by this OwnedBuffer.
| if (IsDisposed) PipelinesThrowHelper.ThrowObjectDisposedException(nameof(MemoryPoolBlock)); | ||
| pointer = (Slab.NativePointer + _offset + index).ToPointer(); | ||
| return true; | ||
| if (index > _length) throw new ArgumentException(nameof(index)); |
| newStart = 0; | ||
| newEnd = length; | ||
| return buffer; | ||
| return (OwnedArray<byte>)buffer; |
There was a problem hiding this comment.
Why is this cast necessary?
There was a problem hiding this comment.
implicit cast caannot go through two hops (array -> OwnedArray -> OwnedBuffer)
|
@KrzysztofCwalina I have been experimenting with your refactor today partly to enable I wonder if it makes sense to create a method that is called when a BufferHandle is released: protected internal abstract void ReleaseHandle(); I tried to refactor the code a little to remove the lifetime management from the surface that I have pushed the first part of my experiment mjp41@944933e |
1e650f7 to
2c2483a
Compare
|
@mj1856, how is ReleaseHandle different from Release? Also, BufferSource has RetainHandle and ReleaseHandle. How are these different from Retain/Release? |
|
So
At the moment, I hooked them up to calling the |
|
Sorry, I still don't understand the difference. OwnedBuffer.Retain/Release can do whatever the subclasses want, i.e. don't have to do lifetime management. |
|
So for |
|
Ah, I get it. You want to have different lifetime management strategy for stack bound handles and heap ownedbuffer. Though, I have to say I am not a big fan of having two similar but different APIs. I really think we need to simplify lifetime management and I am not sure if this is a simplification or additional complexity that users/implementers would have to think about. Let me think about it a bit more. |
…, preview2-25305-01, respectively (dotnet#1528)
…otnet#1516) * Renaming charactersWritten/Consumed to codePointsWritten/Consumed. * Renaming codePoints to codeUnits * Updating Utf8TextEncoder arg name to match public surface of TextEncoder.
|
@davidfowl, any more feedback on this? |
| } | ||
| } | ||
|
|
||
| private ArraySegment<byte> _buffer; |
There was a problem hiding this comment.
Private fields belong at the top of the class.
davidfowl
left a comment
There was a problem hiding this comment.
There are a few style nits.
…rzysztofCwalina/corefxlab into PreparingToChangeOBToInterface
|
@KrzysztofCwalina @mjp41 Is there a reason |
|
@KodrAus, they seem to be equivalent. Buffer.Retain, just calls OwnedBuffer.Retain. |
|
@KrzysztofCwalina I guess the difference is |
The perf (elapsed time and instructions retired) of E2EPipelineTests benchmark improved very slightly 0.5%, but consistently.
cc: @davidfowl, @mjp41, @ahsonkhan, @shiftylogic