性能优化需先定位瓶颈再编码,用benchmarkdotnet测完整调用链并启用memorydiagnoser,优先减少分配:复用对象、预设容量、使用arraypool和值类型key,避免热路径用linq/foreach/反射。

性能优化不是“写得快就行”,而是先确认瓶颈在哪、再动代码。盲目改 foreach 为 for 或硬套 Span<t></t>,往往没效果还引入 bug。
用 BenchmarkDotNet 测热路径,别用手动 Stopwatch
真实性能差异藏在热路径里(比如高频循环、序列化、字典查找),而 Stopwatch 容易受 JIT 预热、GC 干扰、CPU 频率波动影响,测不准。
- 必须用
BenchmarkDotNet:它自动做预热、多轮采样、统计显著性,并隔离 GC 影响 - 别只测单个方法——要测完整调用链,例如
ParseJsonThenMapToDto(),而不是只测JsonSerializer.Deserialize() - 加
[MemoryDiagnoser]看分配量,Gen0/Gen1/Gen2 GC次数比耗时更重要 - 在 Release 模式 +
dotnet run -c Release下运行,禁用调试器附加
减少分配是 C# 性能优化的第一优先级
.NET 的 GC 压力主要来自短生命周期对象的高频分配。一次 new List<int>()</int>、一个隐式装箱、甚至 string.Substring() 都可能触发 Gen0 回收。
- 避免在循环内创建集合:
new Dictionary<string int>()</string>放循环外;已知容量就传入capacity参数 - 用
ArrayPool<t>.Shared.Rent(n)</t>替代new T[n],尤其在缓冲区场景(如网络包解析、日志格式化) - 值类型作字典 Key 时,别用
object——Dictionary<int string></int>比Dictionary<object string></object>快 10 倍以上,且零装箱 -
StringBuilder复用时调用.Clear(),而非反复new StringBuilder()
别在热路径上用 LINQ、foreach 和反射
这些语法糖在可读性和开发效率上很香,但编译后可能生成额外枚举器、闭包类或虚调用,对吞吐敏感场景就是瓶颈。
-
collection.Where(x => x > 5).First()→ 改成for循环 + 提前break,尤其当集合大且命中靠前时 -
foreach (var item in list)对List<t></t>和数组会隐式调用GetEnumerator(),产生堆分配;换成for (int i = 0; i 可消除 - 反射调用(如
propertyInfo.GetValue(obj))比直接访问慢 50–100 倍;高频场景用Expression.Compile()缓存委托,或 Source Generator 生成强类型访问器 -
string.Format、$"x={x}"在热路径中要警惕:它们内部分配StringBuilder;固定格式可用string.Create()+Span<char></char>零分配拼接
升级到 .NET 9/10,不开源代码也能提速
很多团队卡在 .NET 6/7,却花大量时间手动向量化或手写 unsafe 代码——其实新版本 JIT 已默认开启 Dynamic PGO,能根据运行时热点自动优化分支预测、内联深度和循环展开。
- .NET 9 的 JIT 对
for循环自动向量化支持更强,尤其配合Span<t></t>和Vector<t></t>时,无需改逻辑就能获得 SIMD 加速 - GC 默认启用 “Ephemeral Generation Tuning”,大幅降低 Gen0 回收频率;大对象堆(LOH)碎片也更少
- Native AOT 不只是“打包小”,它移除了 JIT 编译开销和部分运行时类型检查,启动快 3–5 倍,内存常驻下降 30%+,适合微服务冷启场景
- 别忽略
dotnet-trace collect --providers Microsoft-DotNETCore-EventPipe,它比 PerfView 更轻量,能快速抓到 CPU 热点和 GC 卡顿
最常被忽略的点:优化前不跑 profiler,优化后不压测真实负载。哪怕把 Dictionary.TryGetValue 改成结构体 key + 预分配容量,如果瓶颈其实在下游 HTTP 调用,那所有改动都白忙。











