.NET8极致性能优化-线程
前言
原文地址:.NET8极致性能优化-线程
首先来看下,为什么性能会一直持续性优化。.NET8引入的SSE-XMM(16字节)Register和AVX-YMM(32字节)Register是关键,传统的Register一般指令集层次能移动的最多只有8位,就算是最新的x64系统。但是SSE和AVX改变了这种局面,它们能一次性移动64位系统的一倍乃至四倍,这就是优化的关键。
前面本公众号(jianghupt)多篇文章,展示了很多.NET8的性能优化,基本上都是核心级的CLR/JIT优化,包括了VM,Zeroing,CHRL,Exception,Non_GC ,Branch,GC,Reflection,AOT,Enum,DateTime等等。但是漏掉了一个较为重要的东西:线程。本篇来看下.NET8里面的线程优化。
公众号:江湖评谈(jianghupt),扫一扫关注。
ThreadStatic
.NET在新的版本中,对线程,并发,并行,异步等方面做出了非常大的改进。比如ThreadPool完全重写,异步方法基础部分的完全重写,ConcurrentQueue队列的完全重写等等。.NET8在这些的基础上,进行了更为深思熟虑的和更为有影响力的改进。比如ThreadStatic。
.NET运行时里面运用本地数据和线程的关联,就是本地线程存储(TLS)。在托管代码上实现这一点,最常用的方法就是用[ThreadStatic]属性注解一个静态字段(当然这里还有个用途更高级的ThreadLocal<T>),这样就会导致.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 <optsmallperfcritical.h>
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.0
using 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 中添加 <PublishAot>true</PublishAot>。先运行下它,结果显示如下:
at System.Environment.get_StackTrace()
at ThreadPool_.Program.<>c.<Main>b__0_0() in E:\Visual Studio Project\Test_\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
我们运行下路径\bin\Release\net8.0\win-x64\publish里的exe文件,可以看到如下:
at System.Environment.get_StackTrace() + 0x21
at ThreadPool_.Program.<>c.<Main>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 中的 <PropertyGroup/> 中,添加 :
<UseWindowsThreadPool>false</UseWindowsThreadPool>
false表示不使用Windows线程池,True表示使用。其它的,也可以设置环境变量,来使用Windows线程池,设置0则不使用。
DOTNET_ThreadPool_UseWindowsThreadPool=1
目前来说,没有确切的证据证明哪个线程池好用,或者效率更高。但是开发者可以使用上面的选项来进行自己的选择,有一个测试就是在Windows线程池在比较大的机器上的IO扩展性不太好。如果你的应用程序已经大量的使用了Windows线程池,那么可以通过以上设置为另一个线程池操作也是可以的。此外,线程池经常被阻塞,Windows线程池对此有更多的处理,也能更有效的比托管线程处理的更好。如以下代码:
// dotnet run -c Release -f net8.0
using System.Diagnostics;
var sw = 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线程池处理的更好。
感兴趣的话,也可以来知识星球看看。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MySQL 全文索引触发 OOM 一例
业务监控告警内存不足,笔者进行了全面系统的故障分析并给出解决方案。 作者:付祥,现居珠海,主要负责 Oracle、MySQL、mongoDB 和 Redis 维护工作。 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 本文约 1700 字,预计阅读需要 6 分钟。 MySQL 版本 5.7.34 故障现象 某业务监控报警内存不足,发现 mysqld 进程由于内存不足被 kill 自动重启了。 [root@xxxxxx ~]# ps -ef|grep mysqld root 17117 62542 0 20:26 pts/1 00:00:00 grep --color=auto mysqld mysql 27799 1 7 09:54 ? 00:48:32 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid [root@xxxxxx ~]# # 操作系统日志记录 MySQL 被 OOM Dec 8 09:54:42 xxxxxx kernel: Out of memory: K...
- 下一篇
为什么需要在 OpenShift 上部署企业级 Ingress Controller
原文作者:Max Mortillaro of GigaOm 原文链接:为什么需要在 OpenShift 上部署企业级 Ingress Controller 转载来源:NGINX 中文官网 NGINX 唯一中文官方社区 ,尽在nginx.org.cn Red Hat OpenShift作为业界备受推崇的 Kubernetes 平台解决方案,凭借其全面的功能集、稳健的架构和企业级支持获得了诸多企业的青睐。毫无疑问,这些企业也在寻求企业级流量控制功能及自动化,以增强其 Kubernetes 平台并加快应用开发和部署速度。 Kubernetes 要求使用 Ingress 接口来处理进入集群的外部流量。在实践中,访问 Kubernetes 应用的外部客户端通过网关进行通信,该网关将四层至七层的流量暴露给集群内的 Kubernetes 服务。 要实现这一点,Ingress 资源中的流量路由规则需要由Ingress controller来实施。没有 Ingress controller,Ingress 将一无用处。在下图中,Ingress controller 将所有外部流量发送到单个 Kubern...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2整合Redis,开启缓存,提高访问速度