优化字符串拼接之二:非托管内存应用
前(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%!
代码托管平台
我发布的代码,没有任何版权,遵守WTFPL协议(如有引用,请遵守被引用代码的协议)
qq群:5946699 希望各位喜爱C#的朋友可以在这里交流学习,分享编程的心得和快乐