.NET8性能优化之线程

前言
首先来看下,为什么性能会一直持续性优化。.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里面的线程优化。
threadstatic
.net在新的版本中,对线程,并发,并行,异步等方面做出了非常大的改进。比如threadpool完全重写,异步方法基础部分的完全重写,concurrentqueue队列的完全重写等等。.net8在这些的基础上,进行了更为深思熟虑的和更为有影响力的改进。比如threadstatic。
.net运行时里面运用本地数据和线程的关联,就是本地线程存储(tls)。在托管代码上实现这一点,最常用的方法就是用[threadstatic]属性注解一个静态字段(当然这里还有个用途更高级的threadlocal),这样就会导致.net运行时会把这个静态字段的存储复制到每个线程,而不是全局的进程上面。
例如以下threadstaitc属性注解的用法
private static int s_oneperprocess;[threadstatic]private static int t_oneperthread;  
在.net8之前访问被theadstatic标记的字段,需要一个jit的非内联辅助方法corinfo_help_getshared_nongcthreadstatic_base_noctor。它的原型实际上就是jit_getsharednongcthreadstaticbase。如下:
#include hcimpl2(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// dotnet run -c release -f net7.0 --filter * --runtimes nativeaot7.0 nativeaot8.0using benchmarkdotnet.attributes;using benchmarkdotnet.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 中添加 true。先运行下它,结果显示如下:
at system.environment.get_stacktrace()at threadpool_.program.c.b__0_0() in e:visual studio projecttest_threadpool_program.cs:line 7at 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() + 0x21at threadpool_.program.c.b__0_0() + 0x9at system.threading.executioncontext.runfromthreadpooldispatchloop(thread, executioncontext, contextcallback, object) + 0x3dat system.threading.tasks.task.executewiththreadlocal(task&, thread) + 0xccat system.threading.threadpoolworkqueue.dispatch() + 0x289at 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.0using system.diagnostics;var sw = stopwatch.startnew();var barrier = new barrier(environment.processorcount * 2 + 1);for (int i = 0; i { console.writeline(${sw.elapsed}: {id}); barrier.signalandwait(); }, i);}barrier.signalandwait();console.writeline($done: {sw.elapsed});  
以上创建了很多工作项,所有的工作项都会被阻塞,直到所有工作项都被处理完毕。这里可以设置dotnet_threadpool_usewindowsthreadpool 为 1。看下对比的结果,显示windows线程池处理的更好。


导热凝胶已应用于自动驾驶毫米波雷达散热
10kv变频器电路
realme Buds Air真无线耳机将于2020年1月7日正式发布
新型的送货机器人怎样才能工作好
Cuk 拓扑电源原理及工作过程解析
.NET8性能优化之线程
前瞻6G趋势 三大运营商扬帆创新征途
如何基于 ES6 的 JavaScript 进行 TensorFlow.js 的开发
日本研究人员开发出一个由LED芯片构成的可植入式装置,可有效地治疗肿瘤
加速传感器ADXL150特性及其精度影响因素
e络盟供应Molex SL™系列线对线模块化连接器系统
是德科技推出第1至第3层全方位以太网性能测试平台
浪漫经济正在成为拉动GDP的一股新生力量
基于空闲检测机制的电力集中抄表系统设计
智能油井在线监控解决方案,第一时间掌握所有动态
区块链正在缓慢的主导世界经济
智慧交通未来有哪一些机遇
英特尔推出基于FPGA的全新可编程加速卡
三星拟投59亿美元到LCD显示产品线
火酷震机械键盘拆解评测 逼格很高拆解方便