-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Proposal: MemoryMarshal.GetArrayDataReference<T>(T[,]) overload #35528
Comments
On a side note, @GrabYourPitchforks I have a few questions for you about the changes you made in #1036, I'd like to get a decent understanding of what's going on there but I'm a bit confused 🤔
Just to be extremely clear, I'm not in any way trying to criticize the design/implementation there, I'd just love to understand why the code is structured this way in particular and to learn something new. 😄 |
Good questions!
|
Hey, thank you so much for taking the time to reply, I really appreciate it! 😊 It all makes way more sense now, in particular I was completely missing that point about generic methods not being inlined correctly in that case, that cleared things up quite a bit! And yes by "intrinsics" I was indeed referring to APIs in I guess I have one more question related to these two things you said:
Wouldn't that cause a decent performance hit though? I mean, what I was thinking for this mov eax, [rcx] ; null check
lea rax, [rcx+0x18] ; skip length, pad, HxW, bounds
ret Essentially, what can already be achieved with: public static ref T DangerousGetReference<T>(this T[,] array)
{
var arrayData = Unsafe.As<RawArray2DData>(array);
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
return ref r0;
}
private sealed class RawArray2DData
{
public IntPtr Length;
public int Height;
public int Width;
public int HeightLowerBound;
public int WidthLowerBound;
public byte Data;
} With the difference being that it would be baked in into the BCL, and possibly with the JIT intrinsics you mentioned so that it would also work well in R2R scenarios. I mean, considering that this would be a very performance-oriented API, I'd argue that devs using this would also very likely not be using custom bounds at all in the first place, and even if they were, they should just have to handle them manually after retrieving the reference. This would also make the behavior consistent with Curious to know what you guys think, thanks again for your time! 😄 |
Sure, but the following assembly would work for any-rank MD array: ; assume rcx := obj reference
mov rdx, qword ptr [rcx] ; rdx := pMethodTable, also handles null check
mov edx, dword ptr [rdx + offset_to_basesize] ; edx := pMethodTable->BaseSize
lea rax, [rcx + rdx + some_const_offset] ; rax := addr of where first element in MD array would be
ret Given the relative rarity of MD arrays in general when compared to SZ arrays, I don't think it's worth optimizing away a single instruction for the rank-2 MD array case. |
I see. I wonder what the performance hit could be though in scenarios where this API was used in a tight loop, as that second instruction involves yet another memory access 🤔 Just my two cents, but I often had the feeling that the low usage of ND arrays (especially 2D arrays) was sort of the chicken and the egg problem. By that I mean, they're not so commonly used, so they get low support. The codegen for them wasn't as optimized as it could've been (see #35293), and as an example, when the I do think that at least for the 2D case, it might be nice to have some dedicated, optimized APIs. There are many scenarios (eg. image processing) where they would be a very user friendly data structure to use, as opposed to SZ arrays with manual offsetting, or even worse, jagged arrays (with all their added bonus of much higher page faults thanks to their discontinuous memory layout) 😄 I'm glad I could start a conversation around this! |
The API proposal suggested here is for the absolute lowest of low level consumers. The 0.00001% audience. By definition it's not friendly since it drops into unsafe code and assumes expert knowledge by its callers. Additionally, it's not really expected that people will call this in a tight loop. If you look at existing consumers, they often call this before entering a tight loop, but it's not within the loop body itself. Unless you're in the habit of accessing element 0 over and over and over again, I suppose. :) If the goal were instead to make MD arrays more user-friendly generally, including giving them better codegen for common scenarios, that's laudable! But I think the issue #35293 that you had already pointed to is the right place for that discussion. |
Absolutely, I agree that an API like this (same as with the existing public readonly struct ArrayColumn<T>
{
private readonly T[,] array;
private readonly int height;
private readonly int width;
private readonly int column;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArrayColumn(T[,] array, int column)
{
// Assign fields...
}
public ref T this[int y]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if ((uint)y >= (uint)this.height)
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionForInvalidColumn();
}
ref T r0 = ref MemoryMarshal.GetArrayDataReference(this.array);
ref T r1 = ref Unsafe.Add(ref r0, this.column + y * this.width);
return ref r1;
}
}
} This is just to make an example, but you can see how in a situation like this, caching the initial reference would not be possible for users (without going through extra hoops and handling the offsetting manually, which defeats the point of having a helper type like this), while having access to a fast API like I think having APIs like these would be useful for library authors to provide more efficient types that can be used by general developers that are not expected to know how to use any of these unsafe APIs. Again, this was just to clarify what I meant about this API also being used directly in tight loops. And I agree that reusing the same method with the added lookup to the method table would simplify the codebase and avoid additional overloads, I was just wondering about the performance hit of the second memory lookup in cases where caching the reference and reuse it directly is not an option. On this point, personally I think it'd be nice to have dedicated, more efficient overloads at least for 2D arrays (if not 3D arrays as well) with the base size being baked in, and then have a general purpose API only targeting Again, I really appreciate you taking the time to have this conversation 😊 |
Pinging @bartonjs and @joperezr as area owners, was wondering about the necessary steps to move this to ready for review. I'm absolutely fine with @GrabYourPitchforks's idea for how to make the implementation easier to share across the various overloads, and personally I wouldn't mind even a single overload just taking As for examples of usage, this would help a lot in the |
No need for the method to be generic. It could return Lines 188 to 189 in c5b6881
A That would make the public API: public static class MemoryMarshal
{
// strawman
public static ref byte GetMDArrayDataRef(Array arr);
} |
Personally, I would be fine with that too, since the important bit would just be to have a portable API to reliably get a Please correct me if I'm wrong, from what I can tell here we basically have:
My initial proposal was like this last point, for consistency with |
The proposed Ideally, we would make this return Note that |
Ooh, I see, Wait, should Michal edit the first post of his issue then, or did I read that wrong? He says:
That in bold is the bit that led me to assume that API would've worked the same in this scenario. As for Levi's proposal, in case returning I wasn't aware of Side point, for my specific use case which is restricted to 2D and 3D arrays instead, I think I can just keep using the portable approach for now (as in, before this API possibly ships with .NET 6)? As in, basically the equivalent of what the OG EDIT: nevermind, I just realized I couldn't have used that API anyway since I need to work on arrays of managed types too, and I don't think those can actually be pinned, without possibly even more hacks. |
I assumed that the public API would do what the internal GetRawData does. The internal GetRawData does not do this. |
Right, I see. I think then we should either update the description in #28001 if that doesn't reflect the actual behavior that that proposed API would have, or otherwise decide that the public API would behave as currently described, in which case that would basically replace the proposed API in this issue, as they'd essentially be identical when used on arrays? 🤔 |
Spoke with Sergio offline and he said taking |
Just to clarify, would we want to keep the generic parameters and technically allow consumers to break type safety when getting a reference (since they could just pass whatever type argument and get the returned reference reinterpreted to it), or just make the returned reference a Having the So in general, what do we want the final signature of this API to be? 🙂 |
If desired, we could force the user to pass a generic argument, even though the method would still take just |
Approved as a System.Array-based overload of GetArrayDataReference. There's an open question of what happens if the array is an array of reference types: is byte the right answer for the return type? namespace System.Runtime.InteropServices
{
partial class MemoryMarshal
{
public static ref byte GetArrayDataReference(Array arr);
}
} |
@jkotas, could you weigh in on the concern raised? Specifically is |
I do not see a problem with it. It is super unsafe API though.
If we changed the API to return |
Thanks! I don't have a particular preference here but might lean towards the little additional safety. I'd think it shouldn't be problematic in practice as the cost of a type check (particularly if we are already going to check SZ vs MD) seems negligible compared to It also seems like something that could be "statically" determined in a few cases, since you will often have a I'll defer to what the runtime team feels is the correct approach, however. |
Getting the address of the first element for any arbitrary array (including an MDArray) is trivial and shouldn't add measurable overhead. See #35528 (comment) for more info.
It would make the API safer. However, we do not perform this same check for |
Making the API safer will make it also less usable. You won't be able to call it unless you have the We actually have very similar APIs available already |
Edit by @GrabYourPitchforks: See alternative API proposal at #35528 (comment).
Overview
This issue is about adding a new API to
System.Runtime.InteropServices.MemoryMarshal
:This would just be an overload of the existing
MemoryMarshal.GetArrayDataReference<T>(T[])
API (introduced here). Additional overloads for 3/4/4D arrays could be considered as well.Motivation/behavior
Same as
GetArrayDataReference<T>(T[])
, just extended to more array types. As an additional note, this API would ignore custom lower bounds, if present, and always just return a reference to the first array element, if the array is not empty, no matter its expected index. This API is expected to offer the same behavior as the overload for SZ arrays, and if custom lower bounds are present it's up to individual developers to track them correctly after retrieving the reference.This API would also be particularly useful considering the fact that the standard index accessor for ND arrays is much slower than the one for SZ arrays (related issue on this point: #35293).
cc. @tannergooding
The text was updated successfully, but these errors were encountered: