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