熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

減少.NET應用程序內存占用的一則實踐

2022-06-13   來源: .NET編程 

  最近一周比較忙主要的工作內容是在做一個叫“鍵盤精靈”的東西簡單來講就是將很多數據放到內存中對這些數據進行快速檢索然後找出根據輸入條件最匹配的條記錄並予以展示具體和下面兩款炒股軟件的相關功能類似

減少.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使用起來方便是因為有很多這些強大的內置數據結構

減少.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可能不是最好的選擇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應用程序內存占用的一則實踐

  然後再在NET中使用P/Invoke技術引入C++ DLL中定義的方法

減少.NET應用程序內存占用的一則實踐

  這樣就可以在NET中調用這些方法了需要說明的是方法的傳入值這裡是使用String類型的第二個StringBuilder類型的參數是方法的真正返回值方法的整體int型返回值表明方法是否執行成功在調用查找方法時第二個StringBuilder參數必須初始化一個最大的查詢結果的大小因為在C++中會往這個對象中寫入結果不初始化或者初始化太小都會拋出異常當然也可以直接返回結構體這個就需要額外定義這裡返回的都是字符串完了自己在NET裡面對其進行解析

  需要注意的是調試的時候如果需要調試C++裡面的代碼需要指定DLL的生成目錄及啟動目標並且將C++項目設置為啟動項目這裡我設定生成 DLL的目錄為SecuDataTest項目生成的SecuDataTestexe文件所在的目錄調試啟動目標設置為 SecuDataTestexe這樣在C++項目中設置斷點啟動NET Winform程序當P/Invoke觸發斷點時能夠逐步調試C++代碼

  在發布的時候最好將默認的動態庫配置修改為靜態庫這樣VS會把依賴的相關C++庫打包到生成的dll中部署到客戶機器上不會出現問題SecuData類庫項目的屬性設置如下圖:

減少.NET應用程序內存占用的一則實踐

  改成這種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應用程序內存占用的一點兒實踐和總結希望對您有所幫助


From:http://tw.wingwit.com/Article/program/net/201311/14229.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.