前言
首先來(lái)看下,為什么性能會(huì)一直持續(xù)性?xún)?yōu)化。.NET8引入的SSE-XMM(16字節(jié))Register和AVX-YMM(32字節(jié))Register是關(guān)鍵,傳統(tǒng)的Register一般指令集層次能移動(dòng)的最多只有8位,就算是最新的x64系統(tǒng)。但是SSE和AVX改變了這種局面,它們能一次性移動(dòng)64位系統(tǒng)的一倍乃至四倍,這就是優(yōu)化的關(guān)鍵。
之前的多篇文章展示了很多.NET8的性能優(yōu)化,基本上都是核心級(jí)的CLR/JIT優(yōu)化,包括了VM,Zeroing,CHRL,Exception,Non_GC,Branch,GC,Reflection,AOT,Enum,DateTime等等。但是漏掉了一個(gè)較為重要的東西:線程。本篇來(lái)看下.NET8里面的線程優(yōu)化。
.NET在新的版本中,對(duì)線程,并發(fā),并行,異步等方面做出了非常大的改進(jìn)。比如ThreadPool完全重寫(xiě),異步方法基礎(chǔ)部分的完全重寫(xiě),ConcurrentQueue隊(duì)列的完全重寫(xiě)等等。.NET8在這些的基礎(chǔ)上,進(jìn)行了更為深思熟慮的和更為有影響力的改進(jìn)。比如ThreadStatic。
.NET運(yùn)行時(shí)里面運(yùn)用本地?cái)?shù)據(jù)和線程的關(guān)聯(lián),就是本地線程存儲(chǔ)(TLS)。在托管代碼上實(shí)現(xiàn)這一點(diǎn),最常用的方法就是用[ThreadStatic]屬性注解一個(gè)靜態(tài)字段(當(dāng)然這里還有個(gè)用途更高級(jí)的ThreadLocal
例如以下ThreadStaitc屬性注解的用法
private static int s_onePerProcess; [ThreadStatic] private static int t_onePerThread;
在.NET8之前訪問(wèn)被TheadStatic標(biāo)記的字段,需要一個(gè)JIT的非內(nèi)聯(lián)輔助方法CORINFO_HELP_GETSHARED_NONGCTHREADSTATIC_BASE_NOCTOR。它的原型實(shí)際上就是JIT_GetSharedNonGCThreadStaticBase。如下:
#includeHCIMPL2(void*, JIT_GetSharedNonGCThreadStaticBase, DomainLocalModule *pDomainLocalModule, DWORD dwClassDomainID) { //為了便于觀看,此處省略 return HCCALL1(JIT_GetNonGCThreadStaticBase_Helper, pMT); } HCIMPLEND
因?yàn)檫@個(gè)方法本身是有優(yōu)化空間的,經(jīng)過(guò)dotnet/runtime#82973 and dotnet/runtime#85619它的函數(shù)本體被內(nèi)聯(lián)到了調(diào)用者當(dāng)中了。省略了函數(shù)調(diào)用以及跳轉(zhuǎn)的成本。通過(guò)一個(gè)基準(zhǔn)測(cè)試來(lái)看下這個(gè)效果。
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0 //dotnetrun-cRelease-fnet7.0--filter"*"--runtimesnativeaot7.0nativeaot8.0 using BenchmarkDotNet.Attributes; usingBenchmarkDotNet.Running; BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args); [HideColumns("Error", "StdDev", "Median", "RatioSD")] public partial class Tests { [ThreadStatic] private static int t_value; [Benchmark] public int Increment() => ++t_value; }
測(cè)試結(jié)果如下,提升明顯:
方法 | 運(yùn)行時(shí) | 平均值 | 比率 |
---|---|---|---|
Increment | .NET 7.0 | 8.492 ns | 1.00 |
Increment | .NET 8.0 | 1.453 ns | 0.17 |
同樣的通過(guò)
dotnet/runtime#84566 和 dotnet/runtime#87148為.NET AOT做的一個(gè)優(yōu)化,提升同樣明顯。
方法 | 運(yùn)行時(shí) | 平均值 | 比率 |
---|---|---|---|
Increment | NativeAOT 7.0 | 2.305 ns | 1.00 |
Increment | NativeAOT 8.0 | 1.325 ns | 0.57 |
ThreadPool
TheadPool優(yōu)化在于線程池方面,之前老版本的.NET基本上都是通過(guò)封裝Windows線程池,然后通過(guò)托管代碼調(diào)用。但是在.NET6里面開(kāi)始.NET運(yùn)行時(shí)實(shí)現(xiàn)了自己的托管線程池,也就是說(shuō)新版的.NET包含了兩個(gè)線程池。分別為托管調(diào)用的windows線程池,以及托管代碼自己實(shí)現(xiàn)的托管線程池。現(xiàn)在,在.NET8里面可以自由切換這兩個(gè)線程池,你想使用哪個(gè)就用哪個(gè),以提升程序的性能。
我們來(lái)看下,這個(gè)過(guò)程。首先新建一個(gè).NET8.0控制臺(tái)應(yīng)用程序,代碼如下
static void Main(string[] args) { Task.Run(() => Console.WriteLine(Environment.StackTrace)).Wait(); Console.ReadLine(); }
并在 .csproj 中添加
at System.Environment.get_StackTrace() at ThreadPool_.Program.<>c.b__0_0() in E:Visual Studio ProjectTest_ThreadPool_Program.cs:line 7 at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) at System.Threading.ThreadPoolWorkQueue.Dispatch() at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
PortableThreadPool這個(gè)就是.NET6以來(lái)新增的托管線程池操控的代碼。我們下面再來(lái)看下Windows線程池方面,把上面代碼進(jìn)行AOT編譯
dotnet publish -c Release -r win-x64
我們運(yùn)行下路徑inRelease et8.0win-x64publish里的exe文件,可以看到如下:
at System.Environment.get_StackTrace() + 0x21 at ThreadPool_.Program.<>c.b__0_0() + 0x9 at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread, ExecutionContext, ContextCallback, Object) + 0x3d at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task&, Thread) + 0xcc at System.Threading.ThreadPoolWorkQueue.Dispatch() + 0x289 at System.Threading.WindowsThreadPool.DispatchCallback(IntPtr, IntPtr, IntPtr) + 0x45
很明顯的看到這里是WindowsThreadPool(Windows線程池調(diào)用),而上面的則是PortableThreadPool(.NET運(yùn)行時(shí)自己實(shí)現(xiàn)的托管線程池)。這里有個(gè)疑問(wèn),為什么AOT可以看到Windows線程池,因?yàn)锳OT是本地預(yù)編譯機(jī)器碼,它不包含托管代碼,所以只能Windows自帶線程池調(diào)用。但是如果是托管代碼,不是AOT化,那么可以看到原汁原味的托管線程池調(diào)用。
通過(guò)issuse:dotnet/runtime#85373,Windows上運(yùn)行的.NET8應(yīng)用程序可以選擇任何一個(gè)線程池。
可以在 .csproj 中的
false
false表示不使用Windows線程池,True表示使用。其它的,也可以設(shè)置環(huán)境變量,來(lái)使用Windows線程池,設(shè)置0則不使用。
DOTNET_ThreadPool_UseWindowsThreadPool=1
目前來(lái)說(shuō),沒(méi)有確切的證據(jù)證明哪個(gè)線程池好用,或者效率更高。但是開(kāi)發(fā)者可以使用上面的選項(xiàng)來(lái)進(jìn)行自己的選擇,有一個(gè)測(cè)試就是在Windows線程池在比較大的機(jī)器上的IO擴(kuò)展性不太好。如果你的應(yīng)用程序已經(jīng)大量的使用了Windows線程池,那么可以通過(guò)以上設(shè)置為另一個(gè)線程池操作也是可以的。此外,線程池經(jīng)常被阻塞,Windows線程池對(duì)此有更多的處理,也能更有效的比托管線程處理的更好。如以下代碼:
// dotnet run -c Release -f net8.0 usingSystem.Diagnostics; varsw=Stopwatch.StartNew(); var barrier = new Barrier(Environment.ProcessorCount * 2 + 1); for (int i = 0; i < barrier.ParticipantCount; i++) { ThreadPool.QueueUserWorkItem(id => { Console.WriteLine($"{sw.Elapsed}: {id}"); barrier.SignalAndWait(); }, i); } barrier.SignalAndWait(); Console.WriteLine($"Done:{sw.Elapsed}");
以上創(chuàng)建了很多工作項(xiàng),所有的工作項(xiàng)都會(huì)被阻塞,直到所有工作項(xiàng)都被處理完畢。這里可以設(shè)置DOTNET_ThreadPool_UseWindowsThreadPool 為 1??聪聦?duì)比的結(jié)果,顯示W(wǎng)indows線程池處理的更好。
審核編輯:湯梓紅
-
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68520 -
指令集
+關(guān)注
關(guān)注
0文章
222瀏覽量
23378 -
線程
+關(guān)注
關(guān)注
0文章
504瀏覽量
19675
原文標(biāo)題:.NET8極致性能優(yōu)化-線程
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論