歡迎您光臨本站 註冊首頁

c#壓縮字符串的方法

←手機掃碼閱讀     ljg58026 @ 2020-06-26 , reply:0

一:背景
 

1. 講故事
 

在我們的一個全內存項目中,需要將一家大品牌店鋪小千萬的trade灌入到內存中,大家知道trade中一般會有訂單來源,省市區 ,當把這些字段灌進去後,你會發現他們特別侵蝕內存,因為都是字符串類型,不知道大家對內存侵蝕性是不是很清楚,我就問一個問題。

Question: 一個空字符串佔用多大內存? 你知道嗎?

思考之後,下面我們就一起驗證下,使用windbg去託管堆一查究竟,代碼如下:

   static void Main(string[] args)   {    string s = string.Empty;      Console.ReadLine();   }    0:000> !clrstack -l  OS Thread Id: 0x308c (0)   Child SP  IP Call Site  ConsoleApp6.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp6Program.cs @ 19]   LOCALS:   0x00000087391febd8 = 0x000002605da91420  0:000> !DumpObj /d 000002605da91420  Name: System.String  String:   Fields:    MT Field Offset   Type VT Attr  Value Name  00007ff9eb2b85a0 4000281 8  System.Int32 1 instance  0 m_stringLength  00007ff9eb2b6838 4000282 c  System.Char 1 instance  0 m_firstChar  00007ff9eb2b59c0 4000286 d8 System.String 0 shared  static Empty       >> Domain:Value 000002605beb2230:NotInit <<  0:000> !objsize 000002605da91420  sizeof(000002605da91420) = 32 (0x20) bytes (System.String)

 

從圖中你可以看到,僅僅一個空字符串就要佔用 32byte,如果500w個空字符串就是: 32byte x 500w = 152M,是不是不算不知道,一算嚇一跳。。。 這還僅僅是一個什麼都沒有的空字符串哦。

2. 迴歸到Trade

問題也已經擺出來了,接下來回歸到Trade中,為了方便演示,先模擬以文件的形式從數據庫讀取20w的trade。

   class Program   {   static void Main(string[] args)   {    var trades = Enumerable.Range(0, 20 * 10000).Select(m => new Trade()    {    TradeID = m,    TradeFrom = File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")       .ElementAt(m % 4)    }).ToList();      GC.Collect(); //方便測試,把臨時變量清掉    Console.WriteLine("執行成功");    Console.ReadLine();   }   }     class Trade   {   public int TradeID { get; set; }   public string TradeFrom { get; set; }   }

 

然後用windbg去跑一下託管堆,再量一下trades的大小。

  0:000> !dumpheap -stat  Statistics:    MT Count TotalSize Class Name  00007ff9eb2b59c0 200200 7010246 System.String    0:000> !objsize 0x000001a5860629a8  sizeof(000001a5860629a8) = 16097216 (0xf59fc0) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

 

從上面輸出中可以看到託管堆有200200 = 20w(程序分配)+ 200(系統分配)個,然後再看size: 16097216/1024/1024= 15.35M,這就是展示的所有原始情況。

二:壓縮技巧分析

1. 使用字典化處理

其實在託管堆上有20w個字符串,但你仔細觀察一下會發現其實就是4種狀態的重複顯示,要麼一淘,要麼淘寶。。。這就給了我優化機會,何不在獲取數據的時候構建好OrderFrom的字典,然後在trade中附增一個TradeFromID記錄字典中的映射值,因為特徵值少,用byte就可以了,有了這個思想,可以把代碼修改如下:

   class Program   {   public static DictionaryorderfromDict = new Dictionary();     static void Main(string[] args)   {    var trades = Enumerable.Range(0, 20 * 10000).Select(m =>    {    var tradefrom = File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")       .ElementAt(m % 4);      var kv = orderfromDict.FirstOrDefault(k => k.Value == tradefrom);      if (kv.Key == 0)    {     orderfromDict.Add(orderfromDict.Count + 1, tradefrom);    }      var trade = new Trade() { TradeID = m, TradeFromID = (byte)kv.Key };      return trade;      }).ToList();      GC.Collect(); //方便測試,把臨時變量清掉      Console.WriteLine("執行成功");      Console.ReadLine();   }   }     class Trade   {   public int TradeID { get; set; }     public byte TradeFromID { get; set; }     public string TradeFrom   {    get    {    return Program.orderfromDict[TradeFromID];    }   }   }

 

代碼還是很簡單的,接下來用windbg看一下空間到底壓縮了多少?

  0:000> !dumpheap -stat  Statistics:    MT Count TotalSize Class Name  00007ff9eb2b59c0 204 10386 System.String    0:000> !clrstack -l  OS Thread Id: 0x2ce4 (0)   Child SP  IP Call Site  ConsoleApp6.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp6Program.cs @ 42]   LOCALS:   0x0000006f4d9ff078 = 0x0000016fdcf82ab8    0000006f4d9ff288 00007ff9ecd96c93 [GCFrame: 0000006f4d9ff288]   0:000> !objsize 0x0000016fdcf82ab8  sizeof(0000016fdcf82ab8) = 6897216 (0x693e40) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

 

從上面的輸出中可以看到,託管堆上string現在是:204 = 4(程序分配) + 200(系統分配)個,這4個就是字典中的4個哦,空間的話:6897216 /1024/1024= 6.57M,對應之前的 15.35M優化了將近60%。

雖然優化了60%,但這種優化是破壞性的優化,需要修改我的Trade結構,同時還要定義個Dictionary,而且還有不小幅度的修改業務邏輯,大家都知道線上的代碼是能不改則不改,不改肯定沒錯,改出問題肯定是你兜著走,是吧,那問題就來了,如何最小化的修改而且還能壓縮空間,有這樣兩全其美的事情嗎???

2. 利用字符串駐留池

貌似一說出來,大家都如夢初醒,駐留池的出現就是為了解決這個問題,CLR會在內部維護了一個我剛才定義的字典機制,重複的字符串就不需要在堆上再次分配,直接存它的引用地址即可,如果你不清楚駐留池,建議看一下我這篇: https://www.jb51.net/article/189450.htm

接下來只需要在tradefrom 字段包一層 string.Intern 即可,改動不要太小,代碼如下:

   static void Main(string[] args)   {    var trades = Enumerable.Range(0, 20 * 10000).Select(m => new Trade()    {    TradeID = m,    TradeFrom = string.Intern(File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")       .ElementAt(m % 4)), //包一層 string.Intern    }).ToList();      GC.Collect(); //方便測試,把臨時變量清掉    Console.WriteLine("執行成功");    Console.ReadLine();   }

 

然後用windbg抓一下託管堆。

  0:000> !dumpheap -stat   Statistics:    MT Count TotalSize Class Name  00007ff9eb2b59c0 204 10386 System.String    0:000> !clrstack -l  OS Thread Id: 0x13f0 (0)   Child SP  IP Call Site    ConsoleApp6.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp6Program.cs @ 27]   LOCALS:   0x0000005e4d3ff0a8 = 0x000001f8a15129a8    0000005e4d3ff2b8 00007ff9ecd96c93 [GCFrame: 0000005e4d3ff2b8]   0:000> !objsize 0x000001f8a15129a8  sizeof(000001f8a15129a8) = 8497368 (0x81a8d8) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

 

觀察後發現,當用了駐留池之後空間為: 8497368 /1024/1024 =8.1M,你可能有疑問,為什麼和字典化相比內存要大24%呢? 仔細觀察你會發現,當用駐留池後,List中的TradeFrom存的是string在堆中的內存地址,在x64機器上要佔用8個字節,而字典化方式內存堆上Trade是不分配TradeFrom,而是用了一個byte來替代,總體來說相當於一個trade省了7byte的空間,然後用windbg看一下。

  0:000> !da -length 1 -details 000001f8b16f9b68  Name: ConsoleApp6.Trade[]  Size: 2097176(0x200018) bytes  Array: Rank 1, Number of elements 262144, Type CLASS     Fields:     MT Field Offset   Type VT Attr  Value Name   00007ff9eb2b85a0 4000001 10  System.Int32 1 instance   0k__BackingField   00007ff9eb2b59c0 4000002 8  System.String 0 instance 000001f8a1516030k__BackingField    0:000> !DumpObj /d 000001f8a1516030  Name: System.String  String: WAP

 

可以看到, 000001f8a1516030 就是 堆上 string=Wap的引用地址,這個地址佔用了8byte空間。

再回頭dump一下使用字典化方式的Trade,可以看到它是沒有 k__BackingField字段的。

  0:000> !da -length 1 -details 000001ed52759ac0  Name: ConsoleApp6.Trade[]  Size: 262168(0x40018) bytes  Array: Rank 1, Number of elements 32768, Type CLASS   Fields:     MT Field Offset   Type VT Attr  Value Name   00007ff9eb2b85a0 4000002 8  System.Int32 1 instance   0k__BackingField   00007ff9eb2b7d20 4000003 c  System.Byte 1 instance   0k__BackingField

 

三:總結

大家可以根據自己的情況使用,使用駐留池方式是改變最小的,簡單粗暴,自己構建字典化雖然最省內存,但需要修正業務邏輯,這個風險自擔哦。。。

               

   


[ljg58026 ] c#壓縮字符串的方法已經有242次圍觀

http://coctec.com/docs/c/language/show-post-239781.html