優化字符串拼接之二:非托管內存應用


前(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;
            }
        }
Length
        /// <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;
        }
TryWrite
        /// <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;
            }
        }
Flush
        /// <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));
        }
ToString

其他一些優化

        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;
        }
Append(Guid val)
        /// <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;
        }
Append(Number val)
/// <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;
        }
Append(char* point, int offset, int length)

優化后的性能對比

  依然使用 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%!

代碼托管平台

https://code.csdn.net/snippets/436915


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM