Adding {ReadOnly}Memory, OwnedMemory, MemoryHandle, and IRetainable#13583
Conversation
| { | ||
| IRetainable _owner; | ||
| void* _pointer; | ||
| GCHandle _handle; |
There was a problem hiding this comment.
Nit: private should be specified for the above fields.
| // else, object _arrayOrOwnedMemory is a T[] | ||
| readonly object _arrayOrOwnedMemory; | ||
| readonly int _index; | ||
| readonly int _length; |
There was a problem hiding this comment.
Nit: private should be specified for the above fields.
| /// Thrown when the specified <paramref name="start"/> is not in the range (<0 or >=Length). | ||
| /// </exception> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public Memory(T[] array, int start) |
There was a problem hiding this comment.
Should this ctor be removed to match Span<T>?
There was a problem hiding this comment.
I asked the same question. However, we don't have other two argument Memory ctors that Span has such as the one that takes void* or the one that takes ref T. We removed it from Span<T> because of usability concerns (https://github.com/dotnet/corefx/issues/23471). I am not sure if we have such concerns for Memory<T> (yet).
I will still remove it to be consistent though.
| // else, object _arrayOrOwnedMemory is a T[] | ||
| readonly object _arrayOrOwnedMemory; | ||
| readonly int _index; | ||
| readonly int _length; |
There was a problem hiding this comment.
Nit: private should be specified for the above fields.
| // There is no need to 'and' _index by the bit mask here | ||
| // since the constructor will set the highest order bit again anyway | ||
| if (memory._index < 0) | ||
| return new ReadOnlyMemory<T>(Unsafe.As<OwnedMemory<T>>(memory._arrayOrOwnedMemory), memory._index, memory._length); |
There was a problem hiding this comment.
Is this use of Unsafe.As really required - can we use regular cast instead? It makes the class even less safe than it needs to be.
| </data> | ||
| </root> | ||
| <data name="Memory_ThrowIfDisposed" xml:space="preserve"> | ||
| <value>The memory has been disposed.</value> |
There was a problem hiding this comment.
Nit: it's a bit odd to me to see "memory" here in lower case when talking about disposing of it. It'd make more sense to me if it were either The Memory<T> has been disposed. or The memory has been freed., but the combination just strikes me weirdly.
| <value>The memory has been disposed.</value> | ||
| </data> | ||
| <data name="Memory_OutstandingReferences" xml:space="preserve"> | ||
| <value>Outstanding references detected that need to be released first</value> |
There was a problem hiding this comment.
Nit: This sentence reads strangely to me. Let's change it to something like "Release all references before disposing this instance." or something like that. I read this sentence the first time and thought that "outstanding references" were detecting something.
| <value>Outstanding references detected that need to be released first</value> | ||
| </data> | ||
| <data name="Memory_PointerIsNull" xml:space="preserve"> | ||
| <value>Cannot get pointer since it is null.</value> |
There was a problem hiding this comment.
Nit: how about just "The pointer is null." or "Cannot get the pointer value when it is null."
| { | ||
| _pointer = pinnedPointer; | ||
| _handle = handle; | ||
| _owner = owner; |
There was a problem hiding this comment.
Nit: please reorder these lines to match both the order of the fields being declared and the order of the parameters to the ctor
| { | ||
| get | ||
| { | ||
| if (_pointer == null) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Memory_PointerIsNull); |
There was a problem hiding this comment.
Why throw here? If someone passes null to the ctor, shouldn't they be able to access null from the property? What are we preventing by adding this?
| { | ||
| public abstract class OwnedMemory<T> : IDisposable, IRetainable | ||
| { | ||
| protected OwnedMemory() { } |
|
|
||
| public abstract Span<T> AsSpan(int index, int length); | ||
|
|
||
| public virtual Span<T> AsSpan() |
There was a problem hiding this comment.
Why does this need to be virtual when the AsSpan(int, int) overload is already abstract?
There was a problem hiding this comment.
I am not sure why we need it. I vaguely recall Krzysztof mentioning a performance penalty.
@KrzysztofCwalina, can you help answer this?
There was a problem hiding this comment.
Regarding why it is virtual. I am not really sure given both IsDisposed and AsSpan(int, int) are both abstract.
There was a problem hiding this comment.
The reason it was made virtual is that you can imagine a faster implementation than delegating to Span(int, int). But I just discussed this with @ahsonkhan: we would like to remove the AsSpan(int, int) method, make the AsSpan() abstract, and ask callers who want a section of the ownedmemory to call AsSpan().Slice(index, len)
| if (IsDisposed) ThrowHelper.ThrowObjectDisposedException(nameof(OwnedMemory<T>), ExceptionResource.Memory_ThrowIfDisposed); | ||
| return new ReadOnlyMemory<T>(this, 0, Length); | ||
| } | ||
| } |
There was a problem hiding this comment.
Why do we need ReadOnlyMemory? We expose Memory, which is efficiently convertible to ReadOnlyMemory, right? It just seems inconsistent when, for example, we don't expose both AsSpan and AsReadOnlySpan, just AsSpan.
Also, from a naming perspective, why is it AsSpan but just Memory rather than having Span/Memory or AsSpan()/AsMemory()?
| public void Dispose() | ||
| { | ||
| if (IsRetained) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Memory_OutstandingReferences); | ||
| Dispose(true); |
There was a problem hiding this comment.
Generally with the Dispose pattern, the base class' Dispose() calls GC.SuppressFinalize() in case a derived class has a finalizer.
|
|
||
| public abstract bool Release(); | ||
|
|
||
| internal static readonly T[] EmptyArray = new T[0]; |
There was a problem hiding this comment.
Why is this needed rather than callers using Array.Empty<T>()?
There was a problem hiding this comment.
From: dotnet/corefxlab#1569 (comment)
We don't have it in NS 1.1, at least I don't see it.
I will use Array.Empty<T>() here but the impl in System.Memory will need to have this EmptyArray property.
| readonly int _index; | ||
| readonly int _length; | ||
|
|
||
| private const int bitMask = 0x7FFFFFFF; |
There was a problem hiding this comment.
Actually, in seeing the usage of it in the code, it'd be nice if the name specified what it was a bit mask for, e.g. RemoveOwnedFlagBitMask or something like that.
| _arrayOrOwnedMemory = owner; | ||
| _index = index | (1 << 31); // Before using _index, check if _index < 0, then 'and' it with bitMask | ||
| _length = length; | ||
| } |
There was a problem hiding this comment.
The only public constructors here take a T[]. What if I have a T* and an int length and I want to call a method that takes a Memory<T>? I need to create a custom OwnedMemory<T>-derived type for that? Should we provide it?
There was a problem hiding this comment.
We have a type in corefxlab but there is still design/API work left for that. https://github.com/dotnet/corefxlab/blob/master/src/System.Buffers.Experimental/System/Buffers/OwnedNativeBuffer.cs
Shall I file an issue to track this?
There was a problem hiding this comment.
What if I have a
T*and anintlength and I want to call a method that takes aMemory<T>? I need to create a customOwnedMemory<T>-derived type for that? Should we provide it?
I discussed with Krzysztof, and we should provide an implementation of OwnedMemory<T> but not a Memory<T> ctor that takes a pointer and length.
cc @KrzysztofCwalina, anything else to add?
| /// <summary> | ||
| /// Returns an empty <see cref="Memory{T}"/> | ||
| /// </summary> | ||
| public static Memory<T> Empty { get; } = OwnedMemory<T>.EmptyArray; |
There was a problem hiding this comment.
There are tradeoffs, so this is just a question, but did you consider instead making this:
public static Memory<T> Empty => Array.Empty<T>();?
| else | ||
| { | ||
| var handle = GCHandle.Alloc(Unsafe.As<T[]>(_arrayOrOwnedMemory), GCHandleType.Pinned); | ||
| void* pointer = Unsafe.Add<T>((void*)handle.AddrOfPinnedObject(), _index); |
There was a problem hiding this comment.
I'd previously seen other methods of getting the address be a bit faster:
dotnet/corefx#22170 (comment)
Just wondering if you looked at other approaches here (and maybe it doesn't matter).
There was a problem hiding this comment.
I had not looked at the other approaches. This is interesting. I will try what Jan suggested.
There was a problem hiding this comment.
The two approaches below do not give the same memory address:
var array = new byte[1000];
var handle = GCHandle.Alloc(array, GCHandleType.Pinned);
void* pointer1 = (void*)handle.AddrOfPinnedObject();
void* pointer2 = Unsafe.AsPointer(ref handle);
Console.WriteLine(new IntPtr(pointer1) + ":" + new IntPtr(pointer2)); // Output: 2201489332176:670203108448There was a problem hiding this comment.
The two approaches below do not give the same memory address
The address of the handle is different from the address wrapped by the handle.
There was a problem hiding this comment.
Hmm. That makes sense. But then how can we get the address without using AddrOfPinnedObject()?
There was a problem hiding this comment.
AddrOfPinnedObject might be perfectly fine. But other approaches you could try would be:
void* pointer;
fixed (void* tmp = array) pointer = tmp;or
void* pointer = array.Length > 0 ?
Unsafe.AsPointer(ref array[0]) :
null;or:
void* pointer = Marshal.UnsafeAddrOfPinnedArrayElement(array, 0);etc.
There was a problem hiding this comment.
Question: Do approaches 2 and 3 work because the array has been pinned by our GCHandle.Alloc call and will not get moved by the GC? If we don't have the GCHandle.Alloc(array, GCHandleType.Pinned); call and get the pointer without using fixed, the array could still move, correct?
There was a problem hiding this comment.
Correct. And even in the first case, once we leave the fixed block, the array could still be moved if it weren't for the previous Alloc.
| } | ||
| else | ||
| { | ||
| memoryHandle = new MemoryHandle(null); |
There was a problem hiding this comment.
If Retain is meant to keep the object alive, then it seems like we still need to store the array into the handle, so as to keep a reference to it. Imagine, for example, the array contained finalizable objects; if I did:
T[] array = ...;
MemoryHandle h = new Memory<T>(array).Retain();
array = null;
...I would expect as long as I'm still holding onto h that the objects in the array wouldn't get finalized, but with this code as it stands, they could be finalized the moment I set array = null.
There was a problem hiding this comment.
Will this work?
var array = Unsafe.As<T[]>(_arrayOrOwnedMemory);
void* pointer = Unsafe.Add<T>((void*)Unsafe.AsPointer(ref array[0]), _index);
memoryHandle = new MemoryHandle(null, pointer);There was a problem hiding this comment.
Will this work?
No. Nothing is keeping the array alive here (and the pointer isn't valid unless something else is pinning the array).
There was a problem hiding this comment.
As implemented today (and envisioned), Retain is not a guarantee that the buffer will be kept alive. It just guarantees that the reference the caller has to the buffer will work (as opposed to being disposed/deleted/etc).
I am not saying that this is the right behavior we should ship, but rather that this is what Retain was originally designed to do.
There was a problem hiding this comment.
If we wanted to change the behavior/design, we could change the MemoryHandle._owner field to be type of Object and store either IRetainable or T[] in the filed. It would keep the array alive. But it could keep the array alive for longer than needed in some cases. It's a tradeoff. I am ok with either behavior.
There was a problem hiding this comment.
I don't have a strong opinion, either. Just seemed strange that Retain() doesn't actually "retain" in that case.
There was a problem hiding this comment.
As to pinning, when the pin parameter is true, we create GC handle for the array, and so I am not sure I understand the comments about the pointer not being pinned. It's pinned when pin == true and it's not pinned when it's false (but then the pointer property returns null)
There was a problem hiding this comment.
so I am not sure I understand the comments about the pointer not being pinned
The comments are in response to Ahson's "will this work" example.
| } | ||
|
|
||
| /// <summary> | ||
| /// Copies the contents of this span from the memory into a new array. This heap |
There was a problem hiding this comment.
Nit: "of this span"... this isn't a span
| if (obj is ReadOnlyMemory<T> readOnlyMemory) | ||
| { | ||
| return readOnlyMemory.Equals(this); | ||
| } |
There was a problem hiding this comment.
The pattern matching is syntax on structs doesn't produce ideal results. This code:
if (obj is ReadOnlyMemory<T> readOnlyMemory) return readOnlyMemory.Equals(this);results in the IL:
IL_0000: ldarg.1
IL_0001: stloc.1
IL_0002: ldloc.1
IL_0003: isinst valuetype ReadOnlyMemory`1<!T>
IL_0008: ldnull
IL_0009: cgt.un
IL_000b: dup
IL_000c: brtrue.s IL_0019
IL_000e: ldloca.s 2
IL_0010: initobj valuetype ReadOnlyMemory`1<!T>
IL_0016: ldloc.2
IL_0017: br.s IL_001f
IL_0019: ldloc.1
IL_001a: unbox.any valuetype ReadOnlyMemory`1<!T>
IL_001f: stloc.0
IL_0020: brfalse.s IL_0031
IL_0022: ldloca.s 0
IL_0024: ldarg.0
IL_0025: constrained. valuetype ReadOnlyMemory`1<!T>
IL_002b: callvirt instance bool [mscorlib]System.Object::Equals(object)
IL_0030: ret
whereas this code:
if (obj is ReadOnlyMemory<T>) return ((ReadOnlyMemory<T>)obj).Equals(this);results in the IL:
IL_0000: ldarg.1
IL_0001: isinst valuetype ReadOnlyMemory`1<!T>
IL_0006: brfalse.s IL_001e
IL_0008: ldarg.1
IL_0009: unbox.any valuetype ReadOnlyMemory`1<!T>
IL_000e: stloc.0
IL_000f: ldloca.s 0
IL_0011: ldarg.0
IL_0012: constrained. valuetype ReadOnlyMemory`1<!T>
IL_0018: callvirt instance bool [mscorlib]System.Object::Equals(object)
IL_001d: ret
There was a problem hiding this comment.
doesn't produce ideal results
To clarify, it doesn't produce an incorrect result though, correct?
Are you suggesting I change the impl to this since it produces smaller IL?
if (obj is ReadOnlyMemory<T>) return ((ReadOnlyMemory<T>)obj).Equals(this);There was a problem hiding this comment.
it doesn't produce an incorrect result though, correct?
Correct
Are you suggesting I change the impl to this since it produces smaller IL?
Yes
| { | ||
| return | ||
| _arrayOrOwnedMemory == other._arrayOrOwnedMemory && | ||
| (_index & bitMask) == (other._index & bitMask) && |
There was a problem hiding this comment.
Why is the bit masking necessary?
There was a problem hiding this comment.
I was thinking of something like this:
var array = new byte[100];
var memory = new Memory<byte>(array);
var ownedMemory = (OwnedMemory<byte>)array;
var memoryOwned = ownedMemory.Memory;
Assert.False(memory .Equals(memoryOwned)); // This will succeed even without the bit masking The bit masking isn't necessary. The first equality catches if the object is an array or an OwnedMemory.
| { | ||
| _arrayOrOwnedMemory = owner; | ||
| _index = index | (1 << 31); // Before using _index, check if _index < 0, then 'and' it with bitMask | ||
| _length = length; |
There was a problem hiding this comment.
Don't we still need to validate arguments? OwnedMemory's Length is abstract, so a derived OwnedMemory could, for example, return a negative value out of Length, which is then passed into this Memory ctor and stored here.
There was a problem hiding this comment.
This is an internal constructor. I don't follow why the argument validation is necessary (which will add overhead to Slice calls).
I could add a null check for owner (and index/length >= 0), but what type of validation are you suggesting? We aren't accessing OwnedMemory.Length property here.
We have the following checks in Slice already:
public Memory<T> Slice(int start, int length)
{
if ((uint)start > (uint)_length || (uint)length > (uint)(_length - start))
ThrowHelper.ThrowArgumentOutOfRangeException();There was a problem hiding this comment.
This is an internal constructor.
It's called by OwnedMemory here:
https://github.com/dotnet/coreclr/pull/13583/files#diff-0184445324edc32335f6249267a0bf70R29
and there's no validation done on the Length, which provides an arbitrary result based on whatever the derived type chose to return:
https://github.com/dotnet/coreclr/pull/13583/files#diff-0184445324edc32335f6249267a0bf70R14
which means I can write an OwnedMemory-derived type that returns -42 for Length, and then get a Memory that has a negative length.
Now, maybe we don't care about that? After all, lots of other things could go wrong. But since we're doing some argument validation elsewhere, makes me question why not here.
| /// If unable to get the array segment, return false with a default array segment. | ||
| /// </summary> | ||
| [EditorBrowsable(EditorBrowsableState.Never)] | ||
| public bool DangerousTryGetArray(out ArraySegment<T> arraySegment) |
There was a problem hiding this comment.
We call it Dangerous here because it gives access that's not denoted by the type name (you can get the array to write to even though the type is called "read only"). But a similar issue exists on Memory<T>: the Memory<T> could have been constructed for a specific range of an array, but calling TryGetArray gives you back an ArraySegment<T>, which exposes the Array, and thus potentially gives you access to portions of data that you weren't supposed to. Do we care? I'm wondering if that one should be called Dangerous, too, or maybe we don't care about that distinction?
There was a problem hiding this comment.
Do you suggest we mark TryGetArray with the [EditorBrowsable(EditorBrowsableState.Never)] attribute as well (along with calling it Dangerous)?
There was a problem hiding this comment.
No, I'd leave it visible. And to be clear, I also wasn't pushing for Memory.TryGetArray to be renamed, rather making sure we though through whether it should be or not.
There was a problem hiding this comment.
I think ArraySegment already communicates that array items outside of the segment's range should not be accessed.
| { | ||
| var array = (T[])_arrayOrOwnedMemory; | ||
| void* pointer = array.Length > 0 ? | ||
| Unsafe.Add<T>((void*)Unsafe.AsPointer(ref array[0]), _index) : |
There was a problem hiding this comment.
@jkotas, instead of using Unsafe.Add, we could (and should) do the following, correct? Should we still keep the newly added Unsafe.Add<T>(void*...) API?
void* pointer = array.Length > 0 ? Unsafe.AsPointer(ref array[_index]) : null;Edit: Nevermind, we still need Unsafe.Add for:
void* pointer = Unsafe.Add<T>((void*)handle.AddrOfPinnedObject(), _index);|
@dotnet-bot test Tizen armel Cross Debug Build |
1 similar comment
|
@dotnet-bot test Tizen armel Cross Debug Build |
|
Any other feedback or is this PR good to go? |
|
|
||
| public void Dispose() | ||
| { | ||
| if (IsRetained) ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Memory_OutstandingReferences); |
There was a problem hiding this comment.
💡 These one-line statements are particularly difficult to read.
There was a problem hiding this comment.
❗️ I'm not a fan of the interaction between IRetainable and IDisposable for this type. I would expect the Dispose semantics to be equivalent to a case where the constructor called Retain and then Dispose calls release, except with additional handling to ensure Dispose is idempotent.
There was a problem hiding this comment.
We want to enable types with reference counting which means that a concrete OwnedMemory can call retain/release multiple times before disposing.
@KrzysztofCwalina, anything you would like to add?
There was a problem hiding this comment.
📝 This comment is related to my suggestion to review the semantics of ReferenceCountedDisposable<T> just to see if it's possible to unify the concepts.
| { | ||
| if (array == null) | ||
| ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); | ||
| if (default(T) == null && array.GetType() != typeof(T[])) |
There was a problem hiding this comment.
❓ Why is this restriction needed? I don't like that it means the implicit conversion operator can throw an exception that is difficult for a programmer to avoid in code.
There was a problem hiding this comment.
I am using the same restrictions as what exist for Span:
https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/Span.cs#L4
There was a problem hiding this comment.
@jkotas Can you shed some light on this (blame shows you as introducing the original validation for Span<T>)?
There was a problem hiding this comment.
This check is required because Span does not support covariance. Look for covariance in Span design doc. https://github.com/dotnet/corefxlab/blob/master/docs/specs/span.md
This check has the same effect as ref T dummy = ref array[0] except when array has zero length.
Array covariance is rarely used, but it comes with performance penalty paid by everybody and complex implementation. We did not want to carry either of these to Span/Memory.
| /// <summary> | ||
| /// Defines an implicit conversion of an array to a <see cref="Memory{T}"/> | ||
| /// </summary> | ||
| public static implicit operator Memory<T>(T[] array) => new Memory<T>(array); |
There was a problem hiding this comment.
❓ Since this is an implicit conversion, why are we not handling null by returning default?
=> array != null ? new Memory<T>(array) : default;There was a problem hiding this comment.
I am following the same semantics as Span:
https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/Span.cs#L325
There was a problem hiding this comment.
❓ Is Memory<T> supposed to be a ref struct in the end?
There was a problem hiding this comment.
No. It is what Span is for. Memory is non-ref counter-part of Span.
Memory design doc: https://github.com/dotnet/corefxlab/blob/master/docs/specs/memory.md
| /// <summary> | ||
| /// Defines an implicit conversion of a <see cref="ArraySegment{T}"/> to a <see cref="Memory{T}"/> | ||
| /// </summary> | ||
| public static implicit operator Memory<T>(ArraySegment<T> arraySegment) => new Memory<T>(arraySegment.Array, arraySegment.Offset, arraySegment.Count); |
There was a problem hiding this comment.
❓ Since this is an implicit conversion, why are we not handling default(ArraySegment<T>) by returning default?
=> arraySegment.Array != null ? new Memory<T>(...) : default;There was a problem hiding this comment.
Same as Span. If arraySegment.Array is null, we throw.
| /// <summary> | ||
| /// Returns an empty <see cref="Memory{T}"/> | ||
| /// </summary> | ||
| public static Memory<T> Empty => Array.Empty<T>(); |
There was a problem hiding this comment.
💡 This is somewhat inefficient for each caller due to the use of a conversion operator and constructor code. Consider using { get; } = instead of =>.
There was a problem hiding this comment.
Can you please expand on why it is inefficient?
There was a problem hiding this comment.
The conversion of T[] to Memory<T> invokes a constructor with two argument checks. You can either perform those argument checks once, or every time the Empty property is evaluated. When you use the => form, it's evaluated every time. The { get; } = form will create a static backing field for the property which is initialized once, and simply return the cached value each time the Empty property is evaluated.
There was a problem hiding this comment.
Or you use a private ctor that doesn't do any checks.
|
💡 My strongest recommendation here would be to carefully review the semantics of |
| ThrowHelper.ThrowArgumentOutOfRangeException(); | ||
|
|
||
| if (_index < 0) | ||
| return new ReadOnlyMemory<T>((OwnedMemory<T>)_arrayOrOwnedMemory, (_index & RemoveOwnedFlagBitMask) + start, _length - start); |
There was a problem hiding this comment.
❓ Is it possible to wrap this cast (which appears several times) in a property that uses the Unsafe class to avoid type checks? If so, would this have a performance win that justifies the use of unsafe? /cc @stephentoub
There was a problem hiding this comment.
Races in user code can make index bit to be out of sync with _arrayOrOwnedMemory. Casts are useful to diagnose this situation before it corrupts the system. If the casts are replaced by Unsafe casts, this situation will tend lead to very hard to diagnose crash during GC.
There was a problem hiding this comment.
Races in user code can make index bit to be out of sync with _arrayOrOwnedMemory. Casts are useful to diagnose this situation before it corrupts the system. If the casts are replaced by Unsafe casts, this situation will tend lead to very hard to diagnose crash during GC.
| } | ||
| else | ||
| { | ||
| var array = (T[])_arrayOrOwnedMemory; |
There was a problem hiding this comment.
This path looks pretty dangerous. It returns unpinned pointer to array. What is going to guarantee that the array is pinned?
There was a problem hiding this comment.
Ignore this comment. I see that the issue is being discussed in #13583 (comment) already.
| else | ||
| { | ||
| var array = (T[])_arrayOrOwnedMemory; | ||
| void* pointer = array.Length > 0 ? |
There was a problem hiding this comment.
Slightly better way to do this that would be more consistent with the pinned case is: void * pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index);.
And for the pinned case, you can use:
var array = (T[])_arrayOrOwnedMemory;
var handle = GCHandle.Alloc(array, GCHandleType.Pinned);
void * pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index);
memoryHandle = new MemoryHandle(null, pointer, handle);
There was a problem hiding this comment.
Slightly better way to do this that would be more consistent with the pinned case is
That's still not going to keep the array alive, though, right? (Nor keep the pointer valid if nothing else is pinning it, though the pointer value isn't actually needed here.)
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static implicit operator ReadOnlyMemory<T>(Memory<T> memory) | ||
| { | ||
| if (memory._index < 0) |
There was a problem hiding this comment.
Did you experiment with doing
var arrayOrOwnedMemory = _arrayOrOwnedMemory;
if (arrayOrOwnedMemory.GetType() != typeof(T[]))
return new ReadOnlyMemory<T>(Unsafe.As<OwnedMemory<T>>(arrayOrOwnedMemory), memory._index & RemoveOwnedFlagBitMask, memory._length);
return new ReadOnlyMemory<T>(Unsafe.As<T[]>(arrayOrOwnedMemory), memory._index, memory._length); rather than the sign of index trick? This would also give type safety, but may perform better?
There was a problem hiding this comment.
This does not work when the array type is not exact. E.g.:
enum X {
}
X[] a = new X[100];
uint[] b = (uint[])(object)a;
Console.WriteLine(b.GetType());
There was a problem hiding this comment.
I thought you were ruling out Covariant typing on Memory and Span? Or do I not understand your example.
There was a problem hiding this comment.
Sorry I hadn't spotted the
default(T) == null in the constructor. I guess this means you could create a Span of type "uint" where the underlying array is of type "X". Why do you need/want this condition?
There was a problem hiding this comment.
Covariant typing
For references, but not for primitive valuetypes - in the current implementation at least.
I am not sure whether covariance is the right name for this behavior of primitive valuetypes. Covariance is asymmetric, but the relationship between uint[] and X[] in the above example is symmetric.
There was a problem hiding this comment.
Why do you need/want this condition?
This condition is there to eliminate the type check for valuetypes. If we remove it, creating Span from byte[] will be slower. We do not want creating Span from byte[] to be slower.
There was a problem hiding this comment.
That applies to Span, but it doesn't apply to Memory, e.g. the Span property has a cast inside:
return new Span<T>((T[])_arrayOrOwnedMemory, _index, _length); However, I can see you want them to be consistent.
Are there cases where byte[] can be viewed at a different type? If not, the JIT could optimize GetType on anything that is forced to be a single type.
There was a problem hiding this comment.
Are there cases where byte[] can be viewed at a different type
Yes.
enum X : byte {
}
X[] a = new X[100];
byte[] b = (byte[])(object)a;
Console.WriteLine(b.GetType());
|
I have addressed the remaining open comments. Please let me know if I missed anything. |
|
I think we should look into ReferenceCountedDisposable, but as a separate workitem, and a potential DRC. For now, the PR looks good to me. |
|
@dotnet-bot test Tizen armel Cross Debug Build |
|
@dotnet-bot test Windows_NT x86 full_opt ryujit CoreCLR Perf Tests Correctness |
|
@dotnet-bot test Ubuntu x64 Checked Build and Test |
Part of dotnet/corefxlab#1711
Adding the following types:
Adding the following Unsafe APIs that are needed by Memory<T> (these already exist in corefx):
Order of changes:
cc @shiftylogic, @KrzysztofCwalina, @davidfowl, @jkotas, @stephentoub