.NET8极致性能优化-线程

💡 原文中文,约5100字,阅读约需13分钟。
📝

内容提要

本文介绍了.NET8中的性能和线程优化,包括引入SSE-XMM和AVX-YMM寄存器以提升性能,使用ThreadStatic属性进行线程优化,以及.NET8中的线程池优化。

🔎

延伸解读

ThreadStatic 内联优化:从辅助方法到直接访问

在 .NET8 之前,访问 [ThreadStatic] 字段需要 JIT 调用非内联辅助方法,带来额外开销。.NET8 通过 dotnet/runtime#82973 和 #85619 将该辅助方法内联到调用者中,消除了函数调用和跳转成本。基准测试显示,Increment 操作在 .NET8 上耗时仅为 .NET7 的 17%,NativeAOT 下也有显著提升。这意味着频繁访问线程静态字段的代码将直接受益。

双线程池可选:托管与 Windows 线程池的取舍

.NET8 允许开发者通过 <UseWindowsThreadPool> 或环境变量 DOTNET_ThreadPool_UseWindowsThreadPool 在托管线程池(PortableThreadPool)和 Windows 线程池之间切换。文章指出,Windows 线程池在大规模 IO 扩展性上表现不佳,但对频繁阻塞的场景处理更有效。目前尚无确凿证据表明哪个线程池整体更优,开发者应根据应用特性进行选择。

AOT 编译下的线程池行为差异

文章通过堆栈对比揭示:非 AOT 的托管代码默认使用 PortableThreadPool,而 AOT 编译后的程序因不包含托管代码,只能调用 Windows 线程池。这解释了为何 AOT 场景下线程池行为不同。开发者若在 AOT 下需要特定线程池,需注意此限制,并通过设置显式指定。

❓

Q&A

为什么.NET 8的性能优化能持续进行?关键硬件支持是什么?

.NET 8引入SSE-XMM(16字节)和AVX-YMM(32字节)寄存器是关键。传统寄存器一次最多移动8位,而SSE和AVX能一次性移动64位系统的一倍乃至四倍,从而大幅提升性能。

ThreadStatic属性在.NET 8中有什么优化?

在.NET 8之前,访问ThreadStatic字段需要JIT的非内联辅助方法,有函数调用开销。.NET 8通过优化(dotnet/runtime#82973和#85619)将该方法内联到调用者中,减少了调用和跳转成本。基准测试显示,Increment方法在.NET 7上平均8.492 ns,在.NET 8上仅1.453 ns,提升明显。

.NET 8中如何选择使用托管线程池还是Windows线程池?

可以通过在.csproj的PropertyGroup中添加<UseWindowsThreadPool>false</UseWindowsThreadPool>来设置(false表示不使用Windows线程池,true表示使用)。也可以设置环境变量DOTNET_ThreadPool_UseWindowsThreadPool=1来使用Windows线程池,设置为0则不使用。

托管线程池和Windows线程池各有什么优缺点?

目前没有确切证据证明哪个线程池更好。但Windows线程池在大型机器上的IO扩展性表现不佳;而如果应用程序大量使用Windows线程池,可以切换为托管线程池。另外,线程池经常被阻塞时,Windows线程池有更多的处理机制,可能比托管线程池更有效。

为什么AOT编译后看到的线程池是Windows线程池?

因为AOT是本地预编译机器码,不包含托管代码,所以只能调用Windows自带线程池。而托管代码(非AOT)则可以看到.NET运行时自己实现的托管线程池(PortableThreadPool)。

在.NET 8中,ThreadStatic字段的访问是如何被优化的?

通过将原本需要JIT非内联辅助方法(CORINFO_HELP_GETSHARED_NONGCTHREADSTATIC_BASE_NOCTOR)的调用内联到调用者中,消除了函数调用和跳转的开销。这得益于dotnet/runtime#82973和#85619的改进。

🏷️

标签

➡️

继续阅读