90%性能问题无需改逻辑,用dotnet-trace定位瓶颈(如gen2 gc频次、热点方法)、list预设容量、arraypool复用数组、stringbuilder替代字符串拼接、泛型集合避免装箱、span零分配切片,并以benchmarkdotnet验证效果。

别一上来就改代码。90%的性能问题根本不用动逻辑,靠定位+针对性调整就能解决。
用 dotnet-trace 找出真瓶颈
很多人直接看 CPU 占用率,但 GC 停顿、同步阻塞、锁争用这些根本不会体现在 CPU 图上。真正该抓的是 runtime 事件流。
-
dotnet-trace collect --providers Microsoft-DotNETCore-SampleProfiler能捕获调用栈采样,适合快速发现热点方法 - 加
--providers Microsoft-DotNETCore-EventPipe::0x1111111111111111:4可同时抓 GC、ThreadPool、JIT 编译事件 - 生成的
trace.nettrace用PerfView或 VS 的「诊断工具」打开,重点看「Hot Path」和「GC Heap View」 - 特别注意
Gen2 GC频次——如果每秒超过 1 次,说明有大对象或长期存活的小对象在拖慢回收
List 初始化容量不设,等于默认开销翻倍
没指定 capacity 的 List<t></t> 初始长度是 0,第一次 Add 就分配 4 个元素;第 5 次再翻倍到 8;依此类推。数据量大时,光数组复制就吃掉大量时间。
- 知道大致数量?直接
new List<int>(estimatedCount)</int> - 不确定但有下限?比如日志条目通常 1–100 条,
new List<logentry>(32)</logentry>比默认更稳 - 用
ArrayPool<t>.Shared.Rent(n)</t>替代临时数组分配,尤其在循环内反复创建小数组时(如解析帧、分块读取) - 别迷信「预估不准就宁可不设」——即使估高 2 倍,也比反复扩容拷贝强
字符串拼接不换 StringBuilder,就是在喂 GC
string 不可变,+= 每次都 new 一个新对象。1000 次拼接 ≈ 1000 次堆分配 + 后续 GC 压力。
- 固定格式输出(如日志、SQL 拼装)优先用
$"..."插值——编译器会自动选string.Create或StringBuilder,比手写更优 - 循环内拼接?必须用
StringBuilder,且初始化时带容量:new StringBuilder(1024) - 纯切片/子串?用
ReadOnlySpan<char></char>,比如input.AsSpan().Slice(start, length),零分配 - 避免
string.Concat或string.Join处理超长集合——它们内部仍会 new 数组,大数据量时不如先StringBuilder累加再.ToString()
泛型集合不替代非泛型,就是在主动装箱
用 ArrayList 存 int,每次 Add 都触发一次装箱;取出来再拆箱。这不是慢一点,是「为每项多分配一次内存 + 多一次类型检查」。
- 所有值类型容器,无条件用
List<int></int>、Dictionary<long bool></long>这类泛型,不是「推荐」,是底线 - 自定义结构体(如
Point、Range)也必须泛型化,否则List<object></object>一样装箱 - 接口约束场景?用
IList<t></t>而非IList,避免运行时类型擦除带来的间接调用开销 - Unity 项目要额外注意:所有返回数组的 API(如
GetComponents<t>()</t>)都会触发 GC Alloc,能缓存就缓存,不能缓存就用Span<t></t>接收
最常被忽略的点:优化前不测,优化后不验。同一段代码在 Debug 和 Release 下行为可能完全不同,JIT 甚至会把空循环整个删掉。真要调性能,dotnet-trace + BenchmarkDotNet 组合才是闭环。其他都是猜。











