如果想在 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: 0 TestGetByteCount 2 Time Elapsed: 41ms CPU Cycles: 90,435,216 Gen 0: 5 Gen 1: 0 Gen 2: 0 TestGetByteCount 5 Time Elapsed: 35ms CPU Cycles: 77,586,978 Gen 0: 2 Gen 1: 0 Gen 2: 0 TestGetByteCount 10 Time Elapsed: 32ms CPU Cycles: 71,327,412 Gen 0: 1 Gen 1: 0 Gen 2: 0 TestGetByteCount 100 Time Elapsed: 32ms CPU Cycles: 65,847,702 Gen 0: 0 Gen 1: 0 Gen 2: 0 TestGetByteCount 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: 0 TestGetByteCount 2 Time Elapsed: 38ms CPU Cycles: 83,395,672 Gen 0: 0 Gen 1: 0 Gen 2: 0 TestGetByteCount 5 Time Elapsed: 34ms CPU Cycles: 74,867,809 Gen 0: 0 Gen 1: 0 Gen 2: 0 TestGetByteCount 10 Time Elapsed: 31ms CPU Cycles: 70,190,804 Gen 0: 0 Gen 1: 0 Gen 2: 0 TestGetByteCount 100 Time Elapsed: 31ms CPU Cycles: 68,862,872 Gen 0: 0 Gen 1: 0 Gen 2: 0 TestGetByteCount 65536 Time Elapsed: 30ms CPU Cycles: 65,830,539 Gen 0: 0 Gen 1: 0 Gen 2: 0
可以很明顯的看到,內存問題完全解決了,而且速度也有略微提升。如果需要多次調用 GetByteCount,還是調用 Encoder 的方法更好。