-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Enabled ByReference<T> usage in ref-like types #3367
Enabled ByReference<T> usage in ref-like types #3367
Conversation
Thanks Sergio0694 for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am not a religious man, but if there is a god of code he will damn this to the deepest depths of hell.
That being said, LGTM!
Here's an idea: you could even get rid of that Something like: public bool HasValue => !Unsafe.AreSame(ref reference.Value, ref Unsafe.AsRef<T>(null)) |
@ltrzesniewski Ahahah yeah I like it! Not sure why I hadn't thought about that before, thanks! 😄🚀 EDIT: done in 224a23c. |
CI build is fine and all tests are passing, it actually worked! 🚀🚀🚀 Thank you @ltrzesniewski again so much for the help setting this up! 🍻 |
This is needed so that when disassembling the ByReference<T> type imported from the HighPerformance package installed from NuGet, the size of the struct is correctly reported (previously it would show up as 1 byte instead). This change has no actual influence on how the type works, it's purely to improve user experience when using tools such as Re#'s disassembler
Just to be 1000% sure, I've also tested this locally, installing the preview |
I'm curious: how is the codegen better after removing |
Hey @ltrzesniewski - you're absolutely correct that throw helper methods should not be inlined, yup!
So basically, using Consider this simple example: public ref struct ByRef
{
private int foo;
public int Foo
{
get
{
if (foo == 0)
{
ThrowException();
}
return foo;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowException()
{
throw new InvalidOperationException("Foo");
}
} This gives us the following: ByRef.get_Foo()
L0000: push rsi
L0001: sub rsp, 0x20
L0005: mov rsi, rcx
L0008: cmp dword ptr [rsi], 0
L000b: jne short L0012
L000d: call ByRef.ThrowException()
L0012: mov eax, [rsi]
L0014: add rsp, 0x20
L0018: pop rsi
L0019: ret You can see how the correct path (that This instead is the codegen when we remove that ByRef.get_Foo()
L0000: sub rsp, 0x28
L0004: mov eax, [rcx]
L0006: test eax, eax
L0008: je short L000f
L000a: add rsp, 0x28
L000e: ret
L000f: call ByRef.ThrowException()
L0014: int3 You can see that the JIT can now correctly see that the method we invoke is indeed always throwing, so it switches the branch condition (we now have Also in this case the resulting assembly is also slightly more optimized, in particular the second version is not using Also you can see the faulting path now just issues an |
Thanks for the detailed explanation! 😄 |
Right, and yeah even then users could still end up with issues if they use it with incorrect arguments (as the reference can't be validated to be inside the given object). That makes perfect sense, I'll just add that as a general note then 👍
Oh sorry, I misread that! I hadn't seen the note about an empty array and thought that was just a general disclaimer to basically say "this is extremely dangerous and we don't encourage you to use this for indexing, so use this at your own risk" 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sergio0694 can you just add a doc PR and link here? Then I think we'll be good to move forward with this update?
@@ -0,0 +1,26 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a fake project/build thing for trickery and nothing that ends up in the final bits/package right?
Should we call that out in a comment above as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a note for that in the ByReference<T>
file, as I thought that would've probably be the first file new devs would've tried to open when loading the project, as opposed to just double clicking the actual .csproj
file in a text editor? 🤔
I can move this in the .csproj
file if you feel it's better, let me know! 😊
@michael-hawker Opened a doc PR with the update: MicrosoftDocs/WindowsCommunityToolkitDocs#396.
|
@Sergio0694 this is waiting on #3356 as well, eh? |
@michael-hawker Yup, waiting on that one to get merged so that I can properly setup the necessary attributes and code paths here when on the .NET 5 target so that the new Will sync this PR and get it ready for review as soon as #3356 is merged! |
The documentation for this package is at https://docs.microsoft.com . It has a high chance of creating confusion with Microsoft customers that this package is supported by Microsoft and the .NET team. Can we move the community toolkit documentation to some other place before this is merged? |
@jkotas this package is supported by Microsoft, that's my role here. It's because of that support that we ship under the I don't think we've had cases of anyone in the past filing issues on the .NET repos for issues with the Toolkit specifically. If you've seen evidence contrary to this, please let me know. |
If this PR is merged, this package won't be supported by Microsoft anymore: If somebody calls Microsoft support that they are getting crash in GC, it will get to my team. If we trace the crash to this package, we will say that it is not a supported scenario. |
I guess the basic question is - what happens if a customer reports an issue and it's traced back to this? Presumably the resolution would be for the community toolkit to back out the |
Converting this back to draft until we can detail all the implications if we ship this. In the meantime @Sergio0694 can you provide some metrics in your use-cases (or others like @ltrzesniewski's) on the perf benefits this provides? If anything even if we don't ship this PR, we can use it to help vet and aid in the proposed features for .NET 6. |
Going to close this - the Thank you @jkotas and @GrabYourPitchforks for the input and for your time, really appreciate it! 😊 |
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Since the
ByReference<T>
type is not public, we needed to hack our way around it by using aSpan<T>
and theMemoryMarshal.CreateSpan<T>(ref T, int)
API. This worked, but was slower and used more memory (types needed to store an entireSpan<T>
instead of just a reference). It also introduced overhead when accessing the actual reference from theSpan<T>
field.What is the new behavior?
Following an incredible discovery shared by @ltrzesniewski (here), we're now externally making the
ByReference<T>
type public through a fake assembly referenced by the .NET Core targets in theHighPerformance
package. As a result, alll theref
-like types in the package (eg.Ref<T>
) now use that behind the scenes, offering the best performance possible. 🎉Why is this a BIG deal?
The ability to directly use
ByReference<T>
completely removes the overhead from our ref-like types (Ref<T>
,ReadOnlyRef<T>
, etc.). This is because if we just need aByReference<T>
field in our types, the whole type only has the size equal to a native integer on any given platform (a pointer size), which allows the whole type to be stored in a single register. This effectively makes these types a 1:1 replacement forref
variables as far as performance goes! 🚀Consider the following simple example:
This used to produce the following, before this PR:
Note how the reference needs to actually be loaded into a register first (as
Ref<int>
itself was 16 bytes large (!) on x64, so it could only be passed by reference as it couldn't fit into a single register). Then once the reference was loaded, the final value could be accessed. It worked fine, but it was really not ideal especially for tight loops. In fact theRef<T>
type was primarily meant to be used for convenience (ie. to haveref T
fields inref struct
types), but it couldn't replace manual code with eg.Unsafe.Add
in critical paths). Compare that with the update codegen, with the changes from this PR:Here you can see the code is virtually identical to the one produced by a native
ref int
value: theRef<T>
value fits in a single register (as it's just theIntPtr
size now) and it's treated by the JIT exactly like any otherref T
argument. Basically theRef<T>
type now has no overhead at all over native reference types! 🎉Size reductions
As mentioned above, this greatly improves the efficiency and size of all the ref-like types in the package.
In particular, on x64:
Ref<T>
: 16 bytes ---> 8 bytesReadOnlyRef<T>
: 16 bytes ---> 8 bytesNullableRef<T>
: 16 bytes ---> 8 bytesNullableReadOnlyRef<T>
: 16 bytes ---> 8 bytesThe
[ReadOnly]NullableRef<T>
types we have the same size reduction: in this case we're also completely removing the need for a secondary field to track whether or not we do have a value (like inNullable<T>
), because we can simply derive that value by checking whether the wrapper reference is in fact anull
reference. Thanks to @ltrzesniewski for the suggestion here 😄PR Checklist
Please check if your PR fulfills the following requirements:
Sample in sample app has been added / updated (for bug fixes / features)Icon has been created (if new sample) following the Thumbnail Style Guide and templates