字符串太佔內存了,我想了各類奇思淫巧對它進行壓縮

一:背景

1. 講故事

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

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

思考以後,下面咱們就一塊兒驗證下,使用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:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.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的大小。spa

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,這就是展現的全部原始狀況。code

二:壓縮技巧分析

1. 使用字典化處理

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

class Program
    {
        public static Dictionary<int, string> orderfromDict = new Dictionary<int, string>();

        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看一下空間到底壓縮了多少?blog

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:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.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,並且還有不小幅度的修改業務邏輯,<font color="red">你們都知道線上的代碼是能不改則不改,不改確定沒錯,改出問題確定是你兜着走</font>,是吧,那問題就來了,如何最小化的修改並且還能壓縮空間,有這樣一箭雙鵰的事情嗎???element

2. 利用字符串駐留池

貌似一說出來,你們都如夢初醒,駐留池的出現就是爲了解決這個問題,CLR會在內部維護了一個我剛纔定義的字典機制,重複的字符串就不須要在堆上再次分配,直接存它的引用地址便可,若是你不清楚駐留池,建議看一下我這篇: https://www.cnblogs.com/huang...

接下來只須要在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:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.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<Trade> 中的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                    0     <TradeID>k__BackingField
        00007ff9eb2b59c0  4000002        8            System.String      0     instance     000001f8a1516030     <TradeFrom>k__BackingField

0:000> !DumpObj /d 000001f8a1516030
Name:        System.String
String:      WAP

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

再回頭dump一下使用字典化方式的Trade,能夠看到它是沒有 <TradeFrom>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                    0     <TradeID>k__BackingField
        00007ff9eb2b7d20  4000003        c              System.Byte      1     instance                    0     <TradeFromID>k__BackingField

三:總結

你們能夠根據本身的狀況使用,使用駐留池方式是改變最小的,簡單粗暴,本身構建字典化雖然最省內存,但須要修正業務邏輯,這個風險自擔哦。。。

相關文章
相關標籤/搜索