Redian新闻
>
.NET8极致性能优化-线程

.NET8极致性能优化-线程

公众号新闻

前言

首先来看下,为什么性能会一直持续性优化。.NET8引入的SSE-XMM(16字节)Register和AVX-YMM(32字节)Register是关键,传统的Register一般指令集层次能移动的最多只有8位,就算是最新的x64系统。但是SSE和AVX改变了这种局面,它们能一次性移动64位系统的一倍乃至四倍,这就是优化的关键。

之前的多篇文章展示了很多.NET8的性能优化,基本上都是核心级的CLR/JIT优化,包括了VMZeroingCHRLExceptionNon_GC ,BranchGCReflectionAOTEnumDateTime等等。但是漏掉了一个较为重要的东西:线程。本篇来看下.NET8里面的线程优化。

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.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.08.492 ns1.00
Increment.NET 8.01.453 ns0.17

同样的通过

dotnet/runtime#84566 和 dotnet/runtime#87148为.NET AOT做的一个优化,提升同样明显。

方法运行时平均值比率
IncrementNativeAOT 7.02.305 ns1.00
IncrementNativeAOT 8.01.325 ns0.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 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

我们运行下路径\bin\Release\net8.0\win-x64\publish里的exe文件,可以看到如下:

at System.Environment.get_StackTrace() + 0x21at ThreadPool_.Program.<>c.<Main>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 中的 <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线程池处理的更好。

END



“老实人”程序员遇渣女



这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Stability AI开源上新:3D生成引入视频扩散模型,质量一致性up,4090可玩解密得物Trace2.0:日PB级数据量下的计算与存储性能优化实战AI早知道|微博上线AI评论机器人“罗伯特”;Midjourney计划上线一致性角色生成功能英特尔 Arrow Lake-S 处理器规格泄露:最高 24 核 32 线程,原生支持雷电 4神舟战神 T8 16 英寸笔记本电脑配置上新:i9-14900HX + 140W RTX 4070,售 9999 元零拷贝,性能优化必争之地!1张图2分钟转3D!纹理质量、多视角一致性新SOTA|北大出品Spring Boot 3.2 正式发布,开箱即用的虚拟线程和 GraalVM,尝鲜一下!斯洛文尼亚布莱德湖(Lake Bled),宁静世界优化资源利用:Kubernetes 装箱的效益和挑战雜家皮草,專家奇點Javier Milei, John Keynes, Milton Friedman千元成本搞定专业大模型,系统优化+开源大模型是关键 | 潞晨卞正达@MEET2024神舟战神 T8 16 英寸游戏本配置上新:i7-13650HX + RTX4060 首发价格 6999 元并发王座易主?Java 21 虚拟线程强势崛起,Go & Kotlin还稳得住吗 | 盘点Winter Break Nearing, China Targets Illegal Student Competitions40个小技巧,帮你Java性能优化案例分析|线程池相关故障梳理&总结原来,这才是 JDK 推荐的线程关闭方式红色日记 主席逝世 9.9-15.NET8动态PGO简析SaaS 时代,如何确保 API 版本控制的一致性?官宣! OpenCSG 发布 StarNet Beta 版,打造中国版 Huggingface+,加快形成新质生产力Fish Shell 采用 Rust 重写会导致性能下降【提示】8批次雪地靴耐折性能、防寒性能等不合格,这份抽查报告请收好→长期手机辐射,或致性功能异常、影响发育与脑活动?施一公院士:电磁辐射对生物系统的影响,这次说清楚了!深入了解抖音的性能成本优化策略|QCon《星级男人通鉴》第46章 让现实教育你CPU程序性能优化我的财富捷径Redis7 单线程VS多线程这些年背过的面试题——多线程篇效果可能优于GLP-1减肥药?辉瑞合作伙伴开发减重新药,预计明年启动临床II期试验分布式一致性协议与算法:没你想的那么简单 | 极客时间【避坑】线程池没用好,直接出现了生产事故....Stability AI杀回来了:视频生成新Demo效果惊人,网友:一致性超群
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。