C# 中容易忽視的 Encoding.GetByteCount 記憶體問題

來源:互聯網
上載者:User

如果想在 C# 中判斷字元是全形還是半形的,通常的辦法是使用 Encoding.Default.GetByteCount 方法,使用它的時候卻有很容易忽視的記憶體問題,具體表現為多次(數萬次,不同電腦可能不同)調用 GetByteCount 方法時,會導致記憶體記憶體回收,那麼意味著在這個過程中產生了大量的臨時對象。

下面這段測試代碼就是對總長為 6 萬的 char 數組計算它的位元組數,迴圈 10 次。其中測試一:一次取 1 個字元,每次迴圈調用 GetByteCount 60000 次;測試二:一次取 2 個字元,每次迴圈調用 30000 次;測試三:一次取 5 個字元,每次迴圈調用 12000 次;這樣一直到測試六:一次取 60000 個字元,每次迴圈調用 1 次。其中用到的 CodeTimer 類是一個來自老趙的效能計數器。

char[] charArr = new char[60000];for (int i = 0; i < 60000; i++){charArr[i] = (char)RandomExt.Next(char.MaxValue);}GC.Collect();CodeTimer.Time("TestGetByteCount 1", 10, () =>{for (int i = 0; i < 60000; i++){Encoding.Default.GetByteCount(charArr, i, 1);}});CodeTimer.Time("TestGetByteCount 2", 10, () =>{for (int i = 0; i < 60000 / 2; i++)Encoding.Default.GetByteCount(charArr, i * 2, 2);});CodeTimer.Time("TestGetByteCount 5", 10, () =>{for (int i = 0; i < 60000 / 5; i++)Encoding.Default.GetByteCount(charArr, i * 5, 5);});CodeTimer.Time("TestGetByteCount 10", 10, () =>{for (int i = 0; i < 60000 / 10; i++)Encoding.Default.GetByteCount(charArr, i * 10, 10);});CodeTimer.Time("TestGetByteCount 100", 10, () =>{for (int i = 0; i < 60000 / 100; i++)Encoding.Default.GetByteCount(charArr, i * 100, 100);});CodeTimer.Time("TestGetByteCount 65536", 10, () =>{Encoding.Default.GetByteCount(charArr, 0, 60000);});

不用看測試結果也知道,效率肯定是前面的低,後面的高。但重點不是這個,下面是測試結果,注意看 Gen 0 這一項(表示 0 代記憶體回收次數)。

TestGetByteCount 1  Time Elapsed:   52ms  CPU Cycles:     113,265,292  Gen 0:   8  Gen 1:   0  Gen 2:   0TestGetByteCount 2  Time Elapsed:   41ms  CPU Cycles:     90,435,216  Gen 0:   5  Gen 1:   0  Gen 2:   0TestGetByteCount 5  Time Elapsed:   35ms  CPU Cycles:     77,586,978  Gen 0:   2  Gen 1:   0  Gen 2:   0TestGetByteCount 10  Time Elapsed:   32ms  CPU Cycles:     71,327,412  Gen 0:   1  Gen 1:   0  Gen 2:   0TestGetByteCount 100  Time Elapsed:   32ms  CPU Cycles:     65,847,702  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 65536  Time Elapsed:   34ms  CPU Cycles:     72,340,460  Gen 0:   0  Gen 1:   0  Gen 2:   0

單獨把記憶體回收次數列出來,分別是 8,5,2,1,0,0,有沒有感覺很神奇?明明沒有建立任何臨時對象,卻導致了好幾次的記憶體回收。用 VS 內建的效能分析器分析看看,得到下面的圖:

圖 1 分配最多記憶體的函數

好吧,現在知道全都是 System.Text.EncodingNLS.GetByteCount(char[], int32, int32) 的錯了……但是這是系統內建的函數,還是要先嘗試從自身找問題,再看看分配視圖:

圖 2 分配視圖

看分配數遙遙領先的第一項:System.Text.InternalEncoderBestFitFallbackBuffer,好吧,原來就是 EncoderFallbackBuffer 的問題,它是提供一個允許回退處理常式在無法編碼輸入的字元時返回備用字串到編碼器的緩衝區。在調用 Encoding.GetByteCount 時,有可能會發生回退,因此編碼器內部會建立一個緩衝區以處理回退問題。又由於在每次調用時都會建立新的緩衝區,用完即扔,因此就會導致上面的現象——大量的臨時緩衝區被建立,又被回收,導致記憶體壓力增大。

這種問題並不明顯,需要有六七萬次以上的調才行(在我的電腦上),但是有問題就要想辦法去解決。

我這裡提供一個簡單的辦法,就是調用 Encoding.Default.GetEncoder(),擷取預設編碼的編碼器,然後調用這個編碼器的 GetByteCount 方法,就可以完美解決。這裡需要注意的是,Encoder 的 GetByteCount 方法比 Encoding 的方法多了一個參數 flush,表示時候要在計算後類比編碼器內部狀態的清除過程,需要注意。

更改後的代碼為:

char[] charArr = new char[60000];for (int i = 0; i < 60000; i++){charArr[i] = (char)RandomExt.Next(char.MaxValue);}Encoder encoder = Encoding.Default.GetEncoder();CodeTimer.Time("TestGetByteCount 1", 10, () =>{for (int i = 0; i < 60000; i++){encoder.GetByteCount(charArr, i, 1, true);}});CodeTimer.Time("TestGetByteCount 2", 10, () =>{for (int i = 0; i < 60000 / 2; i++)encoder.GetByteCount(charArr, i * 2, 2, true);});CodeTimer.Time("TestGetByteCount 5", 10, () =>{for (int i = 0; i < 60000 / 5; i++)encoder.GetByteCount(charArr, i * 5, 5, true);});CodeTimer.Time("TestGetByteCount 10", 10, () =>{for (int i = 0; i < 60000 / 10; i++)encoder.GetByteCount(charArr, i * 10, 10, true);});CodeTimer.Time("TestGetByteCount 100", 10, () =>{for (int i = 0; i < 60000 / 100; i++)encoder.GetByteCount(charArr, i * 100, 100, true);});CodeTimer.Time("TestGetByteCount 65536", 10, () =>{encoder.GetByteCount(charArr, 0, 60000, true);});

測試結果為:

TestGetByteCount 1  Time Elapsed:   45ms  CPU Cycles:     98,742,656  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 2  Time Elapsed:   38ms  CPU Cycles:     83,395,672  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 5  Time Elapsed:   34ms  CPU Cycles:     74,867,809  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 10  Time Elapsed:   31ms  CPU Cycles:     70,190,804  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 100  Time Elapsed:   31ms  CPU Cycles:     68,862,872  Gen 0:   0  Gen 1:   0  Gen 2:   0TestGetByteCount 65536  Time Elapsed:   30ms  CPU Cycles:     65,830,539  Gen 0:   0  Gen 1:   0  Gen 2:   0

可以很明顯的看到,記憶體問題完全解決了,而且速度也有略微提升。如果需要多次調用 GetByteCount,還是調用 Encoder 的方法更好。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.