如果想在 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 的方法更好。