淺談C# StringBuilder內(nèi)存碎片對(duì)性能的影響
StringBuilder內(nèi)部是由多段char[]組成的半自動(dòng)鏈表,因此頻繁從中間修改StringBuilder,會(huì)將原本連續(xù)的內(nèi)存分隔為多段,從而影響讀取/遍歷性能。
連續(xù)內(nèi)存與不連續(xù)內(nèi)存的性能差,可能高達(dá)1600倍。
背景
用StringBuilder的用戶可能大都想用StringBuilder拼接html/json模板、組裝動(dòng)態(tài)SQL等正常操作。但在一些特殊場(chǎng)景中——如為某種編程語(yǔ)言寫語(yǔ)言服務(wù),或者寫一個(gè)富文本編輯器時(shí),StringBuilder依然也有用武之地,通過里面的Insert/Remove兩個(gè)方法來(lái)修改。
測(cè)試方法
Talk is cheap, show me the code:
int docLength = 10000; void Main() { (from power in Enumerable.Range (1, 16) let mutations = (int) Math.Pow (2, power) select new { mutations, PerformanceRatio = Math.Round (GetPerformanceRatio (docLength, mutations), 1) }).Dump(); } float GetPerformanceRatio (int docLength, int mutations) { var sb = new StringBuilder ("".PadRight (docLength)); var before = GetPerformance (sb); FragmentStringBuilder (sb, mutations); var after = GetPerformance (sb); return (float) after.Ticks / before.Ticks; } void FragmentStringBuilder (StringBuilder sb, int mutations) { var r = new Random(42); for (int i = 0; i < mutations; i++) { sb.Insert (r.Next (sb.Length), 'x'); sb.Remove (r.Next (sb.Length), 1); } } TimeSpan GetPerformance (StringBuilder sb) { var sw = Stopwatch.StartNew(); long tot = 0; for (int i = 0; i < sb.Length; i++) { char c = sb[i]; tot += (int) c; } sw.Stop(); return sw.Elapsed; }
關(guān)于這段代碼,請(qǐng)注意以下幾點(diǎn):
- 通過.PadRight(n)來(lái)直接創(chuàng)建長(zhǎng)度為n的空白字符串,可以用new string(' ', n)來(lái)代替;
- new Random(42)處,我指定了一個(gè)隨機(jī)因子,確保每次分隔后分隔的位置完全相同,有利于做對(duì)照組;
- 我分別對(duì)字符串進(jìn)行了2^1 ~ 2^16次修改,分別比較經(jīng)過這么多次修改之后的性能差異;
- 我使用sb[i]來(lái)逐一訪問StringBuilder中的位置,使內(nèi)存不連續(xù)性更加突顯。
運(yùn)行結(jié)果
mutations | PerformanceRatio |
---|---|
2 | 1 |
4 | 1 |
8 | 1 |
16 | 1 |
32 | 1 |
64 | 1.1 |
128 | 1.2 |
256 | 1.8 |
512 | 5.2 |
1024 | 19.9 |
2048 | 81.3 |
4096 | 274.5 |
8192 | 745.8 |
16384 | 1578.8 |
32768 | 1630.4 |
65536 | 930.8 |
可見如果在StringBuilder中間進(jìn)行大量修改,其性能會(huì)急據(jù)下降,注意看32768次修改的情況下,遍歷時(shí)會(huì)產(chǎn)生高達(dá)1630.4倍的性能差!
解決方式
如果一定要用StringBuilder,可以考慮在修改一定次數(shù)后,重新創(chuàng)建一個(gè)新的StringBuilder,以使得訪問時(shí)獲得最佳的內(nèi)存連續(xù)性,即可解決此問題:
void FragmentStringBuilder (StringBuilder sb, int mutations) { var r = new Random(42); for (int i = 0; i < mutations; i++) { sb.Insert (r.Next (sb.Length), 'x'); sb.Remove (r.Next (sb.Length), 1); // 重點(diǎn) const int defragmentCount = 250; if (i % defragmentCount == defragmentCount - 1) { string buf = sb.ToString(); sb.Clear(); sb.Append(buf); } } }
如上,每經(jīng)過250次修改,即將原StringBuilder刪除,然后重新創(chuàng)建一個(gè)新的StringBuilder,此時(shí)運(yùn)行效果如下:
mutations | PerformanceRatio |
---|---|
2 | 1.2 |
4 | 0.7 |
8 | 1 |
16 | 1 |
32 | 1 |
64 | 1.1 |
128 | 1.2 |
256 | 1 |
512 | 1 |
1024 | 1 |
2048 | 1 |
4096 | 1.1 |
8192 | 1.5 |
16384 | 1.3 |
32768 | 1 |
65536 | 1 |
可見,在幾乎所有情況下,受內(nèi)存不連續(xù)造成的訪問性能問題,解決——同時(shí)250可能是一個(gè)相對(duì)比較合理的數(shù)字,在插入性能與查詢/遍歷性能中,獲得平衡。
反思與總結(jié)
眾所周知,由于string的不可變性,拼接大量字符串時(shí),會(huì)浪費(fèi)大量?jī)?nèi)存。但使用StringBuilder也需要了解它的結(jié)構(gòu)。
StringBuilder這樣做成鏈?zhǔn)降慕Y(jié)構(gòu)并非沒有原因,如果考慮插入性能,做成鏈?zhǔn)浇涌谑亲顑?yōu)秀的。但如果考慮查詢性能,鏈?zhǔn)浇Y(jié)構(gòu)就非常不利了,如果設(shè)計(jì)為非鏈?zhǔn)浇Y(jié)構(gòu),從中間插入時(shí),StringBuilder的內(nèi)存空間可能不夠,因此需要重新分配內(nèi)存,這樣相當(dāng)于將StringBuilder降格為string,因此完全喪失了StringBuilder適合做“頻繁插入”的優(yōu)勢(shì)。
本文說的其實(shí)是一個(gè)非常特殊的例子,現(xiàn)實(shí)中除了語(yǔ)言服務(wù)、編輯器外,很少會(huì)需要這種即要頻繁插入快,也要頻繁修改快的場(chǎng)景。如果想簡(jiǎn)單點(diǎn)搞,用StringBuilder會(huì)是一個(gè)有條件合適的解決方案。更適合的解決方案當(dāng)然是專門的數(shù)據(jù)結(jié)構(gòu)——PieceTable,微軟在VSCode編輯器中,為了確保大文件編輯性能,使用了該數(shù)據(jù)結(jié)構(gòu),取得了非常不錯(cuò)的成果,參考鏈接:Text Buffer Reimplementation。
到此這篇關(guān)于淺談StringBuilder內(nèi)存碎片對(duì)性能的影響的文章就介紹到這了,更多相關(guān)StringBuilder 內(nèi)存碎片內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# winform實(shí)現(xiàn)右下角彈出窗口結(jié)果的方法
這篇文章主要介紹了C# winform實(shí)現(xiàn)右下角彈出窗口結(jié)果的方法,結(jié)合實(shí)例形式分析了C#窗口操作的相關(guān)技巧,需要的朋友可以參考下2017-06-06Unity?制作一個(gè)分?jǐn)?shù)統(tǒng)計(jì)系統(tǒng)
項(xiàng)目中經(jīng)常遇到分?jǐn)?shù)統(tǒng)計(jì)的需求,例如操作正確則計(jì)分,相反則不計(jì)分失去該項(xiàng)分?jǐn)?shù),為了應(yīng)對(duì)需求需要一個(gè)分?jǐn)?shù)統(tǒng)計(jì)系統(tǒng)。本文主要介紹了通過Unity實(shí)現(xiàn)這樣的一個(gè)計(jì)分系統(tǒng),快來(lái)跟隨小編一起學(xué)習(xí)吧2021-12-12C#中使用FilleStream實(shí)現(xiàn)視頻文件的復(fù)制功能
這篇文章主要介紹了C#中使用FilleStream實(shí)現(xiàn)視頻文件的復(fù)制功能,本文通過實(shí)例代碼給大家介紹的非常想詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09WPF實(shí)現(xiàn)html中的table控件的示例代碼
相信很多做WPF開發(fā)的小伙伴都遇到過表格類的需求,雖然現(xiàn)有的Grid控件也能實(shí)現(xiàn),但是使用起來(lái)的體驗(yàn)感并不好,所以本文我們就來(lái)用WPF自己實(shí)現(xiàn)一個(gè)html中的table控件吧2024-03-03C#使用linq計(jì)算執(zhí)行元素在列表中出現(xiàn)次數(shù)的方法
這篇文章主要介紹了C#使用linq計(jì)算執(zhí)行元素在列表中出現(xiàn)次數(shù)的方法,涉及C#使用linq擴(kuò)展進(jìn)行列表查詢的技巧,需要的朋友可以參考下2015-04-04C#實(shí)現(xiàn)String字符串轉(zhuǎn)化為SQL語(yǔ)句中的In后接的參數(shù)詳解
在本篇文章中小編給大家分享的是一篇關(guān)于C#實(shí)現(xiàn)String字符串轉(zhuǎn)化為SQL語(yǔ)句中的In后接的實(shí)例內(nèi)容和代碼,需要的朋友們參考下。2020-01-01C#?Unity使用正則表達(dá)式去除部分富文本的代碼示例
正則表達(dá)式在我們?nèi)粘i_發(fā)中的用處不用多說了吧,下面這篇文章主要給大家介紹了關(guān)于C#?Unity使用正則表達(dá)式去除部分富文本的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03