Uh oh!
There was an error while loading. Please reload this page.
[NativeAOT] Simplifying access to thread static variables - #84566
Conversation
ghost
commented
Apr 10, 2023
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas Issue DetailsFixes: #84373
|
There was a problem hiding this comment.
The "fast" access got a lot simpler here. RhpGetThreadStaticBaseForType is what we want eventually not called, but directly inlined by the JIT into callers.
We could yet make this a bit simpler by removing an indirection into the array of storage instances. It may be somewhat challenging though.
There was a problem hiding this comment.
Is there any other technical reason why we don't have such assembly helper for Arm64 apart that it is not implemented?
There was a problem hiding this comment.
The helper for Arm64 is now implemented
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Is there any other technical reason why we don't have such assembly helper for Arm64 apart that it is not implemented?
VSadov
commented
Apr 11, 2023
no reason. Also no reason for the helper to be in assembly. |
kunalspathak
commented
Apr 11, 2023
are you thinking about it as part of this PR? |
VSadov
commented
Apr 11, 2023
yes. It would also make it easier to do further changes - no need to fix multiple helpers. |
kunalspathak
commented
Apr 11, 2023
What is the speedup from just this change? |
VSadov
commented
Apr 11, 2023
i think the cost of the call is still there, so this might not be a huge improvement. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
db1c5d2 to
723e2ecCompareVSadov
commented
Apr 12, 2023
I see about 15% speedup on x64. That is with both old and new implementations still making a call to the helper. With a simple microbenchmark which calls a method accessing bunch of threadstatics in a loop I see: the benchmark: usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;namespaceConsoleApp22{classC00{[ThreadStatic]publicstaticintt_i00;}classC01{[ThreadStatic]publicstaticintt_i01;}classC02{[ThreadStatic]publicstaticintt_i02;}classC03{[ThreadStatic]publicstaticintt_i03;}classC04{[ThreadStatic]publicstaticintt_i04;}classC05{[ThreadStatic]publicstaticintt_i05;}classC06{[ThreadStatic]publicstaticintt_i06;}classC07{[ThreadStatic]publicstaticintt_i07;}classC08{[ThreadStatic]publicstaticintt_i08;}classC09{[ThreadStatic]publicstaticintt_i09;}classC10{[ThreadStatic]publicstaticintt_i10;}classC11{[ThreadStatic]publicstaticintt_i11;}classC12{[ThreadStatic]publicstaticintt_i12;}classC13{[ThreadStatic]publicstaticintt_i13;}classC14{[ThreadStatic]publicstaticintt_i14;}classC15{[ThreadStatic]publicstaticintt_i15;}classC16{[ThreadStatic]publicstaticintt_i16;}classC17{[ThreadStatic]publicstaticintt_i17;}classC18{[ThreadStatic]publicstaticintt_i18;}classC19{[ThreadStatic]publicstaticintt_i19;}classC20{[ThreadStatic]publicstaticintt_i20;}classC21{[ThreadStatic]publicstaticintt_i21;}classC22{[ThreadStatic]publicstaticintt_i22;}classC23{[ThreadStatic]publicstaticintt_i23;}classC24{[ThreadStatic]publicstaticintt_i24;}classC25{[ThreadStatic]publicstaticintt_i25;}classC26{[ThreadStatic]publicstaticintt_i26;}classC27{[ThreadStatic]publicstaticintt_i27;}classC28{[ThreadStatic]publicstaticintt_i28;}classC29{[ThreadStatic]publicstaticintt_i29;}internalclassProgram{constintiters=1000000;staticvoidMain(string[]args){for(;;){Time(AccessTLS);}}staticvoidTime(Actiona){varsw=Stopwatch.StartNew();for(inti=0;i<100;i++){a();}sw.Stop();System.Console.WriteLine(sw.ElapsedMilliseconds);}staticvoidAccessTLS(){for(inti=0;i<iters;i++){OneTLSAccess();}}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticvoidOneTLSAccess(){C00.t_i00=C00.t_i00+C01.t_i01+C02.t_i02+C03.t_i03+C04.t_i04+C05.t_i05+C06.t_i06+C07.t_i07+C08.t_i08+C09.t_i09+C10.t_i10+C11.t_i11+C12.t_i12+C13.t_i13+C14.t_i14+C15.t_i15+C16.t_i16+C17.t_i17+C18.t_i18+C19.t_i19+C20.t_i20+C21.t_i21+C22.t_i22+C23.t_i23+C24.t_i24+C25.t_i25+C26.t_i26+C27.t_i27+C28.t_i28+C29.t_i29;}}} |
VSadov
commented
Apr 12, 2023
The codegen looks like the folllowing: before the changes: after the changes: |
| TypeManager* pSingleTypeManager = GetRuntimeInstance()->GetSingleTypeManager(); | ||
| if (pSingleTypeManager != NULL) | ||
| { | ||
| InitInlineThreadStatics(pSingleTypeManager); |
There was a problem hiding this comment.
This will make thread attach a potential source of unhandled OOM that leads to fail fast. This fail fast is impossible for user code to catch or recover from. I am not sure whether it is a good trade-off to make for native AOT. It will make native AOT less suitable for system-programming like tasks where uncatchable OOMs are a problem.
There was a problem hiding this comment.
One possible solution is to ignore the OOM on thread attach and live the storage as NULL and turn the AV on the first use of the threadstatic into an OOM exception. That could be inconvenient though when the access is JIT-inlined.
Or we can do one null-check for the whole thing and call the initializer on the first use.
There was a problem hiding this comment.
I think I will switch this to "allocate on first use" pattern. In such case the OOM is most likely to happen while creating managed Thread, either directly or indirectly by accessing managed thread ID.
It would still be possible to access a random threadstatic and get an unexpected OOM, but I think it would be always catcheable.
@MichalStrehovsky The codegen for the helper on Linux is indeed not very good. Unlike on Windows where it seems better than hand-written assembly. System.Collections.Tests`::RhpGetThreadStaticBaseForType(uint32_t):0x55555542c040 <+0>: pushrbp0x55555542c041 <+1>: movrbp,rsp0x55555542c044 <+4>: pushrbx0x55555542c045 <+5>: pushrax0x55555542c046 <+6>: movebx,edi-> 0x55555542c048 <+8>: movrax, qword ptr fs:[0x0]0x55555542c051 <+17>: learax,[rax-0xf0]0x55555542c058 <+24>: movrax, qword ptr [rax+0x98]0x55555542c05f <+31>: addebx,0x20x55555542c062 <+34>: movrax, qword ptr [rax+8*rbx]0x55555542c066 <+38>: addrsp,0x80x55555542c06a <+42>: poprbx0x55555542c06b <+43>: poprbp0x55555542c06c <+44>: retThis looks pretty bad. I wonder what makes the compiler confused. |
VSadov
commented
Apr 13, 2023
Are we forcing frame pointers somehow? |
MichalStrehovsky
commented
Apr 13, 2023
The frame is there on Linux because this is generated as a call. It gets turned into a mov by linker magic during linking. But linker magic is not able to mop up things around it. Linker magic can be only done when we produce executable. Not when we produce a shared library. This was a good article on Linux TLS I read some time ago: https://maskray.me/blog/2021-02-14-all-about-thread-local-storage |
VSadov
commented
Apr 13, 2023
Call to |
VSadov
commented
Apr 13, 2023
Right, we do disable frame pointer optimizations and that is likely causing this kind of code: runtime/eng/native/configurecompiler.cmake Line 407 in 0ac097f The reason is "to make it easier to profile". Perhaps CoreClr has other reasons for requiring frame pointers (but why on Unix only?) |
jkotas
commented
Apr 13, 2023
Profiling tools on Linux want to have RBP chain. We can double check whether it is still the case - I would expect it to be.
We do that on Windows too (look for |
MichalStrehovsky
commented
Apr 13, 2023
Fedora had a big discussion about it last year (didn't follow where that went but it is likely still an issue if they were having heated discussions about it): https://www.phoronix.com/news/Fedora-37-No-Omit-Frame-Pointer |
VSadov
commented
Apr 13, 2023
Not suppressing that optimization results in as good codegen as there could be: System.Collections.Tests`::RhpGetThreadStaticBaseForType(uint32_t):0x55555542be20 <+0>: pushrbx0x55555542be21 <+1>: movebx,edi-> 0x55555542be23 <+3>: movrax, qword ptr fs:[0x0]0x55555542be2c <+12>: learax,[rax-0xf0]0x55555542be33 <+19>: movrax, qword ptr [rax+0x98]0x55555542be3a <+26>: addebx,0x20x55555542be3d <+29>: movrax, qword ptr [rax+8*rbx]0x55555542be41 <+33>: poprbx0x55555542be42 <+34>: ret |
perf allows to choose DWARF, FP chain (a bit more lightweight), or LBR (very lightweight, but Intel-specific).
This would probably be the best option, if possible. |
MichalStrehovsky
commented
Apr 13, 2023
Thinking about this - does RyuJIT support the equivalent of |
On platforms that can use frame chains (all platforms except Win x64), RyuJIT is smart about adding the frame to more complex methods only, so that the trivial methods do not pay for the frame. Also, it does not allocate the frame pointer register so that methods with omitted frame do not break the frame chain. For the frame chain walking profiler, it looks as if there was more method inlining - some methods are omitted in the frame chain, but the frame chain still gives you pretty accurate picture of the callstack. Look for Also, there is |
VSadov
commented
Apr 13, 2023
GCC has multiple ways to do this (attributes, pragmas), but clang has none. There is also |
VSadov
commented
May 4, 2023
/azp run runtime-extra-platforms |
|
Azure Pipelines successfully started running 1 pipeline(s). |
VSadov
commented
May 4, 2023
NativeAOT OSX failures are #85600 |
VSadov
commented
May 4, 2023
mono failures appear to be unrelated. Either device connection, build or test failures that happen in a noop test run as well. #85694 |
Fixes: #84373