前(tu)言(cao)
之前一篇雖然也強調了,可是回復中還是有人來挑刺,並且還有人支持?!
#5樓 2013-08-26 21:39樓主看下StringBuilder的makeroom方法吧。微軟官方的,有些東西不是人家做不到,而是人家考慮的更多。
我只是想我的字符串拼接快一點,至於其他功能,暫時我不需要!謝謝
對上一篇的簡述
上一篇中的主要思路就是,參照StringBuilder中Append的重載,自己寫一個新的對象,保證每一個Append方法都比StringBuilder更快就好了(實際上有部分是達不到這個要求的,但是能保證大部分快,一部分相同,1,2慢一些,整體上來說就能達到我的要求了)
並且在上一篇中有一個緩沖區的概念,是一個Char數組,當字符串超過緩沖區大小之后,將重新申請新的char數組,比原先的大一倍,並將原先的字符串賦值到新數組(這里成了新的一個瓶頸點)
上一篇(精簡版StringBuilder,提速字符串拼接)鏈接在此,沒看過的可以先移步去看一下
應用
很多人說,節省這么點時間有什么用,我覺得這個觀點本身就是錯誤的,古語有雲:不積跬步無以至千里,不積小流無以成江海;
好吧又有很多人會說在放屁了,下面說個實例;
在我的Json組件中,下面有一組ObjectToJsonString的時間測試,(這次我沒有拿千萬次,百萬次來測試)
//======= StringBuilder ========================================================================= ======= 一萬次 單個實體 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,367ms 2,708,098,820 358 0 0 ======= 一千次 實體集合 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,256ms 2,479,181,117 129 0 0 //======= 優化前 ================================================================================ ======= 一萬次 單個實體 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,089ms 2,170,463,778 350 0 0 ======= 一千次 實體集合 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 802ms 1,565,483,218 140 0 0
其中 單個實體 是 一個擁有15個左右屬性的實體 . 實體集合是 200個實體對象 每個實體 5個屬性,從上面可以看出一些性能的差異,至於到底值不值得,就仁者見仁了
所以這個類設計之后被我應用到了很多地方,幾乎所有的StringBuilder都可以用這個代替(我是說幾乎,不是全部)
優化擴容操作
剛才中有提到緩沖區在擴容的時候是一個性能瓶頸,其中有2個操作,1申請新的數組,2將原有數據拷貝到新數組中,這個拷貝的操作將是十分耗時的;所以我第一想到的就是不拷貝,僅申請一個新數組,不夠的話再申請一個,直到最后的ToString 再把所有char數組合並 String.Concat
最終定下一個方案,僅在初始化時開辟一個8長度的String數組buffer作為二級緩沖,當一級緩沖char數組滿了之后,把二級緩沖 string.Concat(buffer) 組成一個新的字符串,放入buffer[0],其他清空,這樣就可以規避一些拷貝數據帶來的性能損失
內存指針操作數組
字符串操作,其實就是在操作Char數組, 說到數組的操作很多人會想到指針,沒錯數組操作,使用指針定位會比索引取值要快很多,但是.NET中指針操作被成為是"不安全代碼",因為微軟告訴我們
指向可移動托管變量的指針的作用很小,因為垃圾回收可能不可預知地重定位變量。
這就意味着一旦發生垃圾回收,你的指針指向的對象就有可能已經不是原來的對象了
比如:當我正在操作char數組的時候 我的指針指向 char數組的第10位 char* p = (char*)char[]; p[10]但是由於垃圾回收,當我得到p的時候 char[]被重新安排在內存的另外一個地方了,但是p並沒有改變,那么p[10]所獲取或設置的值,就已經不是原來char數組的值了
當然微軟也有辦法解決,其中fixed是一種方式:
char[] buffer = new char[100]; fixed (char* p = buffer) { p[10] = 'a'; //.... }
這樣確實可以固定這個對象,保證不因為垃圾回收而改變內存位置,但是這是一個方法級的語塊;這就意味着你無法固定一個類的字段,想想我們有那么多的Append方法,不可能每個方法都固定一次(fixed也是有性能損失的)
另外一個方法就是申請非托管內存,所謂非托管,也就是說垃圾回收將不處理這塊內存, 所以這也意味着,你可以不用擔心GC來搗亂,但是需要自己去釋放它,不過是小問題;
非托管內存
申請非托管內存很簡單,參考MSDN
//生成字符串緩沖區指針 ,一個char是2個字節,所以要乘以2 IntPtr _currIntPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size * 2); char* _current = (char*)_currIntPtr.ToPointer();
當然用完要記得釋放,這個就實現IDisposable
還有一點需要注意的就是 這個方法是不能重復執行的,會報錯,所以需要判斷一下
private int _disposeMark = 0; public void Dispose() { var mark = System.Threading.Interlocked.Exchange(ref _disposeMark, 1); if (mark == 1) { return; } System.Runtime.InteropServices.Marshal.FreeHGlobal(_currIntPtr); GC.SuppressFinalize(this); }
代碼優化
把以上2點結合起來,就有了以下代碼
/// <summary> 指針句柄 /// </summary> private readonly IntPtr _currIntPtr; /// <summary> 一級緩沖指針 /// </summary> private char* _current; /// <summary> 二級緩沖 /// </summary> readonly string[] _buffer = new string[8]; /// <summary> 備用二級緩沖索引 /// </summary> int _bufferIndex; /// <summary> 總字符數 /// </summary> int _length; /// <summary> 結束位,一級緩沖長度減一 /// </summary> int _endPosition; /// <summary> 一級緩沖當前位置 /// </summary> int _position;

/// <summary> 初始化對象,並指定緩沖區大小 /// </summary> /// <param name="size"></param> public QuickStringWriter(ushort size) { //確定最后一個字符的位置 長度-1 _endPosition = size - 1; //生成字符串緩沖指針 ,一個char是2個字節,所以要乘以2 _currIntPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size * 2); _current = (char*)_currIntPtr.ToPointer(); }

/// <summary> 獲取當前實例中的字符串總長度 /// </summary> public int Length { get { return _length + _position; } }

/// <summary> 嘗試在一級緩沖區寫入一個字符 /// <para>如果一級緩沖區已滿,將會自動調用Flush方法轉移一級緩沖區中的內容</para> /// </summary> private void TryWrite() { if (_position > _endPosition) { Flush(); } else if (_endPosition == int.MaxValue) { throw new Exception("指針尚未准備就緒!"); } } /// <summary> 嘗試在一級緩沖區寫入指定數量的字符 /// </summary> /// <para>如果嘗試寫入的字符數大於一級緩沖區的大小,返回false</para> /// <para>如果嘗試寫入的字符數超出一級緩沖區剩余容量,自動調用Flush方法</para> /// <param name="count">嘗試寫入的字符數</param> /// <returns></returns> private bool TryWrite(int count) { if (count >= _endPosition) { return false; } var pre = _position + count; if (pre >= _endPosition) { Flush(); } else if (_endPosition == int.MaxValue) { throw new Exception("指針尚未准備就緒!"); } return true; }

/// <summary> 清理當前實例的一級緩沖區的內容,使所有緩沖數據寫入二級緩沖區。 /// </summary> public void Flush() { if (_position > 0) { _length += _position; if (_bufferIndex == 8) { _buffer[0] = string.Concat(_buffer); _buffer[1] = new string(_current, 0, _position); _buffer[2] = _buffer[3] = _buffer[4] = _buffer[5] = _buffer[6] = _buffer[7] = null; _bufferIndex = 2; } else { _buffer[_bufferIndex++] = new string(_current, 0, _position); } _position = 0; } }

/// <summary> 返回當前實例中的字符串 /// </summary> public override string ToString() { if (_bufferIndex == 0) { return new string(_current, 0, _position); } if (_bufferIndex <= 3) { return string.Concat(_buffer[0], _buffer[1], _buffer[2], new string(_current, 0, _position)); } return string.Concat(_buffer[0], _buffer[1], _buffer[2], _buffer[3], _buffer[4], _buffer[5], _buffer[6], _buffer[7], new string(_current, 0, _position)); }
其他一些優化

private static char HexToChar(int a) { a &= 15; return a > 9 ? (char)(a - 10 + 0x61) : (char)(a + 0x30); } /// <summary> 將 Guid 對象轉換為字符串追加到當前實例。 /// </summary> public QuickStringWriter Append(Guid val, char format = 'd') { int flag; switch (format) { case 'd': case 'D': flag = 1; TryWrite(36); break; case 'N': case 'n': flag = 0; TryWrite(32); break; case 'P': case 'p': TryWrite(38); _current[_position++] = '('; flag = ')'; break; case 'B': case 'b': TryWrite(38); _current[_position++] = '{'; flag = '}'; break; default: Append(val.ToString(format.ToString())); return this; } var bs = val.ToByteArray(); _current[_position++] = HexToChar(bs[3] >> 4); _current[_position++] = HexToChar(bs[3]); _current[_position++] = HexToChar(bs[2] >> 4); _current[_position++] = HexToChar(bs[2]); _current[_position++] = HexToChar(bs[1] >> 4); _current[_position++] = HexToChar(bs[1]); _current[_position++] = HexToChar(bs[0] >> 4); _current[_position++] = HexToChar(bs[0]); if (flag > 0) { _current[_position++] = '-'; } _current[_position++] = HexToChar(bs[5] >> 4); _current[_position++] = HexToChar(bs[5]); _current[_position++] = HexToChar(bs[4] >> 4); _current[_position++] = HexToChar(bs[4]); if (flag > 0) { _current[_position++] = '-'; } _current[_position++] = HexToChar(bs[7] >> 4); _current[_position++] = HexToChar(bs[7]); _current[_position++] = HexToChar(bs[6] >> 4); _current[_position++] = HexToChar(bs[6]); if (flag > 0) { _current[_position++] = '-'; } _current[_position++] = HexToChar(bs[8] >> 4); _current[_position++] = HexToChar(bs[8]); _current[_position++] = HexToChar(bs[9] >> 4); _current[_position++] = HexToChar(bs[9]); if (flag > 0) { _current[_position++] = '-'; } _current[_position++] = HexToChar(bs[10] >> 4); _current[_position++] = HexToChar(bs[10]); _current[_position++] = HexToChar(bs[11] >> 4); _current[_position++] = HexToChar(bs[11]); _current[_position++] = HexToChar(bs[12] >> 4); _current[_position++] = HexToChar(bs[12]); _current[_position++] = HexToChar(bs[13] >> 4); _current[_position++] = HexToChar(bs[13]); _current[_position++] = HexToChar(bs[14] >> 4); _current[_position++] = HexToChar(bs[14]); _current[_position++] = HexToChar(bs[15] >> 4); _current[_position++] = HexToChar(bs[15]); if (flag > 1) { _current[_position++] = (char)flag; } return this; }

/// <summary> 將 Int64 對象轉換為字符串追加到當前實例。 /// </summary> public QuickStringWriter Append(Int64 val) { if (val == 0) { TryWrite(); _current[_position++] = '0'; return this; } var zero = (long)'0'; var pos = 19; var f = val < 0; if (f) { _number[pos] = (char)(~(val % 10L) + (long)'1'); if (val < -10) { val = val / -10; _number[--pos] = (char)(val % 10L + zero); } } else { _number[pos] = (char)(val % 10L + zero); } while ((val = val / 10L) != 0L) { _number[--pos] = (char)(val % 10L + zero); } if (f) { _number[--pos] = '-'; } var length = 20 - pos; Append(_number, pos, length); return this; }

/// <summary> 將內存中的字符串追加到當前實例。 /// </summary> /// <param name="point">內存指針</param> /// <param name="offset">指針偏移量</param> /// <param name="length">字符長度</param> /// <returns></returns> public QuickStringWriter Append(char* point, int offset, int length) { if (length > 0) { if (TryWrite(length)) { char* c = point + offset; if ((length & 1) != 0) { _current[_position++] = c[0]; c++; length--; } int* p1 = (int*)&_current[_position]; int* p2 = ((int*)c); _position += length; while (length >= 8) { (*p1++) = *(p2++); (*p1++) = *(p2++); (*p1++) = *(p2++); (*p1++) = *(p2++); length -= 8; } if ((length & 4) != 0) { (*p1++) = *(p2++); (*p1++) = *(p2++); } if ((length & 2) != 0) { (*p1) = *(p2); } } else { Flush(); _buffer[_bufferIndex++] = new string(point, offset, length); _length += length; } } return this; }
優化后的性能對比
依然使用 ObjectToJsonString 來做對比
//======= StringBuilder ========================================================================= ======= 一萬次 單個實體 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,367ms 2,708,098,820 358 0 0 ======= 一千次 實體集合 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,256ms 2,479,181,117 129 0 0 //======= 優化前 ================================================================================ ======= 一萬次 單個實體 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 1,089ms 2,170,463,778 350 0 0 ======= 一千次 實體集合 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 802ms 1,565,483,218 140 0 0 //======= 優化后 ================================================================================ ======= 一萬次 單個實體 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 688ms 1,353,917,147 52 0 0 ======= 一千次 實體集合 === 運行時間 CPU時鍾周期 垃圾回收( 1代 2代 3代 ) 663ms 1,322,653,932 78 0 0
這樣對我來說就很滿意了,至少比StringBuilder快了50%!