1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 // ===========================================================================
5 // File: TieredCompilation.CPP
7 // ===========================================================================
14 #include "win32threadpool.h"
15 #include "tieredcompilation.h"
17 // TieredCompilationManager determines which methods should be recompiled and
18 // how they should be recompiled to best optimize the running code. It then
19 // handles logistics of getting new code created and installed.
22 // # Current feature state
24 // This feature is incomplete and currently experimental. To enable it
25 // you need to set COMPLUS_EXPERIMENTAL_TieredCompilation = 1. When the environment
26 // variable is unset the runtime should work as normal, but when it is set there are
27 // anticipated incompatibilities and limited cross cutting test coverage so far.
28 // Profiler - Anticipated incompatible with ReJIT, untested in general
29 // ETW - Anticipated incompatible with the ReJIT id of the MethodJitted rundown events
30 // Managed debugging - Anticipated incompatible with breakpoints/stepping that are
31 // active when a method is recompiled.
34 // Testing that has been done so far largely consists of regression testing with
35 // the environment variable off + functional/perf testing of the Music Store ASP.Net
36 // workload as a basic example that the feature can work. Running the coreclr repo
37 // tests with the env var on generates about a dozen failures in JIT tests. The issues
38 // are likely related to assertions about optimization behavior but haven't been
39 // properly investigated yet.
41 // If you decide to try this out on a new workload and run into trouble a quick note
42 // on github is appreciated but this code may have high churn for a while to come and
43 // there will be no sense investing a lot of time investigating only to have it rendered
44 // moot by changes. I aim to keep this comment updated as things change.
47 // # Important entrypoints in this code:
50 // a) .ctor and Init(...) - called once during AppDomain initialization
51 // b) OnMethodCalled(...) - called when a method is being invoked. When a method
52 // has been called enough times this is currently the only
53 // trigger that initiates re-compilation.
54 // c) OnAppDomainShutdown() - called during AppDomain::Exit() to begin the process
55 // of stopping tiered compilation. After this point no more
56 // background optimization work will be initiated but in-progress
57 // work still needs to complete.
61 // Methods initially call into OnMethodCalled() and once the call count exceeds
62 // a fixed limit we queue work on to our internal list of methods needing to
63 // be recompiled (m_methodsToOptimize). If there is currently no thread
64 // servicing our queue asynchronously then we use the runtime threadpool
65 // QueueUserWorkItem to recruit one. During the callback for each threadpool work
66 // item we handle as many methods as possible in a fixed period of time, then
67 // queue another threadpool work item if m_methodsToOptimize hasn't been drained.
69 // The background thread enters at StaticOptimizeMethodsCallback(), enters the
70 // appdomain, and then begins calling OptimizeMethod on each method in the
71 // queue. For each method we jit it, then update the precode so that future
72 // entrypoint callers will run the new code.
76 // The overall principle is don't swallow terminal failures that may have corrupted the
77 // process (AV for example), but otherwise for any transient issue or functional limitation
78 // that prevents us from optimizing log it for diagnostics and then back out gracefully,
79 // continuing to run the less optimal code. The feature should be constructed so that
80 // errors are limited to OS resource exhaustion or poorly behaved managed code
81 // (for example within an AssemblyResolve event or static constructor triggered by the JIT).
83 #ifdef FEATURE_TIERED_COMPILATION
85 // Called at AppDomain construction
86 TieredCompilationManager::TieredCompilationManager() :
87 m_isAppDomainShuttingDown(FALSE),
88 m_countOptimizationThreadsRunning(0),
89 m_callCountOptimizationThreshhold(30),
90 m_optimizationQuantumMs(50)
92 LIMITED_METHOD_CONTRACT;
93 m_lock.Init(LOCK_TYPE_DEFAULT);
96 // Called at AppDomain Init
97 void TieredCompilationManager::Init(ADID appDomainId)
108 SpinLockHolder holder(&m_lock);
109 m_domainId = appDomainId;
112 // Called each time code in this AppDomain has been run. This is our sole entrypoint to begin
113 // tiered compilation for now. Returns TRUE if no more notifications are necessary, but
114 // more notifications may come anyways.
116 // currentCallCount is pre-incremented, that is to say the value is 1 on first call for a given
118 BOOL TieredCompilationManager::OnMethodCalled(MethodDesc* pMethodDesc, DWORD currentCallCount)
120 STANDARD_VM_CONTRACT;
122 if (currentCallCount < m_callCountOptimizationThreshhold)
124 return FALSE; // continue notifications for this method
126 else if (currentCallCount > m_callCountOptimizationThreshhold)
128 return TRUE; // stop notifications for this method
131 // Insert the method into the optimization queue and trigger a thread to service
132 // the queue if needed.
134 // Terminal exceptions escape as exceptions, but all other errors should gracefully
135 // return to the caller. Non-terminal error conditions should be rare (ie OOM,
136 // OS failure to create thread) and we consider it reasonable for some methods
137 // to go unoptimized or have their optimization arbitrarily delayed under these
138 // circumstances. Note an error here could affect concurrent threads running this
139 // code. Those threads will observe m_countOptimizationThreadsRunning > 0 and return,
140 // then QueueUserWorkItem fails on this thread lowering the count and leaves them
141 // unserviced. Synchronous retries appear unlikely to offer any material improvement
142 // and complicating the code to narrow an already rare error case isn't desirable.
144 SListElem<MethodDesc*>* pMethodListItem = new (nothrow) SListElem<MethodDesc*>(pMethodDesc);
145 SpinLockHolder holder(&m_lock);
146 if (pMethodListItem != NULL)
148 m_methodsToOptimize.InsertTail(pMethodListItem);
151 if (0 == m_countOptimizationThreadsRunning && !m_isAppDomainShuttingDown)
153 // Our current policy throttles at 1 thread, but in the future we
154 // could experiment with more parallelism.
155 m_countOptimizationThreadsRunning++;
159 return TRUE; // stop notifications for this method
165 if (!ThreadpoolMgr::QueueUserWorkItem(StaticOptimizeMethodsCallback, this, QUEUE_ONLY, TRUE))
167 SpinLockHolder holder(&m_lock);
168 m_countOptimizationThreadsRunning--;
169 STRESS_LOG1(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OnMethodCalled: "
170 "ThreadpoolMgr::QueueUserWorkItem returned FALSE (no thread will run), method=%pM\n",
176 SpinLockHolder holder(&m_lock);
177 m_countOptimizationThreadsRunning--;
178 STRESS_LOG2(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OnMethodCalled: "
179 "Exception queuing work item to threadpool, hr=0x%x, method=%pM\n",
180 GET_EXCEPTION()->GetHR(), pMethodDesc);
182 EX_END_CATCH(RethrowTerminalExceptions);
184 return TRUE; // stop notifications for this method
187 void TieredCompilationManager::OnAppDomainShutdown()
189 SpinLockHolder holder(&m_lock);
190 m_isAppDomainShuttingDown = TRUE;
193 // This is the initial entrypoint for the background thread, called by
195 DWORD WINAPI TieredCompilationManager::StaticOptimizeMethodsCallback(void *args)
197 STANDARD_VM_CONTRACT;
199 TieredCompilationManager * pTieredCompilationManager = (TieredCompilationManager *)args;
200 pTieredCompilationManager->OptimizeMethodsCallback();
205 //This method will process one or more methods from optimization queue
206 // on a background thread. Each such method will be jitted with code
207 // optimizations enabled and then installed as the active implementation
208 // of the method entrypoint.
210 // We need to be carefuly not to work for too long in a single invocation
211 // of this method or we could starve the threadpool and force
212 // it to create unnecessary additional threads.
213 void TieredCompilationManager::OptimizeMethodsCallback()
215 STANDARD_VM_CONTRACT;
217 // This app domain shutdown check isn't required for correctness
218 // but it should reduce some unneeded exceptions trying
219 // to enter a closed AppDomain
221 SpinLockHolder holder(&m_lock);
222 if (m_isAppDomainShuttingDown)
224 m_countOptimizationThreadsRunning--;
229 ULONGLONG startTickCount = CLRGetTickCount64();
230 MethodDesc* pMethod = NULL;
233 ENTER_DOMAIN_ID(m_domainId);
238 SpinLockHolder holder(&m_lock);
239 pMethod = GetNextMethodToOptimize();
240 if (pMethod == NULL ||
241 m_isAppDomainShuttingDown)
243 m_countOptimizationThreadsRunning--;
248 OptimizeMethod(pMethod);
250 // If we have been running for too long return the thread to the threadpool and queue another event
251 // This gives the threadpool a chance to service other requests on this thread before returning to
253 ULONGLONG currentTickCount = CLRGetTickCount64();
254 if (currentTickCount >= startTickCount + m_optimizationQuantumMs)
256 if (!ThreadpoolMgr::QueueUserWorkItem(StaticOptimizeMethodsCallback, this, QUEUE_ONLY, TRUE))
258 SpinLockHolder holder(&m_lock);
259 m_countOptimizationThreadsRunning--;
260 STRESS_LOG0(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OptimizeMethodsCallback: "
261 "ThreadpoolMgr::QueueUserWorkItem returned FALSE (no thread will run)\n");
267 END_DOMAIN_TRANSITION;
271 STRESS_LOG2(LF_TIEREDCOMPILATION, LL_ERROR, "TieredCompilationManager::OptimizeMethodsCallback: "
272 "Unhandled exception during method optimization, hr=0x%x, last method=%pM\n",
273 GET_EXCEPTION()->GetHR(), pMethod);
275 EX_END_CATCH(RethrowTerminalExceptions);
278 // Jit compiles and installs new optimized code for a method.
279 // Called on a background thread.
280 void TieredCompilationManager::OptimizeMethod(MethodDesc* pMethod)
282 STANDARD_VM_CONTRACT;
284 _ASSERTE(pMethod->IsEligibleForTieredCompilation());
285 PCODE pJittedCode = CompileMethod(pMethod);
286 if (pJittedCode != NULL)
288 InstallMethodCode(pMethod, pJittedCode);
292 // Compiles new optimized code for a method.
293 // Called on a background thread.
294 PCODE TieredCompilationManager::CompileMethod(MethodDesc* pMethod)
296 STANDARD_VM_CONTRACT;
299 ULONG sizeOfCode = 0;
302 CORJIT_FLAGS flags = CORJIT_FLAGS(CORJIT_FLAGS::CORJIT_FLAG_MCJIT_BACKGROUND);
303 flags.Add(CORJIT_FLAGS(CORJIT_FLAGS::CORJIT_FLAG_TIER1));
305 if (pMethod->IsDynamicMethod())
307 ILStubResolver* pResolver = pMethod->AsDynamicMethodDesc()->GetILStubResolver();
308 flags.Add(pResolver->GetJitFlags());
309 COR_ILMETHOD_DECODER* pILheader = pResolver->GetILHeader();
310 pCode = UnsafeJitFunction(pMethod, pILheader, flags, &sizeOfCode);
314 COR_ILMETHOD_DECODER::DecoderStatus status;
315 COR_ILMETHOD_DECODER header(pMethod->GetILHeader(), pMethod->GetModule()->GetMDImport(), &status);
316 pCode = UnsafeJitFunction(pMethod, &header, flags, &sizeOfCode);
321 // Failing to jit should be rare but acceptable. We will leave whatever code already exists in place.
322 STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::CompileMethod: Method %pM failed to jit, hr=0x%x\n",
323 pMethod, GET_EXCEPTION()->GetHR());
325 EX_END_CATCH(RethrowTerminalExceptions)
330 // Updates the MethodDesc and precode so that future invocations of a method will
331 // execute the native code pointed to by pCode.
332 // Called on a background thread.
333 void TieredCompilationManager::InstallMethodCode(MethodDesc* pMethod, PCODE pCode)
335 STANDARD_VM_CONTRACT;
337 _ASSERTE(!pMethod->IsNativeCodeStableAfterInit());
339 PCODE pExistingCode = pMethod->GetNativeCode();
340 #ifdef FEATURE_INTERPRETER
341 if (!pMethod->SetNativeCodeInterlocked(pCode, pExistingCode, TRUE))
343 if (!pMethod->SetNativeCodeInterlocked(pCode, pExistingCode))
346 //We aren't there yet, but when the feature is finished we shouldn't be racing against any other code mutator and there would be no
347 //reason for this to fail
348 STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::InstallMethodCode: Method %pM failed to update native code slot. Code=%pK\n",
353 Precode* pPrecode = pMethod->GetPrecode();
354 if (!pPrecode->SetTargetInterlocked(pCode, FALSE))
356 //We aren't there yet, but when the feature is finished we shouldn't be racing against any other code mutator and there would be no
357 //reason for this to fail
358 STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::InstallMethodCode: Method %pM failed to update precode. Code=%pK\n",
364 // Dequeues the next method in the optmization queue.
365 // This should be called with m_lock already held and runs
366 // on the background thread.
367 MethodDesc* TieredCompilationManager::GetNextMethodToOptimize()
369 STANDARD_VM_CONTRACT;
371 SListElem<MethodDesc*>* pElem = m_methodsToOptimize.RemoveHead();
374 MethodDesc* pMD = pElem->GetValue();
381 #endif // FEATURE_TIERED_COMPILATION