日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

字符串池化,减少了三分之一的内存占用

發布時間:2023/12/4 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 字符串池化,减少了三分之一的内存占用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

字符串池化,減少重復實例,內存降低,一切就是這樣的輕松愉快。

開篇摘要

本文通過一個簡單的業務場景,來描述如何通過字符串池化來減少內存中的重復字符串實例,從而減少內存的占用。

在業務中,我們假設如下:

  • 有一百萬個商品,每個商品都有一個 ProductId 和 Color 列保存在數據庫中

  • 需要將所有的數據加載到內存中,作為緩存使用

  • 每個產品都有 Color

  • Color 的范圍是一個有限的范圍,我們假設大約為八十個左右

學習 dotMemory 度量內存

既然需要度量內存優化的可靠性,那么一個簡單有效的度量工具自然必不可少。

本篇,我們介紹 Rider + dotMemory 的組合,如何進行簡單的內存度量。讀者也可以根據自己的實際,選擇自己青睞的工具。

首先,我們創建一個單元測試項目,并且編寫一個簡單的內存字典構建過程:

public?const?int?ProductCount?=?1_000_000;public?static?readonly?List<string>?Colors?=?new[]{"amber",?//?此處實際上有80個左右的字符串,省略篇幅}.OrderBy(x?=>?x).ToList();public?static?Dictionary<int,?ProductInfo>?CreateDict() {var?random?=?new?Random(36524);var?dict?=?new?Dictionary<int,?ProductInfo>(ProductCount);for?(int?i?=?0;?i?<?ProductCount;?i++){dict.Add(i,?new?ProductInfo{ProductId?=?i,Color?=?Colors[random.Next(0,?Colors.Count)]});}return?dict; }

從以上代碼可以看出:

  • 創建了一百萬個商品對象,其中的 Color 通過隨機數進行隨機選取。

提前指定字典的大小的預期值,實際上也是一種優化。請參閱https://docs.microsoft.com/dotnet/api/system.collections.generic.dictionary-2.-ctor?view=net-5.0&WT.mc_id=DX-MVP-5003606#System_Collections_Generic_Dictionary_2__ctor_System_Int32_

然后,我們引入 dotMemory 單元測試度量必要的 nuget 包,和其他一些無關緊要的包:

<ItemGroup><PackageReference?Include="JetBrains.DotMemoryUnit"?Version="3.1.20200127.214830"?/><PackageReference?Include="Humanizer"?Version="2.11.10"?/> </ItemGroup>

接著,我們創建一個簡單的測試來度量以上字典的創建前后,內存的變化:

public?class?NormalDictTest {[Test][DotMemoryUnit(FailIfRunWithoutSupport?=?false)]public?void?CreateDictTest(){var?beforeStart?=?dotMemory.Check();var?dict?=?HelperTest.CreateDict();GC.Collect();dotMemory.Check(memory?=>{var?snapshotDifference?=?memory.GetDifference(beforeStart);Console.WriteLine(snapshotDifference.GetNewObjects().SizeInBytes.Bytes());});} }

從以上代碼可以看出:

  • 在字典創建之前,我們通過dotMemory.Check()來捕捉當前內存的快照,以便后續進行對比

  • 字典創建完畢后,我們比對前后兩次檢查點中新增的對象的大小。

最后,點擊如下圖所示的按鈕,運行這個測試:

run dotMemory

那么,就會到的如下這樣的結果:

result

故而,我們可以得出這樣一個簡單的結論。這樣一個字典,大約需要 61MB 的內存。

而這是理論上,這個字典占用了內存最小情況。因為,其中每個 Color 使用的都是上面的八十個范圍之一。因此,他們達到了沒有任何重復實例的目的。

這個數據將會作為后續代碼的一個基準。

嘗試從數據庫載入到內存

實際業務肯定是從數據庫之類的持久化存儲載入到內存中的。因此,我們度量一下,沒有經過優化情況下,這種載入方式大概需要多大的內存開銷。

這里,我們使用 SQLite 作為演示的存儲數據庫,實際上用什么都可以,因為我們關心的是最終緩存的大小。

我們,引入一些無關緊要的包:

<ItemGroup><PackageReference?Include="Dapper"?Version="2.0.90"?/><PackageReference?Include="System.Data.SQLite.Core"?Version="1.0.115"?/> </ItemGroup>

我們編寫一個測試代碼,將一百萬測試數據寫入到測試庫中:

[Test] public?async?Task?CreateDb() {var?fileName?=?"data.db";if?(File.Exists(fileName)){return;}var?connectionString?=?GetConnectionString(fileName);await?using?var?sqlConnection?=?new?SQLiteConnection(connectionString);await?sqlConnection.OpenAsync();await?using?var?transaction?=?await?sqlConnection.BeginTransactionAsync();await?sqlConnection.ExecuteAsync(@" CREATE?TABLE?Product(ProductId?int?PRIMARY?KEY,Color?TEXT )",?transaction);var?dict?=?CreateDict();foreach?(var?(_,?p)?in?dict){await?sqlConnection.ExecuteAsync(@" INSERT?INTO?Product(ProductId,Color) VALUES(@ProductId,@Color)",?p,?transaction);}await?transaction.CommitAsync(); }public?static?string?GetConnectionString(string?filename) {var?re?=$"Data?Source={filename};Cache?Size=5000;Journal?Mode=WAL;Pooling=True;Default?IsolationLevel=ReadCommitted";return?re; }

以上代碼:

  • 創建一個名為 data.db 的數據

  • 在數據庫中創建一個 Product 表,包含 ProductId 和 Color 兩列

  • 將字典中的所有數據插入到這兩個表中,其實就是前文創建的那個字典

運行這個測試,大概十秒左右,測試數據也就準備好了。后續,我們將重復從這個數據庫讀取數據,作為我們的測試用例。

現在,我們編寫一個從數據庫讀取數據,然后載入到字典的代碼,并且度量一下內存的變化:

[Test] [DotMemoryUnit(FailIfRunWithoutSupport?=?false)] public?async?Task?LoadFromDbAsync() {var?beforeStart?=?dotMemory.Check();var?dict?=?new?Dictionary<int,?ProductInfo>(HelperTest.ProductCount);await?LoadCoreAsync(dict);GC.Collect();dotMemory.Check(memory?=>{var?snapshotDifference?=?memory.GetDifference(beforeStart);Console.WriteLine(snapshotDifference.GetNewObjects().SizeInBytes.Bytes());}); }public?static?async?Task?LoadCoreAsync(Dictionary<int,?ProductInfo>?dict) {var?connectionString?=?HelperTest.GetConnectionString();await?using?var?sqlConnection?=?new?SQLiteConnection(connectionString);await?sqlConnection.OpenAsync();await?using?var?reader?=?await?sqlConnection.ExecuteReaderAsync("SELECT?ProductId,?Color?FROM?Product");var?rowParser?=?reader.GetRowParser<ProductInfo>();while?(await?reader.ReadAsync()){var?productInfo?=?rowParser.Invoke(reader);dict[productInfo.ProductId]?=?productInfo;} }

以上代碼:

  • 我們改變了字典的創建方式,將其中的數據從數據庫中讀取并載入

  • 使用 Dapper 讀取 DataReader 并且全部載入字典

同樣,我們運行 dotMemory 度量變化,可以得到數據為:

95.1?MB

因此,我們得出,采用這種方式,多消耗了 30MB 左右的內存。看起來很少,但其實比前面多了 50%。(一千五工資加薪到三千,漲薪 100%的即時感)

當然,你可能會懷疑,多出來的這些開銷實際上是數據庫操作消耗的。但通過下文的優化,我們可以提前知道:

這些多出來的開銷,實際上是因為存在重復的字符串消耗。

剔除重復的字符串實例

既然我們懷疑多出來的開銷是重復的字符串,那么我們就可以考慮通過將它們轉為同一個對象的方式,減少字典中重復的字符串。

所以,我們就有了下面這個版本的測試代碼:

[Test] [DotMemoryUnit(FailIfRunWithoutSupport?=?false)] public?async?Task?LoadFromDbAsync() {var?beforeStart?=?dotMemory.Check();var?dict?=?new?Dictionary<int,?ProductInfo>(HelperTest.ProductCount);await?DbReadingTest.LoadCoreAsync(dict);foreach?(var?(_,?p)?in?dict){var?colorIndex?=?HelperTest.Colors.BinarySearch(p.Color);var?color?=?HelperTest.Colors[colorIndex];p.Color?=?color;}GC.Collect();dotMemory.Check(memory?=>{var?snapshotDifference?=?memory.GetDifference(beforeStart);Console.WriteLine(snapshotDifference.GetNewObjects().SizeInBytes.Bytes());}); }

以上代碼:

  • 我們仍然從數據庫載入所有的數據到字典中,載入的代碼和先前完全一樣,因此沒有展示

  • 載入之后,我們再次遍歷字典。并且從早在第一個版本就存在的 Color List 搜索到對應的字符串實例,并且賦值給字典中的 Color

  • 通過這樣一搜,一讀,一換。我們使得字典中的 Color 全部來自 Color List

于是,我們再次運行 dotMemory 進行度量,結果非常的 Amazing:

61.69?MB

雖說,最終這個數字的開銷對比,第一個版本略有上升,但其實已經到了相差無幾的地步。

我們通過將相同字符串轉為相同實例的方式,將字典中的相同 Color 轉為了相同實例。而 30MB 的臨時字符串則會由于沒有對象引用它們,因此在最近的一次 GC 中會被立即回收,一切都是這樣的輕松愉快。

直接引入 StringPool

前文我們已經找到了開銷的原因,并且通過辦法進行了優化。不過還存在一些問題實際上要考慮:

  • 很多時候 Color List 并不是靜態的列表,她可能早上還很開心,下午就生氣了

  • Color List 不可能無限大,我們需要一個淘汰算法,淘汰末尾的 10%,把他們輸送給社會

因此,我們可以考慮直接使用 StringPool,別人寫的代碼很棒,現在是我們的了。

讓我們再引入一些無關緊要的包:

<ItemGroup><PackageReference?Include="Microsoft.Toolkit.HighPerformance"?Version="7.0.2"?/> </ItemGroup>

稍微改了一下,就有了新的版本:

[Test] [DotMemoryUnit(FailIfRunWithoutSupport?=?false)] public?async?Task?LoadFromDbAsync() {var?beforeStart?=?dotMemory.Check();var?dict?=?new?Dictionary<int,?ProductInfo>(HelperTest.ProductCount);await?DbReadingTest.LoadCoreAsync(dict);var?stringPool?=?StringPool.Shared;foreach?(var?(_,?p)?in?dict){p.Color?=?stringPool.GetOrAdd(p.Color);}GC.Collect();dotMemory.Check(memory?=>{var?snapshotDifference?=?memory.GetDifference(beforeStart);Console.WriteLine(snapshotDifference.GetNewObjects().SizeInBytes.Bytes());}); }

以上代碼:

  • 使用了 StringPool.Shared 實例存儲字符串實例

  • GetOrAdd 實際上就是實現了我們先前的一搜,一讀,一換三步走戰略

當然,結果也是毫無驚喜可言的驚喜:

61.81?MB

一切就是這樣的輕松愉快。

diff

延伸閱讀

StringPool 和 string.Intern() 有什么異同?

它們都是為了解決重復字符串實例過多,導致浪費內存的情況。

效果上的區別,主要是生存期的區別。string.Intern 是終生制的,一旦加入只要程序不重啟,就會一直存在。這和 StringPool 很不一樣。

因此,如果你有生存期上的考慮,請斟酌選擇。

string.Intern 可以參閱https://docs.microsoft.com/dotnet/api/system.string.intern?view=net-5.0&WT.mc_id=DX-MVP-5003606

StringPool 是怎么實現的?

咱也不懂,咱也不敢亂說。總的來說是一個帶有使用計數標記的優先隊列。源代碼咱也讀不懂。

前面的區域,就交給你探索吧:

https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.HighPerformance/Buffers/StringPool.cs

我該在什么情況下考慮使用 StringPool?

筆者建議,考慮這些字符串入池:

  • 這個字符串可能被很多實例引用

  • 這個字符串需要長期駐留,或者持有它的對象,是長期對象

  • 內存優化確實已經成為你要考慮的事情了

  • 當然,其實存在一個最容易判斷的依據。你可以直接把產線上的內存 dump 下來,查看里面是否存在很多重復的字符串,然后優化他們。現在已經是 2021 年了,不會還有人不會 dump 內存吧,不會吧,不會吧?(手動狗頭 如果你還不會 dump 內存,那么可以參閱黃老師在微軟 Reactor 上分享的視頻進行學習:https://www.bilibili.com/video/BV1jZ4y1P7EY

    好耶!我可以用 StringPool 來存儲枚舉的 DisplayName

    確實,也沒有什么錯。不過,其實還有更好的一些方案:

    https://github.com/Spinnernicholas/EnumFastToStringDotNet

    本篇小結

    dotMemory 度量還有更多姿勢,你可以多多嘗試。

    重復,池化。這是一種非常常見的優化方案。掌握它們,在你需要的時候,這或許就幫到了你。

    本篇文章中代碼實例,可以在以下地址找到,不要忘記為項目 star 喲:

    https://github.com/newbe36524/Newbe.Demo/tree/main/src/BlogDemos/Newbe.StringPools

    總結

    以上是生活随笔為你收集整理的字符串池化,减少了三分之一的内存占用的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 国产妇女馒头高清泬20p多 | 福利第一页 | 天堂岛av| 亚洲天堂va | av不卡一区二区三区 | 国产精品久久久免费视频 | 国产一区二区在线观看免费 | 天堂在线www | 五月婷婷综合久久 | 欧美亚洲色综久久精品国产 | 午夜视频在线观看一区 | 日韩激情小视频 | 欧美日韩视频一区二区三区 | www.久久爱 | 大黄网站在线观看 | 女人高潮特级毛片 | 极品少妇在线观看 | 国产日韩欧美二区 | 日韩一级黄色大片 | 亚洲成人中文 | 国产亚洲欧美在线精品 | 欧美精品久久久久久久 | 超碰人人擦 | 亚洲欧美在线看 | 91香蕉视频在线观看免费 | 久久综合免费 | 无套内谢大学处破女www小说 | 中文字幕在线观看免费视频 | 老司机深夜福利视频 | 探花系列在线观看 | 精品少妇人妻一区二区黑料社区 | 国产精品伦子伦免费视频 | 草草影院最新网址 | 亚洲AV无码成人国产精品色 | 亚洲人免费 | 精品日韩在线视频 | 欧美成人综合网站 | 亚洲射射 | 亚洲欧美国产精品 | 97国产精东麻豆人妻电影 | 91午夜在线 | 色狠狠一区二区三区 | 少妇一级淫免费播放 | 日本理论片午伦夜理片在线观看 | 夜夜嗨av色一区二区不卡 | 波多野结衣在线网址 | 一级黄色录像大片 | 在线观看亚洲大片短视频 | 日本成人中文字幕 | 午夜精品一区二区三区在线 | 丰满少妇被猛烈进入高清播放 | 男朋友是消防员第一季 | 夜夜春很很躁夜夜躁 | 熟妇人妻中文字幕无码老熟妇 | 综合激情在线 | 亚洲精品在线播放视频 | 天天干夜夜做 | www.波多野结衣.com | 亚洲综合在线一区二区 | 国产日韩一区二区在线 | 在线成人一区 | 亚洲 欧美 日韩系列 | 免费成人深夜小野草 | 国产裸体视频网站 | 久久精品视频18 | 最新中文字幕2019 | 欧美特级aaa | 欧美精品自拍视频 | 日日操夜夜干 | 国产又粗又黄又爽视频 | 好吊视频一区 | 九九综合 | 男人午夜影院 | 日韩成人在线视频观看 | 欧美黄色aaa| 婷婷国产成人精品视频 | 久久福利网 | 天天狠天天插天天透 | 国产一线二线三线在线观看 | 九九爱精品视频 | 亚洲23p| 黄色片视频免费看 | 日本涩涩网站 | 精品视频久久久久久 | 国产午夜激情 | 欧美一级免费 | www色亚洲 | 黄色片免费网站 | 国模私拍视频在线 | 国产免费片 | 国产成人在线视频播放 | 激情久久一区 | 久久精品电影 | 国产成人无码av | 人妻视频一区 | 奇米中文字幕 | 精品国产精品三级精品av网址 | 久久久国产精华液 | 成人国产精品入口免费视频 |