字元串太占記憶體了,我想了各種奇思淫巧對它進行壓縮

来源:https://www.cnblogs.com/huangxincheng/archive/2020/06/04/13041367.html
-Advertisement-
Play Games

一:背景 1. 講故事 在我們的一個全記憶體項目中,需要將一家大品牌店鋪小千萬的trade灌入到記憶體中,大家知道trade中一般會有訂單來源,省市區 ,當把這些欄位灌進去後,你會發現他們特別侵蝕記憶體,因為都是字元串類型,不知道大家對記憶體侵蝕性是不是很清楚,我就問一個問題。 Question: 一個空字 ...


一:背景

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:\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的大小。


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

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,而且還有不小幅度的修改業務邏輯,大家都知道線上的代碼是能不改則不改,不改肯定沒錯,改出問題肯定是你兜著走,是吧,那問題就來了,如何最小化的修改而且還能壓縮空間,有這樣兩全其美的事情嗎???

2. 利用字元串駐留池

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

接下來只需要在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


三:總結

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


如您有更多問題與我互動,掃描下方進來吧~


圖片名稱
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C#I/O操作 FileStream FileStream對象表示在磁碟或網路路徑上指向文件的流。這個類提供了在文件中讀寫位元組的方法,使用FileStream能夠對對系統上的文件進行讀、寫、打開、關閉等操作。並對其他與文件相關的操作系統提供句柄操作,如管道,標準輸入和標準輸出。讀寫操作可以指定為同步 ...
  • 日常開發維護項目中,可能會遇到發佈後出現bug,或者忘記改配置文件等等問題,這個時候,可能就需要重新進行下發佈,有的開發小伙伴可能會把編譯後的代碼文件整個替換。這樣做雖然也可以實現發佈,但是有幾個弊端,一個是速度慢,二個是會造成不穩定,假如不關閉站點的話,前端發出請求到後端後,將會出現異常信息。 換 ...
  • 最近維護一批代碼,其中包括一堆if...的使用,多的情況嵌套8、9層,痛苦不堪,所以搜尋一些可以降低if...else的方法來改善一下代碼,寫個簡單總結。 第一種: 優化前 if (measuredValue > 8) return 5 * measuredValue * measuredValue ...
  • 0. 前言 在之前我們介紹了請求通過路由尋找到控制器,以及控制器與視圖的數據流轉。那麼,我們回過頭來,再看看路由的一些其他用法。 1. 路由屬性(Route Attribute) 按照英文的直接翻譯,Routing Attribute 的意思是路由屬性,但實際上 Attribute在微軟的官方稱呼是 ...
  • 一直對async/await存在疑惑,在博客園裡看到了文中提及的博客,感覺講的很好,自己也學習到了。所以進行簡單的摘要 ...
  • 預設情況下所有的Nuget包都會下載到C盤,目前我這邊有幾十個G的大小,這導致我C盤的容量越來越小... 我們可以在Nuget.config中修改package存放路徑,Nuget.config 在C:\Users\{UserName}\AppData\Roaming\NuGet目錄下 預設如下所示 ...
  • 在做介面測試時,經常會碰到請求參數為token的類型,但是可能大部分測試人員對token,cookie,session的區別還是一知半解。為此我查閱大量的資料做瞭如下總結。 此篇文章也許是最全最通俗的關於Token ,Cookie和Session的區別的文章,好好揣摩文章的每一個字,也許你會有更深的 ...
  • C#將DataTable數據導出CSV文件通用方法! //導出按鈕調用導出方法 protected void btnCSV_Click(object sender, EventArgs e) { DataTable dt = ExportData();//獲取datatable數據源 string ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...