优化字符串拼接之二:非托管内存应用

王朝学院·作者佚名  2016-08-27
窄屏简体版  字體: 小  |  中  |  大  |  超大  

前(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,367ms2,708,098,82035800======= 一千次 实体集合 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )1,256ms2,479,181,11712900//======= 优化前 ======================================================================================= 一万次 单个实体 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )1,089ms2,170,463,77835000======= 一千次 实体集合 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )

802ms1,565,483,21814000

其中 单个实体 是 一个拥有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 =newchar[100];fixed(char* p =buffer)

{

p[10] ='a';//....}

这样确实可以固定这个对象,保证不因为垃圾回收而改变内存位置,但是这是一个方法级的语块;这就意味着你无法固定一个类的字段,想想我们有那么多的Append方法,不可能每个方法都固定一次(fixed也是有性能损失的)

另外一个方法就是申请非托管内存,所谓非托管,也就是说垃圾回收将不处理这块内存, 所以这也意味着,你可以不用担心GC来捣乱,但是需要自己去释放它,不过是小问题;

非托管内存申请非托管内存很简单,参考MSDN

//生成字符串缓冲区指针 ,一个char是2个字节,所以要乘以2IntPtr_currIntPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size *2);char* _current = (char*)_currIntPtr.ToPointer();

当然用完要记得释放,这个就实现IDisposable

还有一点需要注意的就是 这个方法是不能重复执行的,会报错,所以需要判断一下

PRivateint_disposeMark =0;publicvoidDispose()

{varmark = System.Threading.Interlocked.Exchange(ref_disposeMark,1);if(mark ==1)

{return;

}

System.Runtime.InteropServices.Marshal.FreeHGlobal(_currIntPtr);GC.SuppressFinalize(this);

}

代码优化把以上2点结合起来,就有了以下代码

///<summary>指针句柄///</summary>privatereadonlyIntPtr _currIntPtr;///<summary>一级缓冲指针///</summary>private char*_current;///<summary>二级缓冲///</summary>readonlystring[] _buffer =newstring[8];///<summary>备用二级缓冲索引///</summary>int_bufferIndex;///<summary>总字符数///</summary>int_length;///<summary>结束位,一级缓冲长度减一///</summary>int_endPosition;///<summary>一级缓冲当前位置///</summary>int_position;

///<summary>初始化对象,并指定缓冲区大小///</summary>///<param name="size"></param>publicQuickStringWriter(ushortsize)

{//确定最后一个字符的位置 长度-1_endPosition = size -1;//生成字符串缓冲指针 ,一个char是2个字节,所以要乘以2_currIntPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size *2);

_current= (char*)_currIntPtr.ToPointer();

}

构造函数

///<summary>获取当前实例中的字符串总长度///</summary>publicintLength

{get{return_length +_position;

}

}

Length

///<summary>尝试在一级缓冲区写入一个字符///<para>如果一级缓冲区已满,将会自动调用Flush方法转移一级缓冲区中的内容</para>///</summary>privatevoidTryWrite()

{if(_position >_endPosition)

{

Flush();

}elseif(_endPosition ==int.MaxValue)

{thrownewException("指针尚未准备就绪!");

}

}///<summary>尝试在一级缓冲区写入指定数量的字符///</summary>///<para>如果尝试写入的字符数大于一级缓冲区的大小,返回false</para>///<para>如果尝试写入的字符数超出一级缓冲区剩余容量,自动调用Flush方法</para>///<param name="count">尝试写入的字符数</param>///<returns></returns>privateboolTryWrite(intcount)

{if(count >=_endPosition)

{returnfalse;

}varpre = _position +count;if(pre >=_endPosition)

{

Flush();

}elseif(_endPosition ==int.MaxValue)

{thrownewException("指针尚未准备就绪!");

}returntrue;

}

TryWrite

///<summary>清理当前实例的一级缓冲区的内容,使所有缓冲数据写入二级缓冲区。///</summary>publicvoidFlush()

{if(_position >0)

{

_length+=_position;if(_bufferIndex ==8)

{

_buffer[0] =string.Concat(_buffer);

_buffer[1] =newstring(_current,0, _position);

_buffer[2] =_buffer[3] =_buffer[4] =_buffer[5] =_buffer[6] =_buffer[7] =null;

_bufferIndex=2;

}else{

_buffer[_bufferIndex++] =newstring(_current,0, _position);

}

_position=0;

}

}

Flush

///<summary>返回当前实例中的字符串///</summary>publicoverridestringToString()

{if(_bufferIndex ==0)

{returnnewstring(_current,0, _position);

}if(_bufferIndex <=3)

{returnstring.Concat(_buffer[0], _buffer[1], _buffer[2],newstring(_current,0, _position));

}returnstring.Concat(_buffer[0], _buffer[1], _buffer[2], _buffer[3],

_buffer[4], _buffer[5], _buffer[6], _buffer[7],newstring(_current,0, _position));

}

ToString

其他一些优化privatestaticcharHexToChar(inta)

{

a&=15;returna >9? (char)(a -10+0x61) : (char)(a +0x30);

}///<summary>将 Guid 对象转换为字符串追加到当前实例。///</summary>publicQuickStringWriter Append(Guid val,charformat ='d')

{intflag;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()));returnthis;

}varbs =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;

}returnthis;

}

Append(Guid val)

///<summary>将 Int64 对象转换为字符串追加到当前实例。///</summary>publicQuickStringWriter Append(Int64 val)

{if(val ==0)

{

TryWrite();

_current[_position++] ='0';returnthis;

}varzero = (long)'0';varpos =19;varf = 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] ='-';

}varlength =20-pos;

Append(_number, pos, length);returnthis;

}

Append(Number val)

///<summary>将内存中的字符串追加到当前实例。///</summary>///<param name="point">内存指针</param>///<param name="offset">指针偏移量</param>///<param name="length">字符长度</param>///<returns></returns>publicQuickStringWriter Append(char* point,intoffset,intlength)

{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++] =newstring(point, offset, length);

_length+=length;

}

}returnthis;

}

Append(char* point, int offset, int length)

优化后的性能对比依然使用 ObjectToJsonString 来做对比

//======= StringBuilder ================================================================================ 一万次 单个实体 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )1,367ms2,708,098,82035800======= 一千次 实体集合 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )1,256ms2,479,181,11712900//======= 优化前 ======================================================================================= 一万次 单个实体 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )1,089ms2,170,463,77835000======= 一千次 实体集合 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )

802ms1,565,483,21814000//======= 优化后 ======================================================================================= 一万次 单个实体 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )

688ms1,353,917,1475200======= 一千次 实体集合 ===运行时间 CPU时钟周期 垃圾回收( 1代 2代 3代 )

663ms1,322,653,9327800

这样对我来说就很满意了,至少比StringBuilder快了50%!

代码托管平台https://code.csdn.net/snippets/436915

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航