字数 1128,阅读大约需 6 分钟
一次 .NET 性能优化之旅:将 GC 压力降低 99%
前言:问题的浮现
最近,我使用 ScottPlot 库开发一个频谱分析应用。应用的核心功能之一是实时显示频谱图,这可以看作是一个高频刷新热力图(Heatmap)。然而,在程序运行一段时间后,我注意到整体性能开始逐渐下降,界面也出现了卡顿。直觉告诉我,这背后一定隐藏着性能瓶颈。
分析:探寻性能瓶颈
面对性能问题,我首先打开了 Visual Studio 的诊断工具,重点关注计数器(Counters)的变化。
VS 诊断工具上图揭示了几个严重的问题:
- 1. GC 频繁:进程内存图表中,GC(垃圾回收)标记几乎连成一片,表明垃圾回收异常频繁。
- 2. GC 耗时过长:% Time in GC since last GC 的值非常高,说明 GC 占用了大量的 CPU 时间。
- 3. 高内存分配率:Allocation Rate 居高不下,意味着程序在以极高的速率分配内存。
显然,问题出在 GC 上。但究竟是哪部分代码导致了如此巨大的 GC 压力呢?
定位:追踪 GC 的“元凶”
为了找出问题的根源,我使用了 Visual Studio 的性能探查器(Performance Profiler),并选择了 .NET 对象分配跟踪(.NET Object Allocation Tracking)模式。
在程序运行一段时间后,我停止了分析,并查看了分配(Allocations)选项卡。结果令人震惊:System.Double 类型的分配次数和字节数都异常巨大。这正是导致 GC 频繁的“元凶”。
通过调用堆栈,我迅速定位到了问题代码:
调用堆栈- 函数名 分配 字节 模块名称
- + ScottPlot.NumericConversion.Clamp<T>(T, T, T) 3,592,245 86,213,880 scottplot
复制代码 所有的矛头都指向了 ScottPlot.NumericConversion.Clamp(T, T, T) 这个函数。
探究:泛型与装箱的“陷阱”
为了弄清真相,我翻阅了 ScottPlot 的源代码,并梳理了整个调用流程:
- 1. 在绘制热力图时,程序会调用 NumericConversion.Clamp 函数,将数据归一化到 0-1 的范围内。
- 2. 接着,程序会根据归一化后的值,从颜色映射表(ColorMap)中获取对应的颜色。
- public Color GetColor(double position)
- {
- position = NumericConversion.Clamp(position, 0, 1);
- int index = (int)((Colors.Length - 1) * position);
- return Colors[index];
- }
复制代码 问题就出在 NumericConversion.Clamp 函数的实现上:- public static T Clamp<T>(T input, T min, T max) where T : IComparable
- {
- if (input.CompareTo(min) < 0) return min;
- if (input.CompareTo(max) > 0) return max;
- return input;
- }
复制代码 这是一个泛型方法,并且 double 是值类型。当 double 作为参数传递给这个泛型方法时,会发生装箱(boxing),即 double 被转换为 IComparable 接口。在每秒数万次的调用下,这会导致频繁的堆分配,从而引发巨大的 GC 压力。
优化:小改动,大提升
找到了问题的根源,解决方案也就水到渠成了。我为 Clamp 函数添加了一个 double 类型的重载版本,从而避免了装箱操作:- public static double Clamp(double input, double min, double max)
- {
- if (input < min) return min;
- if (input > max) return max;
- return input;
- }
复制代码 测试:验证优化效果
为了验证优化效果,我使用 LinqPad 和 BenchmarkDotNet 进行了性能测试。- #load "BenchmarkDotNet"
- void Main()
- {
- RunBenchmark();
- }
- privatedoublevalue = 0.75;
- privatedouble min = 0.0;
- privatedouble max = 1.0;
- [Benchmark]
- public double Clamp_Double()
- => NumericConversion.Clamp(value, min, max);
- [Benchmark]
- public double Clamp_Generic()
- => NumericConversion.Clamp<double>(value, min, max);
- publicstaticclassNumericConversion
- {
- public static double Clamp(double value, double min, double max)
- => value < min ? min : (value > max ? max : value);
- public static T Clamp<T>(T input, T min, T max) where T : IComparable
- {
- if (input.CompareTo(min) < 0) return min;
- if (input.CompareTo(max) > 0) return max;
- return input;
- }
- }
复制代码 测试结果如下:
性能测试结果从上图可以看出,新添加的 Clamp_Double 方法在性能上远超泛型版本。
再次打开 Visual Studio 的诊断工具,GC 压力几乎消失了:
优化后诊断工具总结:性能优化的启示
通过对 GC 压力的分析和优化,我成功解决了程序中的性能瓶颈。这次优化的核心在于,通过为 NumericConversion.Clamp 函数添加 double 类型的重载,避免了高频调用下的装箱操作,从而显著提升了性能,并将 GC 压力降低了 99% 以上。
这次经历不仅提升了程序的运行效率,也为我未来的性能调优工作积累了宝贵的经验。
目前,我已经将针对 ScottPlot 源码的修改提交了 PR:https://github.com/ScottPlot/ScottPlot/pull/4985
欢迎关注我的公众号“nodered-co”,原创技术文章第一时间推送。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |