減少.NET應(yīng)用程序內(nèi)存占用的一則實踐
最近一周比較忙,主要的工作內(nèi)容是在做一個叫“鍵盤精靈”的東西,簡單來講就是將很多數(shù)據(jù)放到內(nèi)存中,對這些數(shù)據(jù)進(jìn)行快速檢索,然后找出根據(jù)輸入條件最匹配的10條記錄并予以展示。具體和下面兩款炒股軟件的相關(guān)功能類似:
數(shù)據(jù)以文本形式存在文件中,且數(shù)據(jù)量較大,有近20萬條,每一條記錄有幾個字段,以分隔符分割。當(dāng)時使用的是6萬條記錄的測試數(shù)據(jù),文本文件將近10M,這個模塊加載到內(nèi)存并建立緩存之后,大概會占用將近70-80M的內(nèi)存。自我接手以后,主要的任務(wù)就是降低內(nèi)存消耗和提高匹配效率。
一、避免創(chuàng)建不必要的對象
拿到代碼后,第一步就是看設(shè)計文檔,然后斷點(diǎn)一步一步的看代碼,大概明白了邏輯之后,發(fā)現(xiàn)思路有一些問題。之前的代碼處理流程思路大概是下面這樣的:
- 將文件讀取到內(nèi)存,實例化
- 根據(jù)條件對文件進(jìn)行檢索,并存儲到結(jié)果集1中
- 對結(jié)果集1中的結(jié)果進(jìn)行匹配度計算,并存儲到結(jié)果集中2
- 按對結(jié)果集2進(jìn)行匹配度排序,取最匹配的10條記錄,然后返回
這個過程中規(guī)中矩。但是其中有很多問題,最大的問題是,臨時變量存儲了太多的中間處理結(jié)果,而這些對象在一次查詢完成后又馬上丟棄,大量的臨時對象帶來了很大的GC壓力。舉例來說,當(dāng)用戶在輸入框中輸入1的時候,假設(shè)使用Contains來匹配,那么從6萬條記錄中找出包含1的記錄可能有4萬多條,然后需要把這4萬多條記錄存儲在臨時變量中進(jìn)行處理,進(jìn)一步計算這4萬條記錄的匹配度,然后存儲到一個類似KeyValuePair的集合中,key為匹配度,然后對這個集合按Key進(jìn)行排序,然后取前10條最優(yōu)記錄??梢钥吹?,中間創(chuàng)建了大量的臨時變量,使得內(nèi)存劇增,大量臨時對象創(chuàng)建之后馬上會被回收,GC 壓力山大。
而在設(shè)計文檔中,只要求返回最最匹配的10條記錄,之前的解決方案中似乎并沒有注意到這一點(diǎn)。所以接手后,第一步就是對上面的處理過程進(jìn)行精簡。精簡后如下:
- 將文件讀取到內(nèi)存,實例化
- 根據(jù)條件對文件進(jìn)行檢索,如果存在,則:
- 計算匹配度。
- 以匹配度為Key,存儲到只有11個容量的SortList中。
- 如果SortList集合添加記錄后大于10個,則移除最后面一個元素,始終保持著前10個最小(匹配度最優(yōu))的記錄。
- 遍歷完成之后,返回這個集合對象
經(jīng)過這一修改,減少了大量臨時數(shù)據(jù)對內(nèi)存的占用,整個過程中,我只是使用一個容量為11的SortList結(jié)構(gòu)存儲中間的過程,每一次插入一個元素,SortList幫我們排好序,然后移除最不匹配的那一個,也就是最后一個元素(從小到大排序,越匹配,值越小)。這里面的消耗主要是 SortList的插入,內(nèi)部排序和移除記錄。 說到這里在選擇SortList還是SortDictionary的問題上糾結(jié)了一下,于是又找了些資料,SortDictionary在內(nèi)部使用紅黑樹實現(xiàn),SortList采用有序數(shù)組實現(xiàn), 在內(nèi)部排序都為O(logn)的前提下,SortDictionary的O(logn)插入及刪除元素的時間復(fù)雜度高于SortList,但是 SortDictionary會比SortList占用更多內(nèi)存?;緛碚f這是一個查詢速度和內(nèi)存分配之間的平衡,由于這里只需要存儲11個對象,所以兩者相差不大。其實即使沒有這種結(jié)構(gòu),自己也可以實現(xiàn)的,無非就是一個集合,每次添加一個,排好序,然后將最大的那個移除。.NET使用起來方便是因為有很多這些強(qiáng)大的內(nèi)置數(shù)據(jù)結(jié)構(gòu)。
經(jīng)過上面這個小小的修改,內(nèi)存占用一下子降低了1倍,從原來的70-80M,降低到了30-40M,其實這就是降低內(nèi)存開銷的一個最基本的原則,那就是避免創(chuàng)建不必要的對象。
二、優(yōu)化數(shù)據(jù)類型及算法
越到后面內(nèi)存的降低越來越困難。仔細(xì)看了代碼之后,除了上面之外,代碼中也有一些其他問題,比如,一開始就將大量的對象實例化到內(nèi)存中,然后一直保存。每一條記錄中的信息比較多,但真正有用的用于搜索匹配的只有下面四個字段,但是整體的實例化會將其他沒有用的字段也一并序列化進(jìn)去了。導(dǎo)致很多內(nèi)存被無用的字段占用。
股票代碼 股票中文名 中文拼音 市場類型 …… 600000 浦發(fā)銀行 PFYH 上證A股 …… |
所以第一步就是在內(nèi)存中只存放需要檢索的上面四個關(guān)鍵字段,每一條記錄剛開始是使用string[]數(shù)據(jù),而不是使用類或者其它結(jié)構(gòu)來保存,也嘗試使用結(jié)構(gòu)提來保存,但是由于四個字段,數(shù)據(jù)量大,中間還要作為參數(shù)傳遞,所以比使用類還大,這里只是簡單的使用了數(shù)組。
除了上面這些之外,為了提高搜索效率,對數(shù)據(jù)按照0-9,a-z開頭對數(shù)據(jù)做了切分分塊緩存,這樣當(dāng)用戶輸入0時,直接從以0為key的塊中讀取數(shù)據(jù),這樣速度是加快了,但是大量的緩存也增加了對內(nèi)存的消耗。緩存的數(shù)據(jù)基本上和加載到內(nèi)存中原始的數(shù)據(jù)一樣大了。并且在搜索的過程中,也是采用的完全搜索,對于17萬條數(shù)據(jù)的四個字段,每一次查詢要進(jìn)行170000*4次遍歷比較,才能找出最匹配的10條數(shù)據(jù)來。
為此,引入了不完全搜索,就是事先對各類型證券,如 股票,基金,債券分類,對每一類按證券代碼進(jìn)行排序。當(dāng)用戶設(shè)置了搜索的優(yōu)先級時,依次在每一類中查找,如果找到滿足條件的10條記錄,則立即返回,因為數(shù)據(jù)已經(jīng)事先按照證券類型和代碼排好序了,所以后面找到的肯定沒有之前找到的匹配度高,這一改進(jìn)直接提高了搜索查詢的效率。對有序的數(shù)據(jù)進(jìn)行查找效率一般會大于無序的數(shù)據(jù)高。我們常見的一些查找算法,比如說,二分查找法,前提也是待查找的集合有序排列。
#p#
三、采用非托管代碼或者模塊編寫數(shù)據(jù)處理邏輯
上面的兩部操作雖然減少了將近50-60%的內(nèi)存占用,但是仍然達(dá)不到領(lǐng)導(dǎo)的要求,于是又嘗試并比較了各種 使用不同的數(shù)據(jù)結(jié)構(gòu)將數(shù)據(jù)載入到內(nèi)存中的內(nèi)存占用大小,包括直接將文件按類型讀成字符串、數(shù)組、結(jié)構(gòu)及類,內(nèi)存占用最小的直接將文件讀成字符串,10M的數(shù)據(jù)文件讀進(jìn)內(nèi)存也會占用20-30M的空間,還不談對其進(jìn)行處理過程中產(chǎn)生的一些臨時變量對內(nèi)存的占用。使用dotTrace及CLR Profile等工具檢查之后,發(fā)現(xiàn)內(nèi)存的占用也是這些原始數(shù)據(jù)。然后以” How to reduce the memory usage of .NET applications” 到網(wǎng)上搜了一下減少.NET內(nèi)存占用的一些方法,在StackOverflow上看到了這一回答:
該同學(xué)指出.NET應(yīng)用程序和其他使用本地代碼編寫的程序相比會有較大的內(nèi)存占用,如果對內(nèi)存開銷比較在意,.NET可能不是最好的選擇。.NET應(yīng)用程序的內(nèi)存一定程度上受垃圾回收的影響。并指出,一些數(shù)據(jù)結(jié)構(gòu)如List,系統(tǒng)會分配多余的空間??梢允褂弥殿愋投皇且妙愋?,不要創(chuàng)建大對象,以免產(chǎn)生內(nèi)存碎片等等降低內(nèi)存占用的建議。
這些都考慮過之后,內(nèi)存還是達(dá)不到要求,所以開始尋找調(diào)用非托管代碼的方式來自己更靈活的控制內(nèi)存的分配與銷毀。但是整個程序都是采用.NET編寫的,全部切換成C或者C++不現(xiàn)實,所以只有兩種方案,一是使用unsafe 代碼,二是將數(shù)據(jù)加載和檢索模塊采用C或者C++編寫,在.NET中采用P/Invoke技術(shù)調(diào)用。
剛開始想采用unsafe代碼,對數(shù)據(jù)的加載及檢索直接在放在unsafe 代碼中。后來覺得代碼有些亂,不同風(fēng)格的代碼混雜在一起不太好,而且數(shù)據(jù)加載和檢索的邏輯也比較復(fù)雜。所以就直接采用第二種方案,使用C++編寫數(shù)據(jù)加載和檢索邏輯。然后在.NET里面調(diào)用。
在開始之前,也做了一些評估,比如將同樣的10M的數(shù)據(jù)加載到內(nèi)存中,都采用字符串的方式存儲,.NET中會占用20-30M的內(nèi)存,而在C++中只有9-10M的樣子,而且變動很小。這正是需要的結(jié)果。
由于對C++不熟,臨時抱佛腳,翻了下C++ Primier Plus中關(guān)于字符串和STL的相關(guān)章節(jié),并請求其他開發(fā)小組給予了一定的協(xié)助,定義了基本的接口。為了演示,我創(chuàng)建了兩個工程,一個是名為 SecuData的C++ Win32 DLL工程,一個是測試該類庫的名為SecuDataTest的C# WinForm程序。
我在C++中定義好了4個方法,一個初始化加載數(shù)據(jù),一個設(shè)置搜索優(yōu)先級,一個查找匹配方法和一個卸載數(shù)據(jù)方法,具體的算法由于工作原因不便貼出,這里只是舉一個簡單的例子,方法名及工程結(jié)構(gòu)如下圖:
然后再在.NET中使用P/Invoke技術(shù)引入C++ DLL中定義的方法。
這樣就可以在.NET中調(diào)用這些方法了,需要說明的是,方法的傳入值這里是使用String類型的,第二個StringBuilder類型的參數(shù)是方法的真正返回值,方法的整體int型返回值表明方法是否執(zhí)行成功。在調(diào)用查找方法時,第二個StringBuilder參數(shù)必須初始化一個最大的查詢結(jié)果的大小,因為在C++中會往這個對象中寫入結(jié)果,不初始化或者初始化太小都會拋出異常。當(dāng)然也可以直接返回結(jié)構(gòu)體,這個就需要額外定義,這里返回的都是字符串。完了自己在.NET里面對其進(jìn)行解析。
唯一需要注意的是,調(diào)試的時候,如果需要調(diào)試C++里面的代碼,需要指定DLL的生成目錄及啟動目標(biāo),并且將C++項目設(shè)置為啟動項目,這里我設(shè)定生成 DLL的目錄為SecuDataTest項目生成的SecuDataTest.exe文件所在的目錄,調(diào)試啟動目標(biāo)設(shè)置為 SecuDataTest.exe,這樣在C++項目中設(shè)置斷點(diǎn),啟動.NET Winform程序,當(dāng)P/Invoke觸發(fā)斷點(diǎn)時能夠逐步調(diào)試C++代碼。SecuData類庫項目的屬性設(shè)置如下圖:
改成這種P/Invoke模式之后,10M數(shù)據(jù)載到內(nèi)存中,內(nèi)存占用只有10M左右,較之前采用.NET的30-40M的內(nèi)存又降低了很多,而且內(nèi)存波動比較小,滿足了對內(nèi)存占用的要求。
采用這種“混搭”方式有一些好處,既有.NET的快速開發(fā),又有C++的靈活的內(nèi)存分配銷毀模式以及代碼安全性保護(hù)。在很多時候,可以將一些對內(nèi)存占用比較敏感,大數(shù)據(jù)量的處理邏輯,放在C++中處理,利用靈活的手動內(nèi)存管理模式降低這部分的內(nèi)存占用;將核心的數(shù)據(jù)結(jié)構(gòu)及算法使用C++編寫,可以提高代碼的安全性,提高程序的反編譯難度。
四、結(jié)語
.NET應(yīng)用程序由于需要加載CLR及一些通用類庫,并且具有垃圾收集機(jī)制,較其他本地語言如C+,C++具有較大的footprint,使用.NET創(chuàng)建一個簡單的Winform可能就會占用近10M的內(nèi)存,所以隨著開發(fā)的進(jìn)行,內(nèi)存占用會比較大。當(dāng)然這些在很多時候是由于開發(fā)者自身對.NET底層機(jī)制不熟悉,比如在有些地方可以使用值類型而使用引用類型;創(chuàng)建了大量的臨時的周期比較短的對象;使用了過多的靜態(tài)變量及成員導(dǎo)致內(nèi)存被長久占用而得不到回收;以及.NET內(nèi)部的一些機(jī)制,比如集合對象會在內(nèi)部預(yù)先分配多余的空間等等。很多時候因為有.NET的GC機(jī)制,使得我們不必去關(guān)注對象的銷毀而很”大方”的去創(chuàng)建新對象,去使用一些重型的內(nèi)置對象,從而導(dǎo)致內(nèi)存占用過大。解決好這些問題,其實可以降低.NET應(yīng)用程序的相當(dāng)大一部分的不必要的內(nèi)存占用。
除了了解.NET框架的一些內(nèi)部機(jī)制之外,良好的思路,高效數(shù)據(jù)結(jié)構(gòu)和算法也可以使得問題變得簡單而減少內(nèi)存的開銷。
最后對于對內(nèi)存要求比較敏感,可以利用C/C++的手動的靈活的內(nèi)存管理語言來編寫相應(yīng)模塊,在.NET中采用P/Invoke技術(shù)進(jìn)行調(diào)用來減少一些內(nèi)存。
以上是我對降低.NET應(yīng)用程序內(nèi)存占用的一點(diǎn)兒實踐和總結(jié),希望對您有所幫助。
原文鏈接:http://www.cnblogs.com/yangecnu/archive/2013/03/10/Reduce_the_DotNET_Applications_Memory_Usage.html