c#

位置:IT落伍者 >> c# >> 浏览文章

减少.NET应用程序内存占用的一则实践


发布日期:2023年05月11日
 
减少.NET应用程序内存占用的一则实践

最近一周比较忙主要的工作内容是在做一个叫“键盘精灵”的东西简单来讲就是将很多数据放到内存中对这些数据进行快速检索然后找出根据输入条件最匹配的条记录并予以展示具体和下面两款炒股软件的相关功能类似

数据以文本形式存在文件中且数据量较大有近万条每一条记录有几个字段以分隔符分割当时使用的是万条记录的测试数据文本文件将近  M这个模块加载到内存并建立缓存之后大概会占用将近M的内存自我接手以后主要的任务就是降低内存消耗和提高匹配效率

避免创建不必要的对象

拿到代码后第一步就是看设计文档然后断点一步一步的看代码大概明白了逻辑之后发现思路有一些问题之前的代码处理流程思路大概是下面这样的

将文件读取到内存实例化

根据条件对文件进行检索并存储到结果集

对结果集中的结果进行匹配度计算并存储到结果集中

按对结果集进行匹配度排序取最匹配的条记录然后返回

这个过程中规中矩但是其中有很多问题最大的问题是临时变量存储了太多的中间处理结果而这些对象在一次查询完成后又马上丢弃大量的临时对象带来了很大的GC压力举例来说当用户在输入框中输入的时候假设使用Contains来匹配那么从万条记录中找出包含的记录可能有万多条然后需要把这万多条记录存储在临时变量中进行处理进一步计算这万条记录的匹配度然后存储到一个类似KeyValuePair的集合中key为匹配度然后对这个集合按Key进行排序然后取前条最优记录可以看到中间创建了大量的临时变量使得内存剧增大量临时对象创建之后马上会被回收GC压力山大

而在设计文档中只要求返回最最匹配的条记录之前的解决方案中似乎并没有注意到这一点所以接手后第一步就是对上面的处理过程进行精简精简后如下

将文件读取到内存实例化

根据条件对文件进行检索如果存在

计算匹配度

以匹配度为Key存储到只有个容量的SortList中

如果SortList集合添加记录后大于则移除最后面一个元素始终保持着前个最小(匹配度最优)的记录

遍历完成之后返回这个集合对象

经过这一修改减少了大量临时数据对内存的占用整个过程中我只是使用一个容量为的SortList结构存储中间的过程每一次插入一个元素SortList帮我们排好序然后移除最不匹配的那一个也就是最后一个元素(从小到大排序越匹配值越小)这里面的消耗主要是  SortList的插入内部排序和移除记录  说到这里在选择SortList还是SortDictionary的问题上纠结了一下于是又找了些资料SortDictionary在内部使用红黑树实现SortList采用有序数组实现  在内部排序都为O(logn)的前提下SortDictionary的O(logn)插入及删除元素的时间复杂度优于SortList但是  SortDictionary会比SortList占用更多内存基本来说这是一个查询速度和内存分配之间的平衡由于这里只需要存储个对象所以两者相差不大其实即使没有这种结构自己也可以实现的无非就是一个集合每次添加一个排好序然后将最大的那个移除NET使用起来方便是因为有很多这些强大的内置数据结构

经过上面这个小小的修改内存占用一下子降低了从原来的M降低到了M其实这就是降低内存开销的一个最基本的原则那就是避免创建不必要的对象

优化数据类型及算法

越到后面内存的降低越来越困难仔细看了代码之后除了上面之外代码中也有一些其他问题比如一开始就将大量的对象实例化到内存中然后一直保存每一条记录中的信息比较多但真正有用的用于搜索匹配的只有下面四个字段但是整体的实例化会将其他没有用的字段也一并序列化进去了导致很多内存被无用的字段占用

“股票代码 股票中文名 中文拼音 市场类型 ……

浦发银行 PFYH 上证A股 ……”

所以第一步就是在内存中只存放需要检索的上面四个关键字段每一条记录刚开始是使用string[]数据而不是使用类或者其它结构来保存也尝试使用结构提来保存但是由于四个字段数据量大中间还要作为参数传递所以比使用类还大这里只是简单的使用了数组

除了上面这些之外为了提高搜索效率对数据按照az开头对数据做了切分分块缓存这样当用户输入直接从以为key的块中读取数据这样速度是加快了但是大量的缓存也增加了对内存的消耗缓存的数据基本上和加载到内存中原始的数据一样大了并且在搜索的过程中也是采用的完全搜索对于万条数据的四个字段每一次查询要进行*次遍历比较才能找出最匹配的条数据来

为此引入了不完全搜索就是事先对各类型证券如  股票基金债券分类对每一类按证券代码进行排序当用户设置了搜索的优先级时依次在每一类中查找如果找到满足条件的条记录则立即返回因为数据已经事先按照证券类型和代码排好序了所以后面找到的肯定没有之前找到的匹配度高这一改进直接提高了搜索查询的效率对有序的数据进行查找效率一般会比无序的数据查找效率高我们常见的一些查找算法比如说二分查找法前提也是待查找的集合有序排列

采用非托管代码或者模块编写数据处理逻辑

上面的两部操作虽然减少了将近%的内存占用但是仍然达不到领导的要求于是又尝试并比较了各种  使用不同的数据结构将数据载入到内存中的内存占用大小包括直接将文件按类型读成字符串数组结构及类内存占用最小的直接将文件读成字符串M的数据文件读进内存也会占用M的空间还不谈对其进行处理过程中产生的一些临时变量对内存的占用使用dotTrace及CLR  Profile等工具检查之后发现内存的占用也是这些原始数据然后以” How to reduce the memory usage of NET  applications” 到网上搜了一下减少NET内存占用的一些方法在StackOverflow上看到了这一回答

该同学指出NET应用程序和其他使用本地代码编写的程序相比会有较大的内存占用如果对内存开销比较在意NET可能不是最好的选择NET  应用程序的内存一定程度上受垃圾回收的影响并指出一些数据结构如List系统会分配多余的空间可以使用值类型而不是引用类型不要创建大对象以免产生内存碎片等等降低内存占用的建议

这些都考虑过之后内存还是达不到要求所以开始寻找调用非托管代码的方式来自己更灵活的控制内存的分配与销毁但是整个程序都是采用NET编写的全部切换成C或者C++不现实所以只有两种方案一是使用unsafe  代码二是将数据加载和检索模块采用C或者C++编写NET中采用P/Invoke技术调用

刚开始想采用unsafe代码对数据的加载及检索直接在放在unsafe  代码中后来觉得代码有些乱不同风格的代码混杂在一起不太好而且数据加载和检索的逻辑也比较复杂所以就直接采用第二种方案使用C++编写数据加载和检索逻辑然后在NET里面调用

在开始之前也做了一些评估比如将同样的M的数据加载到内存中都采用字符串的方式存储NET中会占用M的内存而在C++中只有M的样子而且变动很小这正是需要的结果

由于对C++不熟临时抱佛脚翻了下C++ Primier  Plus中关于字符串和STL的相关章节并请求其他开发小组给予了一定的协助定义了基本的接口为了演示我创建了两个工程一个是名为 SecuData的C++  Win DLL工程一个是测试该类库的名为SecuDataTest的C# WinForm程序

我在C++中定义好了个方法一个初始化加载数据一个设置搜索优先级一个查找匹配方法和一个卸载数据方法具体的算法由于工作原因不便贴出这里只是举一个简单的例子方法名及工程结构如下图

然后再在NET中使用P/Invoke技术引入C++ DLL中定义的方法

这样就可以在NET中调用这些方法了需要说明的是方法的传入值这里是使用String类型的第二个StringBuilder类型的参数是方法的真正返回值方法的整体int型返回值表明方法是否执行成功在调用查找方法时第二个StringBuilder参数必须初始化一个最大的查询结果的大小因为在C++中会往这个对象中写入结果不初始化或者初始化太小都会抛出异常当然也可以直接返回结构体这个就需要额外定义这里返回的都是字符串完了自己在NET里面对其进行解析

需要注意的是调试的时候如果需要调试C++里面的代码需要指定DLL的生成目录及启动目标并且将C++项目设置为启动项目这里我设定生成  DLL的目录为SecuDataTest项目生成的SecuDataTestexe文件所在的目录调试启动目标设置为  SecuDataTestexe这样在C++项目中设置断点启动NET Winform程序当P/Invoke触发断点时能够逐步调试C++代码

在发布的时候最好将默认的动态库配置修改为静态库这样VS会把依赖的相关C++库打包到生成的dll中部署到客户机器上不会出现问题SecuData类库项目的属性设置如下图:

改成这种P/Invoke模式之后M数据载到内存中内存占用只有M左右较之前采用NET的M的内存又降低了很多而且内存波动比较小满足了对内存占用的要求

采用这种“混搭”方式有一些好处既有NET的快速开发又有C++的灵活的内存分配销毁模式以及代码安全性保护在很多时候可以将一些对内存占用比较敏感大数据量的处理逻辑放在C++中处理利用灵活的手动内存管理模式降低这部分的内存占用;将核心的数据结构及算法使用C++编写可以提高代码的安全性提高程序的反编译难度

结语

NET应用程序由于需要加载CLR及一些通用类库并且具有垃圾收集机制较其他本地语言如CC++具有较大的footprint使用NET创建一个简单的Winform可能就会占用近M的内存所以随着开发的进行内存占用会比较大当然这些在很多时候是由于开发者自身对NET底层机制不熟悉比如在有些地方可以使用值类型而使用引用类型;创建了大量的临时的周期比较短的对象;使用了过多的静态变量及成员导致内存被长久占用而得不到回收;以及NET内部的一些机制比如集合对象会在内部预先分配多余的空间等等很多时候因为有NET的GC机制使得我们不必去关注对象的销毁而很”大方”的去创建新对象去使用一些重型的内置对象从而导致内存占用过大解决好这些问题其实可以降低NET应用程序的相当大一部分的不必要的内存占用

除了了解NET框架的一些内部机制之外良好的思路高效数据结构和算法也可以使得问题变得简单而减少内存的开销

最后对于对内存要求比较敏感可以利用C/C++的手动的灵活的内存管理语言来编写相应模块NET中采用P/Invoke技术进行调用来减少一些内存

以上是我对降低NET应用程序内存占用的一点儿实践和总结希望对您有所帮助

               

上一篇:ADO.NET 连接数据库字符串小结

下一篇:.Net平台开发的技术规范与实践