You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, all pinned locals are always stored on the stack. This makes pinning not really ideal for hot paths.
It would be nice if the JIT added support for using a register to store pinned locals, when possible.
As mentioned by @tannergooding, the register would need to be cleared when out of scope to stop tracking.
The method A from above could then become something like this:
movrbx,rcxcall0x00007ffaecb70028xorrbx,rbxret
Here I just used rbx to store the pinned local (just picked the first callee-saved register). I do realize there's plenty of work to make this work and all the various GC data structures need to be updated accordingly to enable tracking, this is the general idea.
Goes without saying that this optimization could bring some nice codegen/perf wins in interop-heavy code 😄
Additionally, given this could be used to restore the RuntimeHelpers.GetHashCode optimization by porting the happy path to C# (as I did in #55273, but possibly without the GC hole ahah), it would automatically speedup virtually every dictionary out there using random reference types as keys. Or, any other data structure that would call GetHashCode at some point on an object that didn't override the default object.GetHashCode implementation.
Goes without saying that this optimization could bring some nice codegen/perf wins in interop-heavy code
I'm not super convinced of the wins here myself.
I think that being able to elide the pin in the case that the pinned value is coming from the stack would be better: #40553
The "friendly signature" for something like BOOL QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency) is bool QueryPerformanceFrequency(out long frequency), in which case the typical usage is likely if (QueryPerformanceFrequency(out var frequency)). In that scenario, the out has to be pinned, even though the P/Invoke wrapper, once inlined, could see that pinning a stack local isn't required.
Regular pinning is effectively a stack spill however, and in the terms of "typical" method calls, isn't going to be much more expensive than the spilling or register shuffling that already typically happens to meet the callee/caller saved register requirements. Further, in the case of P/Invoke, this spill is nothing compared to the transition stub which already spills basically everything to ensure that any GC tracked data is not in a register (see #54107 (comment), which shows the disassembly for such a transition).
I can still think of many cases where the stack spill for the pinning would be the only one. Like, this would be the case for RuntimeHelpers.GetHashCode, which otherwise is all entirely inlined and with all locals enregistered. I would imagine that'd be nice to optimize given the speedup it could give downstream to all users indirectly relying on it for hashcode-based data structures. Also, there could be plenty of other cases where the pinning is the biggest overhead in a method (eg. ComPtr<T>.GetPinnableReference() comes to mind, which would otherwise basically be free if the pinned local was enregistered. All in all I guess I'm just saying, #40553 would sure be nice to have too, but I don't see why this one would be mutually exclusive. Wouldn't it be nice to have both? 😄
For reference, I did try to use fixed as a follow up to #55273, ie:
publicstaticunsafeintGetHashCode(object?o){if(ois not null){uintsyncBlockValue;
fixed (byte*pData=&o.GetRawData()){syncBlockValue=((ObjectHeader*)&((nint*)pData)[-2])->SyncBlockValue;}constuintBIT_SBLK_IS_HASH_OR_SYNCBLKINDEX=0x08000000;constuintBIT_SBLK_IS_HASHCODE=0x04000000;constuintBITS_IS_VALID_HASHCODE=BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX|BIT_SBLK_IS_HASHCODE;constintHASHCODE_BITS=26;constuintMASK_HASHCODE=(1u<<HASHCODE_BITS)-1u;if((syncBlockValue&BITS_IS_VALID_HASHCODE)==BITS_IS_VALID_HASHCODE){returnunchecked((int)(syncBlockValue&MASK_HASHCODE));}}returnInternalGetHashCode(o);}
The performance though was way worse than the current one:
Method
Branch
Mean
Error
StdDev
Ratio
RatioSD
Code Size
GetHashCode
PR
0.6446 ns
0.0021 ns
0.0018 ns
1.45
0.03
78 B
GetHashCode
main
0.4462 ns
0.0102 ns
0.0091 ns
1.00
0.00
9 B
The asm in particular was... Interesting:
; ObjectGetHashCodeBenchmark.GetHashCode()subrsp,28xoreax,eaxmov[rsp+20],raxmovrcx,[rcx+8]testrcx,rcxje short M00_L00learax,[rcx+8]mov[rsp+20],raxmovrax,[rsp+20]moveax,[rax+0FFF4]xoredx,edxmov[rsp+20],rdxmovedx,eaxandedx,0C000000cmpedx,0C000000jne short M00_L00andeax,3FFFFFFjmp short M00_L01M00_L00:call System.Runtime.CompilerServices.RuntimeHelpers.InternalGetHashCode(System.Object)M00_L01:nopaddrsp,28ret; Total bytes of code 78
Looks like there's lots of room for improvements to the codegen in this case? 🤔
I will also say: I still think an Unsafe.AtomicAddByteOffsetAndRead intrinsic would be useful.
It'd both solve this case (no need to use fixed at all), and it'd potentially be useful in other scenarios as well.
As in, the code above could then just be:
publicstaticunsafeintGetHashCode(object?o){if(ois not null){refbytedataRef=refo.GetRawData();nintheaderData=Unsafe.AtomicAddByteOffsetAndRead<nint>(refdataRef,-8);uintsyncBlockValue=((ObjectHeader*)&headerData)->SyncBlockValue;// Rest of the code (with no GC holes)}returnInternalGetHashCode(o);}
I mean yes, that was just an example (plus you'd get the same anyway with a negative offset).
My point was just "some API that can atomically subtract and read with GC tracking and no pinning" 🙂
Of course, such an API would only be allowed to read primitives, or even just int/nuint, doesn't really matter.
In NativeAOT we do have GetHashcode implemented in managed code. Also thin locks.
All operations with ObjectHeader need to pin and some code paths are otherwise fairly simple. Releasing a thin lock, for example is just a few checks for rare cases and then setting a bit in the header.
The part that pinning introduces stack locals hurts the scenario somewhat.
Activity
tannergooding commentedon Jan 5, 2022
I'm not super convinced of the wins here myself.
I think that being able to elide the
pinin the case that thepinned valueis coming from the stack would be better: #40553The "friendly signature" for something like
BOOL QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency)isbool QueryPerformanceFrequency(out long frequency), in which case the typical usage is likelyif (QueryPerformanceFrequency(out var frequency)). In that scenario, theouthas to be pinned, even though the P/Invoke wrapper, once inlined, could see that pinning a stack local isn't required.Regular pinning is effectively a
stack spillhowever, and in the terms of "typical" method calls, isn't going to be much more expensive than the spilling or register shuffling that already typically happens to meet the callee/caller saved register requirements. Further, in the case ofP/Invoke, this spill is nothing compared to the transition stub which already spills basically everything to ensure that any GC tracked data is not in a register (see #54107 (comment), which shows the disassembly for such a transition).Sergio0694 commentedon Jan 5, 2022
I can still think of many cases where the stack spill for the pinning would be the only one. Like, this would be the case for
RuntimeHelpers.GetHashCode, which otherwise is all entirely inlined and with all locals enregistered. I would imagine that'd be nice to optimize given the speedup it could give downstream to all users indirectly relying on it for hashcode-based data structures. Also, there could be plenty of other cases where the pinning is the biggest overhead in a method (eg.ComPtr<T>.GetPinnableReference()comes to mind, which would otherwise basically be free if the pinned local was enregistered. All in all I guess I'm just saying, #40553 would sure be nice to have too, but I don't see why this one would be mutually exclusive. Wouldn't it be nice to have both? 😄JulieLeeMSFT commentedon Jan 25, 2022
cc @kunalspathak.
Sergio0694 commentedon Jun 3, 2022
For reference, I did try to use
fixedas a follow up to #55273, ie:The performance though was way worse than the current one:
The asm in particular was... Interesting:
Looks like there's lots of room for improvements to the codegen in this case? 🤔
I will also say: I still think an
Unsafe.AtomicAddByteOffsetAndReadintrinsic would be useful.It'd both solve this case (no need to use
fixedat all), and it'd potentially be useful in other scenarios as well.As in, the code above could then just be:
SupinePandora43 commentedon Jun 3, 2022
@Sergio0694 more like
Unsafe.AtomicSubstractByteOffsetAndReadSergio0694 commentedon Jun 3, 2022
I mean yes, that was just an example (plus you'd get the same anyway with a negative offset).
My point was just "some API that can atomically subtract and read with GC tracking and no pinning" 🙂
Of course, such an API would only be allowed to read primitives, or even just
int/nuint, doesn't really matter.jakobbotsch commentedon Dec 12, 2022
This would likely fix #35748 too.
VSadov commentedon Feb 28, 2024
I'd like to +1 this.
In NativeAOT we do have
GetHashcodeimplemented in managed code. Also thin locks.All operations with
ObjectHeaderneed to pin and some code paths are otherwise fairly simple. Releasing a thin lock, for example is just a few checks for rare cases and then setting a bit in the header.The part that pinning introduces stack locals hurts the scenario somewhat.
VSadov commentedon Feb 28, 2024
Also see: #97997 , that reminded me of this issue.