前言
首先來看下,為什么性能會一直持續(xù)性優(yōu)化。.NET8引入的SSE-XMM(16字節(jié))Register和AVX-YMM(32字節(jié))Register是關(guān)鍵,傳統(tǒng)的Register一般指令集層次能移動的最多只有8位,就算是最新的x64系統(tǒng)。但是SSE和AVX改變了這種局面,它們能一次性移動64位系統(tǒng)的一倍乃至四倍,這就是優(yōu)化的關(guān)鍵。
之前的多篇文章展示了很多.NET8的性能優(yōu)化,基本上都是核心級的CLR/JIT優(yōu)化,包括了VM,Zeroing,CHRL,Exception,Non_GC,Branch,GC,Reflection,AOT,Enum,DateTime等等。但是漏掉了一個較為重要的東西:線程。本篇來看下.NET8里面的線程優(yōu)化。
.NET在新的版本中,對線程,并發(fā),并行,異步等方面做出了非常大的改進(jìn)。比如ThreadPool完全重寫,異步方法基礎(chǔ)部分的完全重寫,ConcurrentQueue隊(duì)列的完全重寫等等。.NET8在這些的基礎(chǔ)上,進(jìn)行了更為深思熟慮的和更為有影響力的改進(jìn)。比如ThreadStatic。
.NET運(yùn)行時里面運(yùn)用本地?cái)?shù)據(jù)和線程的關(guān)聯(lián),就是本地線程存儲(TLS)。在托管代碼上實(shí)現(xiàn)這一點(diǎn),最常用的方法就是用[ThreadStatic]屬性注解一個靜態(tài)字段(當(dāng)然這里還有個用途更高級的ThreadLocal
例如以下ThreadStaitc屬性注解的用法
private static int s_onePerProcess; [ThreadStatic] private static int t_onePerThread;
在.NET8之前訪問被TheadStatic標(biāo)記的字段,需要一個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)檫@個方法本身是有優(yōu)化空間的,經(jīng)過dotnet/runtime#82973 and dotnet/runtime#85619它的函數(shù)本體被內(nèi)聯(lián)到了調(diào)用者當(dāng)中了。省略了函數(shù)調(diào)用以及跳轉(zhuǎn)的成本。通過一個基準(zhǔn)測試來看下這個效果。
// 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; }
測試結(jié)果如下,提升明顯:
方法 | 運(yùn)行時 | 平均值 | 比率 |
---|---|---|---|
Increment | .NET 7.0 | 8.492 ns | 1.00 |
Increment | .NET 8.0 | 1.453 ns | 0.17 |
同樣的通過
dotnet/runtime#84566 和 dotnet/runtime#87148為.NET AOT做的一個優(yōu)化,提升同樣明顯。
方法 | 運(yùn)行時 | 平均值 | 比率 |
---|---|---|---|
Increment | NativeAOT 7.0 | 2.305 ns | 1.00 |
Increment | NativeAOT 8.0 | 1.325 ns | 0.57 |
ThreadPool
TheadPool優(yōu)化在于線程池方面,之前老版本的.NET基本上都是通過封裝Windows線程池,然后通過托管代碼調(diào)用。但是在.NET6里面開始.NET運(yùn)行時實(shí)現(xiàn)了自己的托管線程池,也就是說新版的.NET包含了兩個線程池。分別為托管調(diào)用的windows線程池,以及托管代碼自己實(shí)現(xiàn)的托管線程池。現(xiàn)在,在.NET8里面可以自由切換這兩個線程池,你想使用哪個就用哪個,以提升程序的性能。
我們來看下,這個過程。首先新建一個.NET8.0控制臺應(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這個就是.NET6以來新增的托管線程池操控的代碼。我們下面再來看下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í)現(xiàn)的托管線程池)。這里有個疑問,為什么AOT可以看到Windows線程池,因?yàn)锳OT是本地預(yù)編譯機(jī)器碼,它不包含托管代碼,所以只能Windows自帶線程池調(diào)用。但是如果是托管代碼,不是AOT化,那么可以看到原汁原味的托管線程池調(diào)用。
通過issuse:dotnet/runtime#85373,Windows上運(yùn)行的.NET8應(yīng)用程序可以選擇任何一個線程池。
可以在 .csproj 中的
false
false表示不使用Windows線程池,True表示使用。其它的,也可以設(shè)置環(huán)境變量,來使用Windows線程池,設(shè)置0則不使用。
DOTNET_ThreadPool_UseWindowsThreadPool=1
目前來說,沒有確切的證據(jù)證明哪個線程池好用,或者效率更高。但是開發(fā)者可以使用上面的選項(xiàng)來進(jìn)行自己的選擇,有一個測試就是在Windows線程池在比較大的機(jī)器上的IO擴(kuò)展性不太好。如果你的應(yīng)用程序已經(jīng)大量的使用了Windows線程池,那么可以通過以上設(shè)置為另一個線程池操作也是可以的。此外,線程池經(jīng)常被阻塞,Windows線程池對此有更多的處理,也能更有效的比托管線程處理的更好。如以下代碼:
// 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)都會被阻塞,直到所有工作項(xiàng)都被處理完畢。這里可以設(shè)置DOTNET_ThreadPool_UseWindowsThreadPool 為 1。看下對比的結(jié)果,顯示W(wǎng)indows線程池處理的更好。
審核編輯:湯梓紅
-
代碼
+關(guān)注
關(guān)注
30文章
4753瀏覽量
68368 -
指令集
+關(guān)注
關(guān)注
0文章
222瀏覽量
23368 -
線程
+關(guān)注
關(guān)注
0文章
504瀏覽量
19653
原文標(biāo)題:.NET8極致性能優(yōu)化-線程
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論