惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
Engineering at Meta
Engineering at Meta
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
I
InfoQ
S
SegmentFault 最新的问题
博客园 - 叶小钗
N
Netflix TechBlog - Medium
Y
Y Combinator Blog
IT之家
IT之家
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
月光博客
月光博客
The Cloudflare Blog
U
Unit 42
GbyAI
GbyAI
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog

博客园 - 张善友

.NET 11 性能全解读:从 JIT 到基础库,这一版到底快了多少 把 LLM 密钥从环境变量里解放出来:OpenClaw.NET 迎来 Vault/OpenBao 密钥后端 RedNb.Nacos 2.0.0 正式发布:.NET 接入 Nacos 3.2.4,AI Registry 全能力落地 Rust 成为微软一线语言之后:谈谈 C# 与 Rust 的互补性 .NET 异常处理的"暗门":代码里写满 catch,你依然能抓住它——从一个 AI Agent 运行时的源码说起 写给 C++ 工程师的 OpenClaw.NET 上手指南:用你熟悉的 C++ 思维,跑起一个生产级 AI Agent .NET 11 RC1 发布:拿到"准生证",生产环境可以上了! 写给 PHP 工程师的 OpenClaw.NET 上手指南:用你熟悉的 PHP 思维,跑起一个生产级 AI Agent 写给 Rust 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Rust 思维,跑起一个生产级 AI Agent 从对标 Java 到对标 Go:Native AOT 的"无痛化"之路,走到哪一站了? NuGet 半年度总结:周下载量从 54 亿到 67 亿,.NET 生态的"新一轮增长期"实锤了 写给 Java 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Spring 思维,跑起一个生产级 AI Agent 写给 TypeScript 工程师的 OpenClaw.NET 上手指南:用你熟悉的 TS 思维,跑起一个生产级 AI Agent 写给 Python 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Python 思维,跑起一个生产级 AI Agent 写给 Golang 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Go 思维,跑起一个生产级 AI Agent MetaSkill 落地 .NET:当 Agent 从「调用工具」进化到「组织工具」 都是 AI 写代码,为什么 C# 比 Java 快半拍 MHS 三部曲(下):谁允许 AI 行动?——权力、合规与中国厂商的答卷 弱模型不能裸奔:Agent Harness 凭什么真实有效 MHS 三部曲(中):8 小时集成、六种被拦截的故障,和一次教科书级的翻车 TensorSharp 3.3.0.0 发布,视频生成、DFlash2 投机解码、安全加固一起来了 MHS 三部曲(上):别急着叫它「物理 MCP」——Anthropic 到底发布了什么 编程语言的「第三条道路」上,走得最远的其实是 C# 纯 .NET 手写 CUDA kernel,GLM-5.3-Flash decode 跑出 llama.cpp 的 2 倍 3 张卡到底能不能跑大模型推理?从 vLLM、llama.cpp 到 TensorSharp 的多卡真相 AI 编程时代,.NET 的机会在哪里? Vibe Coding 月提交量 29 亿次之后:GitHub 的危机、Azure 迁移,以及 .NET 的机会 从 PostgreSQL 到 Kubernetes:开源的护城河,从来不写在代码里 人工智能最先替代的,是人工智能学院自己 企业架构的六种场景:从"四大流派"到数字原生与 AI 原生
.NET 11 性能深度解读:这一次,真的「到 11」了
张善友 · 2026-09-17 · via 博客园 - 张善友

NET 11 性能优化全景架构

原文:Performance Improvements in .NET 11,Stephen Toub,Microsoft DevBlogs(2026-09-15)
译文/解读:本文基于原文精选编译,略有删减

写在前面

每年 .NET 发布前夕,Stephen Toub 都会在 DevBlogs 上发表一篇「Performance Improvements in .NET X」长文。这已经成了 .NET 社区的年度惯例,从 .NET Core 2.0 一路写到了今天的 .NET 11。

今年的开篇是一个《摇滚万万岁》(This Is Spinal Tap)的梗:吉他放大器的音量旋钮最高是 10,Nigel 的定制款「可以到 11」。.NET 11 的寓意正在于此:不是某个革命性的突破,而是成百上千个小改进的复利积累。一个被消除的边界检查、一个不再发生的堆分配、一把没被拿起的锁、一个用 SIMD 重写的数组拷贝,每一项单独看都不起眼,合起来就是实打实、可测量的「响了一度」。

一、Runtime Async:异步编程的底层重构

如果说 .NET 11 只能挑一个亮点,那非 Runtime Async 莫属。这是近十年来 CLR 异步底层最大的一次重构。

传统 async/await 里,每个异步方法编译后都会生成一个状态机,方法之间每传递一次结果都要物化一个 Task 对象。A 调 B、B 调 C,C 同步完成时,结果本可以直接作为普通返回值层层传回,却要被迫包装成 Task<int> 再解包。

.NET 11 中,JIT 能识别「调用一个返回 Task 的方法并立刻 await 它」这个惯用模式:同步完成时,int 直接穿透融合后的调用链,只有最外层边界才把结果包装成 Task;真正发生挂起时,运行时用续体(continuation)记录串起整条链,中间的每一层都不再需要那个 Task<int>

效果立竿见影(仅两层调用的基准测试):

  • 同步完成的路径:快 3 倍以上,并且消除了全部中间 Task 分配;
  • 真正发生挂起的路径:耗时不到原来的一半,每次调用少分配 80 字节。

异常处理的差距更大。过去一个未处理异常穿过 10 层 async 方法,会被「抛出、捕获、存入 Task」重复 10 次,即使所有方法都没有显式的 catch。Runtime Async 下,同步路径上异常正常展开;挂起后也由一次分发循环直接把异常写入可观察的根 Task。

.NET 11 还配套新增了轻量级 async 探查事件流(dotnet/runtime#127238):不为每次小转换发送完整事件,改为写入每线程缓冲、增量编码、批量刷新,并在线程栈上放置可识别的包装帧作为探查器的锚点。某些测量中开销低于 1%,追踪数据量减少了一个数量级,而且对传统编译器状态机同样生效。

注意:Runtime Async 并不会让所有异步操作零分配。如果你把 Task 存进集合、手动挂接续体、或者以对象形式观察它,那个对象依然必要。它的目标是:不为「观察不到的边界」付费。

二、JIT 去抽象化:更聪明的逃逸分析

「逃逸分析」是编译器回答「这个对象会不会逃出当前方法」的技术。如果一个新分配的对象引用可以证明不会逃逸,JIT 就可以把它分配到栈上,分配和回收几乎免费,且对 GC 零压力。

.NET 11 在这一领域收获了一批改进:

可空装箱的内部化(dotnet/runtime#122167):JIT 把 Nullable 的装箱内联展开,临时的 box 因此暴露给逃逸分析。int? 格式化这类场景里,那个 24 字节的临时堆分配被彻底消除:FormatNullableInt 从 9.58 ns 降到 1.99 ns,提速近 5 倍,分配从 24B 降为 0。

接口泛型虚调用也能去虚拟化:把刚 new 出来的 Processor 强转成 IProcessor 再调用泛型方法,IL 层面是接口泛型虚调用;.NET 11 的 JIT 能看到接收者的精确类型(即使是共享代码路径如 string),完成去虚拟化 + 内联,进而把 Unsafe.SizeOf<T>() 折叠成常量,并证明这个短命的 Processor 根本不需要被分配。

constrained. 前缀的地址暴露问题(dotnet/runtime#121918):EqualityComparer<T>.Default 内部的 ObjectEqualityComparer<T>.Equals 里,接收者曾被表示为对局部变量地址的间接读取。仅仅是「取了地址」这个动作,就把局部标记为已暴露,阻止了栈分配。现在改为直接值加载,24 字节堆分配消失。

Tier 0 也优化装箱(dotnet/runtime#129392):ArgumentNullException.ThrowIfNull(value)T 为非空值类型时本来要装箱,过去只在优化后的 Tier 1 消除,现在 Tier 0 就能消除,对启动阶段和分析器里的分配噪声都是好事。

三、边界检查消除:每一代都在啃的硬骨头

for (int i = 0; i < array.Length; i++) 这种循环里的边界检查,JIT 早就免了。但真实代码里有太多变体,.NET 11 又拿下了一批:

!= 循环也终于支持循环克隆(dotnet/runtime#129268):有些开发者觉得 i != length 更高效,结果反而阻止了优化。现在步长为 1 或 -1 时(i++ 可以,i += 2 不行),!= 循环同样能克隆出无检查的快速路径。

return 语句纳入范围检查克隆(dotnet/runtime#124705):abcd[0] + abcd[1] + abcd[2] + abcd[3] 这样的返回语句过去因为位于块末尾而错过克隆,.NET 11 里一次快速路径守卫就能覆盖全部 4 次访问。

基本块内的检查合并(dotnet/runtime#127439):即使没有克隆,JIT 也会把同一个基本块内的多个边界检查合并,把第一个检查强化到最大常量下标,删掉其余。BinaryPrimitives.ReadInt32BigEndian 的辅助函数从 73 字节瘦到 28 字节,只剩原始的长度守卫。

Span 切片跟踪(dotnet/runtime#122040#127117):向量化循环常对 span 一段段 Slice,JIT 过去跟不上「越来越小的切片与原 span 的关系」,导致每轮重复检查。现在跟踪更聪明,去掉了重复检查。

解析器式的前瞻检查(dotnet/runtime#124242#125235):识别 (uint)(i + 2) < (uint)span.Length 这类条件,并据此消除对 span[i + 1]span[i + 2] 的后续检查。

LeadingZeroCount 返回值范围:JIT 现在知道 LeadingZeroCount(uint) 的返回值在 0~32 之间,查找表的下标检查直接消失(516 ns → 438 ns)。

此外还有一长串小改进:同一索引的重复访问不必再查(#121640)、长度可以贯穿整个方法追踪(#121683)、arr[^4] 成立则 arr[^3] 无需再查(#124571)、位运算和无符号除法的范围更精确(#129101),等等。

四、断言传播:让 JIT 学会「从成功中学习」

范围不只来自显式比较,还来自操作本身的成功执行。dotnet/runtime#124711 教会 JIT 从「已经成功完成的操作」中提取隐含事实:

  • 创建数组成功 ⇒ 请求的长度不是负数;
  • 引用数组的协变检查辅助函数成功返回 ⇒ 下标在范围内;
  • 整除 / 取模执行成功 ⇒ 除数不为零。

CovariantArrayStore 基准因此从 3.57 ns 降到 2.99 ns。配套的改进还包括:byte 转换天然限定 0~255 作为范围分析起点(#119474)、??= 懒初始化后两条路径合流时值必然非空(#127810)、静态字符串已知长度传播后可把通用字符串比较变成定长向量化比较(#128522)、结构体拷贝辅助函数里重复的源/目标空检查探测被移除(#128701)。

五、代码简化与无分支化

  • if-conversion 更聪明:bool x = false; if (cond) x = true; 现在也能识别隐式 else,折叠成条件移动指令(#124738);位测试的幂等归一化((A & bit) == bit(A & bit) != 0)让布尔优化能「在半路相遇」(#128533)。
  • 有符号字节比较不再做符号扩展:cmp cl, 0C0 替代 movsx + cmp eax(14 字节 → 10 字节)。
  • AVX-512 / AVX10.2 机器上的 float/doublelong/ulong 转换不再调用辅助函数,保持内联和寄存器内完成(#125180)。

六、向量化与硬件指令

AVX-512 嵌入式广播与掩码:嵌入式广播让指令直接从内存加载单个标量并复制到所有通道,只读数据段从存 64 字节变成存 4 字节;嵌入式掩码把「计算全部通道再混合」折叠进单条指令,省掉单独的 blend 和零向量初始化。VNNI 的 vpdpbusd 现在用 dword {1to4} 广播,常量池占用从 16 字节降到 4 字节。

更宽的向量归约:Vector256.Sum / Vector512.Sum 改为主要在全宽度下归约、最后合并各通道结果,避免为拆成 128 位片段而做的大量 extract/shuffle(#127329)。Vector256<byte> 乘法基准:3.75 ns → 2.17 ns,代码从 114 字节瘦到 73 字节。

stackalloc 清零提速近一倍:Stackalloc16384 从 313 ns 降到 163 ns(0.52)。

Arm64 指令选择大丰收(多位 ARM 工程师贡献):

惯用法 .NET 10 .NET 11
与零比较 and + cmp ands(复用标志位)
(v >> 6) & 0x3F asr + and 专用 ubfx 指令
位计数 通用序列 cnt / ctz(FEAT_CSSC)
相邻 64 位清零 4 条 str 2 条 stp

SVE(可伸缩向量)继续演进:更多循环模式获得硬件生成的谓词掩码、掩码操作减少设置搬运、可伸缩向量常量与向量存储初始化替代标量循环。

七、写屏障与 GC:让每一次引用写入更便宜

.NET 的分代 GC 要求「老对象写入指向新对象的引用」时必须更新 GC 记账(写屏障)。引用写入极其频繁,所以屏障越便宜越好,能证明不需要就越过。

协变数组存储(#126547):JIT 把数组存储辅助函数展开成独立操作,知道数组精确类型时消除协变检查并优化屏障。循环往 object[] 写 4096 个元素:10.85 μs → 6.04 μs(0.56)。

结构体整体拷贝(#128238 + #128542):堆目标分析从单个 store 扩展到整个结构体拷贝,引用字段用引用 store、非引用数据用 SIMD 向量 store。HeapStructCopy:4.13 ns → 3.07 ns(0.74)。

相邻字段合并写(#130535#126562#130107):Int128 赋值从两次 64 位 store 变成一条 16 字节向量写;两个相邻 int 字段甚至能合并成一条 64 位 store(mov rax, 200000001)。

GC 本体也有两处值得一说:

  • vxsort 向量化排序扩展到 Arm64(#110692):压缩式回收需要排序存活对象地址,大列表上排序是回收的重要开销,现在 Arm64 也能吃向量化红利。
  • 依赖句柄「随龄增长」(#78746):ConditionalWeakTable 底层的依赖句柄过去不随引用对象老化,导致每次 gen0/gen1 回收都要重新扫描。现在句柄会随 referent 晋升,年老的句柄可以被年轻的回收直接跳过。

八、启动与部署:MAUI 全面投奔 CoreCLR

.NET 11 起,MAUI 在 Android、iOS 和 Mac Catalyst 上正式切换到 CoreCLR,最后一批还在用 Mono 的 MAUI 平台完成迁移。切换后,移动应用和 ASP.NET Core、云服务、桌面 .NET 共享同一个运行时、同一个 JIT、同一个 GC、同一套诊断设施和性能改进,并引入 CoreCLR 的分层编译、ReadyToRun 和 PGO,也为 NativeAOT 打下共同基础。

NativeAOT 的 AllocHeap 在 Windows 上过去为每个块保留 64 KB 虚拟内存(即使只用 4 KB),现在小块改用普通 new/delete#122822),内存足迹更合理。

九、线程与线程池

  • 运行时内部 HashMap 的全屏障替换为更窄的 acquire/release(#125259);
  • VM 内多张哈希表(如 EEHashTable)引入基于纪元的回收(#124822),读多写少场景下读者不再需要进入协作式 GC 模式;
  • 哈希函数从逐字节哈希换成每次消费 4 字节的 xxHash(#129640);
  • 线程池小任务的调度开销降低(#122726):去掉不必要的内存栅栏和共享状态更新、每批而非每个任务与控制器交互、减少信号量自旋、只在队列确实需要时才请求新 worker,协调开销更少,也少了「队列刚空就被唤醒」的冤种 worker。

十、数值计算:BigInteger 换「大长腿」

BigInteger 的存储单元(limb)从 uint 换成 nuint#125799):64 位机器上每个 limb 从 32 位翻倍到 64 位,而同代价的 64 位运算能一次处理两倍比特。配合 Montgomery 乘法、ModPow 滑动窗口、融合位运算、循环展开与缓存等算法改进,再叠加此前预览版里的大数十进制转换提速(#112178)、Toom-Cook 大数乘法(#112876)、移位与旋转优化(#113005)。

三角函数也大幅提速:Asinfloat 版本 33.7 μs → 8.2 μs(0.24),double 版本 35.8 μs → 11.0 μs(0.31)。TensorPrimitivesBitIncrement/BitDecrement 也走向 SIMD,Half 路径直接操作原始 ushort 位模式,避免与 float 来回转换。

十一、字符串与 Span

  • UTF-8 验证提速 3 倍以上:ValidWithSurrogatePairs 从 2.99 μs 降到 857 ns(0.29);
  • Arm 上 UTF-8 → UTF-16 解码先做廉价的整向量 ASCII 测试,命中慢路径才计算具体 lane 位置(#121382);
  • string.Split 在 x86/x64 上对 ASCII 分隔符把两个 UTF-16 向量打包成一个字节向量一次比较,无匹配路径一次跳过两倍输入(#125379);
  • SequenceEqual<Guid> / SequenceEqual<Int128> 不再逐元素比较,而是按原始字节走优化过的内存比较路径,享受 JIT 展开与向量化(#130644);
  • 字符串相等比较:长度 15 时 6.18 ns → 2.40 ns(0.39)。

十二、全球化:时区转换快了一倍多

方法 .NET 10 .NET 11 比例
ConvertTimeFromUtc 45.13 ns 19.44 ns 0.43
ConvertTimeToUtc 51.97 ns 20.23 ns 0.39
GetLocalNow 76.41 ns 34.39 ns 0.45

ToUpperInvariant/ToLowerInvariant 现在优先走托管 ASCII 路径,能推迟甚至避免 ICU 的初始化(#120685);invariant 模式下不加载 ICU,ASCII 大小写吞吐同样受益。

如何亲手验证

官方文章的基准都基于 BenchmarkDotNet 双目标编译。把 csprojTargetFrameworks 设为 net11.0;net10.0,安装 BenchmarkDotNet,用 --runtimes net10.0 net11.0 跑同一组基准,你就能得到自己机器上的 .NET 11 性能收益报告。升级运行时零代码改动,收益自动到账,这就是 .NET 每年一次「免费午餐」的传统艺能。

结语

.NET 11 的性能故事没有单一主角:Runtime Async 重构异步底座,JIT 在边界检查、断言传播、逃逸分析上继续精进,向量化和 Arm64/SVE 指令选择全面开花,GC 与线程池默默降本,MAUI 全面切换到 CoreCLR。「这些,都可以到 11。」

对于绝大多数应用来说,升级到 .NET 11 不需要改一行代码,就能拿到上述全部收益。建议尽早把基准跑起来,量化属于你工作负载的那一度音量。


原文链接:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/
文中所有数据均来自原文的 BenchmarkDotNet 基准测试,具体结果因机器与工作负载而异。