-
Notifications
You must be signed in to change notification settings - Fork 2.6k
GetPinnableReference on String.cs #23428
GetPinnableReference on String.cs #23428
Conversation
|
@jkotas It's typical that we'd put |
|
Yes, this should follow the pattern used for Span. Also, this should be marked as |
|
@simplejackcoder - For reference to what Jan and I were discussing above, see specifically these two lines for the appropriate attribute patterns:
coreclr/src/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs Line 106 in a593024
|
| /// <summary> | ||
| /// Returns a reference to the first element of the String. If the string is null, an access will throw a NullReferenceException. | ||
| /// </summary> | ||
| [EditorBrowsable(EditorBrowsableState.Never)] |
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.
Prefer fully-qualifying these here rather than introducing using aliases at the top of the file.
|
This is regressing codegen for number of Before: After: @dotnet/jit-contrib Thoughts? |
|
Possibly a consequence of #15706. The jit is very conservative now when it sees a mixture of calls and references to pinned locals. What does the IL look like? |
|
@jkotas You might remember that same stack-spill pattern with the recent public unsafe int GetCharCount(ReadOnlySpan<byte> bytes)
{
fixed (byte* pBytes = &MemoryMarshal.GetReference(bytes))
{
return GetCharCountFast(pBytes, bytes.Length);
}
}Like this instead to avoid the unnecessary stack spills: public unsafe int GetCharCount(ReadOnlySpan<byte> bytes)
{
ref byte rBytes = ref MemoryMarshal.GetReference(bytes);
int length = bytes.Length;
// Don't reference 'pBytes' once it's pinned, as all accesses read from the stack rather than registers
fixed (byte* pBytes = &rBytes)
{
// rBytes and length are both still enregistered here
return GetCharCountFast((byte*)Unsafe.AsPointer(ref rBytes), length);
}
} |
Did you happen to open a JIT bug on this? Nobody should be writing convoluted code like this to get good codegen for this. |
No. The ultimate decision was that it wasn't worth the complexity to save three or four instructions, and we backed out the change and just kind of forgot about it. |
|
I opened #23437 to track the underlying JIT issue. |
|
Does it mean that this PR will regress performance and should not currently be accepted? |
|
@simplejackcoder There's nothing wrong with the PR in my opinion. It's very straightforward and is a useful API addition. I'd recommend leaving the PR open for now while we investigate the JIT behavior on our end. |
|
Left some notes over in #23437, but basically this is a register allocator issue. We have conflicting lifetimes and the allocator spills the pinned value. Diff from the "before" picture is largely that we are now pinning the byref instead of the string itself; this alters the lifetimes the allocator sees and presents a different interference graph. Diffs will be most prominent in a tiny method like For this bit of framework code, we might also consider hoisting the span creation up so that I'm going to look into why the allocator isn't anti-preferencing RDX as the target register; doing so might encourage the allocator to use an extra register to get around the conflict like it does in the before case. But at this point it looks like there might not be an easy fix on the jit side. We could also address this via load CSEs of the DNER pin slot (and that would have many other benefits) but that is too risky/ambitious for 3.0. Or (similar) teach the allocator that there is no point spilling a copy of a DNER local as the value can just be reloaded at its use point. |
Should have been obvious, but since we pin the byref via a local and then use the local, and the local is in memory as DNER (do not enregister), there's no way for the allocator to connect the byref creation with its ultimate use at the call. So I don't see any short-term fix in the jit for this. @CarolEidt feel free to jump in, as I may have overlooked the ability of the allocator to anticipate the need to spill here and do something more clever (see notes on the "string case" over in #23437). I think the diffs from this change are generally a wash. There are a few ugly cases like the above, and other cases where codegen is somewhat better (redundant null checks are removed). Perf for |
jkotas
left a comment
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.
Ok, let's take this
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Signed-off-by: dotnet-bot <[email protected]>
* GetPinnableReference on String.cs * Add attributes for debugger and performance Commit migrated from dotnet/coreclr@7c4717e
This is a feature improvement for https://github.com/dotnet/coreclr/issues/23359
Please let me know if there is more to do this seem too easy!