java字符串编码解码性能怎么提升-mile米乐体育

java字符串编码解码性能怎么提升

这篇“java字符串编码解码性能怎么提升”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“java字符串编码解码性能怎么提升”文章吧。

    1.常见字符串编码

    常见的字符串编码有:

    latin1 只能保存ascii字符,又称iso-8859-1。

    utf-8 变长字节编码,一个字符需要使用1个、2个或者3个byte表示。由于中文通常需要3个字节表示,中文场景utf-8编码通常需要更多的空间,替代的方案是gbk/gb2312/gb18030。

    utf-16 2个字节,一个字符需要使用2个byte表示,又称ucs-2 (2-byte universal character set)。根据大小端的区分,utf-16有两种形式,utf-16be和utf-16le,缺省utf-16指utf-16be。java语言中的char是utf-16le编码。

    gb18030 变长字节编码,一个字符需要使用1个、2个或者3个byte表示。类似utf8,中文只需要2个字符,表示中文更省字节大小,缺点是在国际上不通用。

    为了计算方便,内存中字符串通常使用等宽字符,java语言中char和.net中的char都是使用utf-16。早期windows-nt只支持utf-16。

    2.编码转换性能

    utf-16和utf-8之间转换比较复杂,通常性能较差。

    如下是一个将utf-16转换为utf-8编码的实现,可以看出算法比较复杂,所以性能较差,这个操作也无法使用vector api做优化。

    staticintencodeutf8(char[]utf16,intoff,intlen,byte[]dest,intdp){intsl=off len,last_offset=sl-1;while(off>6));dest[dp  ]=(byte)(0x80|(c&0x3f));}elseif(c>='\ud800'&&c<'\ue000'){intuc;if(c<'\udc00'){if(off>last_offset){dest[dp  ]=(byte)'?';returndp;}chard=utf16[off];if(d>='\udc00'&&d<'\ue000'){uc=(c<<10) d 0xfca02400;}else{thrownewruntimeexception("encodeutf8error",newmalformedinputexception(1));}}else{uc=c;}dest[dp  ]=(byte)(0xf0|((uc>>18)));dest[dp  ]=(byte)(0x80|((uc>>12)&0x3f));dest[dp  ]=(byte)(0x80|((uc>>6)&0x3f));dest[dp  ]=(byte)(0x80|(uc&0x3f));off  ;//2utf16}else{//3dest,16bitsdest[dp  ]=(byte)(0xe0|((c>>12)));dest[dp  ]=(byte)(0x80|((c>>6)&0x3f));dest[dp  ]=(byte)(0x80|(c&0x3f));}}returndp;}

    由于java中char是utf-16le编码,如果需要将char[]转换为utf-16le编码的byte[]时,可以使用sun.misc.unsafe#copymemory方法快速拷贝。比如:

    staticintwriteutf16le(char[]chars,intoff,intlen,byte[]dest,finalintdp){unsafe.copymemory(chars,char_array_base_offset off*2,dest,byte_array_base_offset dp,len*2);dp =len*2;returndp;}

    3.java string的编码

    不同版本的jdk string的实现不一样,从而导致有不同的性能表现。char是utf-16编码,但string在jdk 9之后内部可以有latin1编码。

    3.1. jdk 6之前的string实现

    staticclassstring{finalchar[]value;finalintoffset;finalintcount;}

    在java 6之前,string.substring方法产生的string对象和原来string对象共用一个char[] value,这会导致substring方法返回的string的char[]被引用而无法被gc回收。于是使得很多库都会针对jdk 6及以下版本避免使用substring方法。

    3.2. jdk 7/8的string实现

    staticclassstring{finalchar[]value;}

    jdk 7之后,字符串去掉了offset和count字段,value.length就是原来的count。这避免了substring引用大char[]的问题,优化也更容易,从而jdk7/8中的string操作性能比java 6有较大提升。

    3.3. jdk 9/10/11的实现

    staticclassstring{finalbytecode;finalbyte[]value;staticfinalbytelatin1=0;staticfinalbyteutf16=1;}

    jdk 9之后,value类型从char[]变成byte[],增加了一个字段code,如果字符全部是ascii字符,使用value使用latin编码;如果存在任何一个非ascii字符,则用utf16编码。这种混合编码的方式,使得英文场景占更少的内存。缺点是导致java 9的string api性能可能不如jdk 8,特别是传入char[]构造字符串,会被做压缩为latin编码的byte[],有些场景会下降10%。

    4.快速构造字符串的方法

    为了实现字符串是不可变特性,构造字符串的时候,会有拷贝的过程,如果要提升构造字符串的开销,就要避免这样的拷贝。

    比如如下是jdk8的string的一个构造函数的实现

    publicfinalclassstring{publicstring(charvalue[]){this.value=arrays.copyof(value,value.length);}}

    在jdk8中,有一个构造函数是不做拷贝的,但这个方法不是public,需要用一个技巧实现methodhandles.lookup & lambdametafactory绑定反射来调用,文章后面有介绍这个技巧的代码。

    publicfinalclassstring{string(char[]value,booleanshare){//assertshare:"unsharednotsupported";this.value=value;}}

    快速构造字符的方法有三种:

    • 使用methodhandles.lookup & lambdametafactory绑定反射

    • 使用javalangaccess的相关方法

    • 使用unsafe直接构造

    这三种方法,1和2性能差不多,3比1和2略慢,但都比直接new字符串要快得多。jdk8使用jmh测试的数据如下:

    benchmark mode cnt score error units
    stringcreatebenchmark.invoke thrpt 5 784869.350 ± 1936.754 ops/ms
    stringcreatebenchmark.langaccess thrpt 5 784029.186 ± 2734.300 ops/ms
    stringcreatebenchmark.unsafe thrpt 5 761176.319 ± 11914.549 ops/ms
    stringcreatebenchmark.newstring thrpt 5 140883.533 ± 2217.773 ops/ms

    在jdk 9之后,对全部是ascii字符的场景,直接构造能达到更好的效果。

    4.1 基于methodhandles.lookup & lambdametafactory绑定反射的快速构造字符串的方法

    4.1.1 jdk8快速构造字符串

    publicstaticbifunctiongetstringcreatorjdk8()throwsthrowable{constructorconstructor=methodhandles.lookup.class.getdeclaredconstructor(class.class,int.class);constructor.setaccessible(true);methodhandleslookup=constructor.newinstance(string.class,-1//lookup.trusted);methodhandles.lookupcaller=lookup.in(string.class);methodhandlehandle=caller.findconstructor(string.class,methodtype.methodtype(void.class,char[].class,boolean.class));callsitecallsite=lambdametafactory.metafactory(caller,"apply",methodtype.methodtype(bifunction.class),handle.type().generic(),handle,handle.type());return(bifunction)callsite.gettarget().invokeexact();}

    4.1.2 jdk 11快速构造字符串的方法

    publicstatictointfunctiongetstringcode11()throwsthrowable{constructorconstructor=methodhandles.lookup.class.getdeclaredconstructor(class.class,int.class);constructor.setaccessible(true);methodhandles.lookuplookup=constructor.newinstance(string.class,-1//lookup.trusted);methodhandles.lookupcaller=lookup.in(string.class);methodhandlehandle=caller.findvirtual(string.class,"coder",methodtype.methodtype(byte.class));callsitecallsite=lambdametafactory.metafactory(caller,"applyasint",methodtype.methodtype(tointfunction.class),methodtype.methodtype(int.class,object.class),handle,handle.type());return(tointfunction)callsite.gettarget().invokeexact();}
    if(jdkutils.jvm_version==11){functionstringcreator=jdkutils.getstringcreatorjdk11();byte[]bytes=newbyte[]{'a','b','c'};stringapply=stringcreator.apply(bytes);assertequals("abc",apply);}

    4.1.3 jdk 17快速构造字符串的方法

    在jdk 17中,methodhandles.lookup使用reflection.registerfieldstofilter对lookupclass和allowedmodes做了保护,网上搜索到的通过修改allowedmodes的办法是不可用的。

    在jdk 17中,要通过配置jvm启动参数才能使用methodhandlers。如下:

    --add-opensjava.base/java.lang.invoke=all-unnamed
    publicstaticbifunctiongetstringcreatorjdk17()throwsthrowable{constructorconstructor=methodhandles.lookup.class.getdeclaredconstructor(class.class,class.class,int.class);constructor.setaccessible(true);methodhandles.lookuplookup=constructor.newinstance(string.class,null,-1//lookup.trusted);methodhandles.lookupcaller=lookup.in(string.class);methodhandlehandle=caller.findstatic(string.class,"newstringnorepl1",methodtype.methodtype(string.class,byte[].class,charset.class));callsitecallsite=lambdametafactory.metafactory(caller,"apply",methodtype.methodtype(bifunction.class),handle.type().generic(),handle,handle.type());return(bifunction)callsite.gettarget().invokeexact();}
    if(jdkutils.jvm_version==17){bifunctionstringcreator=jdkutils.getstringcreatorjdk17();byte[]bytes=newbyte[]{'a','b','c'};stringapply=stringcreator.apply(bytes,standardcharsets.us_ascii);assertequals("abc",apply);}

    4.2 基于javalangaccess快速构造

    通过sharedsecrets提供的javalangaccess,也可以不拷贝构造字符串,但是这个比较麻烦,jdk 8/11/17的api都不一样,对一套代码兼容不同的jdk版本不方便,不建议使用。

    javalangaccessjavalangaccess=sharedsecrets.getjavalangaccess();javalangaccess.newstringnorepl(b,standardcharsets.us_ascii);

    4.3 基于unsafe实现快速构造字符串

    publicstaticfinalunsafeunsafe;static{unsafeunsafe=null;try{fieldtheunsafefield=unsafe.class.getdeclaredfield("theunsafe");theunsafefield.setaccessible(true);unsafe=(unsafe)theunsafefield.get(null);}catch(throwableignored){}unsafe=unsafe;}////////////////////////////////////////////objectstr=unsafe.allocateinstance(string.class);unsafe.putobject(str,valueoffset,chars);

    注意:在jdk 9之后,实现是不同,比如:

    objectstr=unsafe.allocateinstance(string.class);unsafe.putbyte(str,coderoffset,(byte)0);unsafe.putobject(str,valueoffset,(byte[])bytes);

    4.4 快速构建字符串的技巧应用:

    如下的方法格式化日期为字符串,性能就会非常好。

    publicstringformatyyyymmdd(calendarcalendar)throwsthrowable{intyear=calendar.get(calendar.year);intmonth=calendar.get(calendar.month) 1;intdayofmonth=calendar.get(calendar.day_of_month);bytey0=(byte)(year/1000 '0');bytey1=(byte)((year/100) '0');bytey2=(byte)((year/10) '0');bytey3=(byte)(year '0');bytem0=(byte)(month/10 '0');bytem1=(byte)(month '0');byted0=(byte)(dayofmonth/10 '0');byted1=(byte)(dayofmonth '0');if(jdkutils.jvm_version>=9){byte[]bytes=newbyte[]{y0,y1,y2,y3,m0,m1,d0,d1};if(jdkutils.jvm_version==17){returnjdkutils.getstringcreatorjdk17().apply(bytes,standardcharsets.us_ascii);}if(jdkutils.jvm_version<=11){returnjdkutils.getstringcreatorjdk11().apply(bytes);}returnnewstring(bytes,standardcharsets.us_ascii);}char[]chars=newchar[]{(char)y0,(char)y1,(char)y2,(char)y3,(char)m0,(char)m1,(char)d0,(char)d1};if(jdkutils.jvm_version==8){returnjdkutils.getstringcreatorjdk8().apply(chars,true);}returnnewstring(chars);}

    5.快速遍历字符串的办法

    无论jdk什么版本,string.charat都是一个较大的开销,jit的优化效果并不好,无法消除参数index范围检测的开销,不如直接操作string里面的value数组。

    publicfinalclassstring{privatefinalcharvalue[];publiccharcharat(intindex){if((index<0)||(index>=value.length)){thrownewstringindexoutofboundsexception(index);}returnvalue[index];}}

    在jdk 9之后的版本,charat开销更大

    publicfinalclassstring{privatefinalbyte[]value;privatefinalbytecoder;publiccharcharat(intindex){if(islatin1()){returnstringlatin1.charat(value,index);}else{returnstringutf16.charat(value,index);}}}

    5.1 获取string.value的方法

    获取string.value的方法有如下:

    • 使用field反射

    • 使用unsafe

    unsafe和field反射在jdk 8 jmh的比较数据如下:

    benchmark mode cnt score error units
    stringgetvaluebenchmark.reflect thrpt 5 438374.685 ± 1032.028 ops/ms
    stringgetvaluebenchmark.unsafe thrpt 5 1302654.150 ± 59169.706 ops/ms

    5.1.1 使用反射获取string.value

    staticfieldvaluefield;static{try{valuefield=string.class.getdeclaredfield("value");valuefield.setaccessible(true);}catch(nosuchfieldexceptionignored){}}////////////////////////////////////////////char[]chars=(char[])valuefield.get(str);

    5.1.2 使用unsafe获取string.value

    staticlongvaluefieldoffset;static{try{fieldvaluefield=string.class.getdeclaredfield("value");valuefieldoffset=unsafe.objectfieldoffset(valuefield);}catch(nosuchfieldexceptionignored){}}////////////////////////////////////////////char[]chars=(char[])unsafe.getobject(str,valuefieldoffset);
    staticlongvaluefieldoffset;staticlongcoderfieldoffset;static{try{fieldvaluefield=string.class.getdeclaredfield("value");valuefieldoffset=unsafe.objectfieldoffset(valuefield);fieldcoderfield=string.class.getdeclaredfield("coder");coderfieldoffset=unsafe.objectfieldoffset(coderfield);}catch(nosuchfieldexceptionignored){}}////////////////////////////////////////////bytecoder=unsafe.getobject(str,coderfieldoffset);byte[]bytes=(byte[])unsafe.getobject(str,valuefieldoffset);

    6.更快的encodeutf8方法

    当能直接获取到string.value时,就可以直接对其做encodeutf8操作,会比string.getbytes(standardcharsets.utf_8)性能好很多。

    6.1 jdk8高性能encodeutf8的方法

    publicstaticintencodeutf8(char[]src,intoffset,intlen,byte[]dst,intdp){intsl=offset len;intdlascii=dp math.min(len,dst.length);//asciionlyoptimizedloopwhile(dp>6));dst[dp  ]=(byte)(0x80|(c&0x3f));}elseif(c>='\ud800'&&c<('\udfff' 1)){//character.issurrogate(c)but1.7finalintuc;intip=offset-1;if(c>='\ud800'&&c<('\udbff' 1)){//character.ishighsurrogate(c)if(sl-ip<2){uc=-1;}else{chard=src[ip 1];//d>='\udc00'&&d<('\udfff' 1)if(d>='\udc00'&&d<('\udfff' 1)){//character.islowsurrogate(d)uc=((c<<10) d) (0x010000-('\ud800'<<10)-'\udc00');//character.tocodepoint(c,d)}else{dst[dp  ]=(byte)'?';continue;}}}else{//if(c>='\udc00'&&c<('\udfff' 1)){//character.islowsurrogate(c)dst[dp  ]=(byte)'?';continue;}else{uc=c;}}if(uc<0){dst[dp  ]=(byte)'?';}else{dst[dp  ]=(byte)(0xf0|((uc>>18)));dst[dp  ]=(byte)(0x80|((uc>>12)&0x3f));dst[dp  ]=(byte)(0x80|((uc>>6)&0x3f));dst[dp  ]=(byte)(0x80|(uc&0x3f));offset  ;//2chars}}else{//3bytes,16bitsdst[dp  ]=(byte)(0xe0|((c>>12)));dst[dp  ]=(byte)(0x80|((c>>6)&0x3f));dst[dp  ]=(byte)(0x80|(c&0x3f));}}returndp;}

    使用encodeutf8方法举例

    char[]chars=unsafe.getobject(str,valuefieldoffset);//ensurecapacity(chars.length*3)byte[]bytes=...;//intbyteslength=ioutils.encodeutf8(chars,0,chars.length,bytes,bytesoffset);

    这样encodeutf8操作,不会有多余的arraycopy操作,性能会得到提升。

    6.1.1 性能测试比较

    测试代码

    publicclassencodeutf8benchmark{staticstringstr="01234567890abcdefghijklmnopqrstuvwzyzabcdefghijklmnopqrstuvwzyz一二三四五六七八九十";staticbyte[]out;staticlongvaluefieldoffset;static{out=newbyte[str.length()*3];try{fieldvaluefield=string.class.getdeclaredfield("value");valuefieldoffset=unsafeutils.unsafe.objectfieldoffset(valuefield);}catch(nosuchfieldexceptione){e.printstacktrace();}}@benchmarkpublicvoidunsafeencodeutf8()throwsexception{char[]chars=(char[])unsafeutils.unsafe.getobject(str,valuefieldoffset);intlen=ioutils.encodeutf8(chars,0,chars.length,out,0);}@benchmarkpublicvoidgetbytesutf8()throwsexception{byte[]bytes=str.getbytes(standardcharsets.utf_8);system.arraycopy(bytes,0,out,0,bytes.length);}publicstaticvoidmain(string[]args)throwsrunnerexception{optionsoptions=newoptionsbuilder().include(encodeutf8benchmark.class.getname()).mode(mode.throughput).timeunit(timeunit.milliseconds).forks(1).build();newrunner(options).run();}}

    测试结果

    encodeutf8benchmark.getbytesutf8 thrpt 5 20690.960 ± 5431.442 ops/ms
    encodeutf8benchmark.unsafeencodeutf8 thrpt 5 34508.606 ± 55.510 ops/ms

    从结果来看,通过unsafe 直接调用encodeutf8方法, 编码的所需要开销是newstringutf8的58%。

    6.2 jdk9/11/17高性能encodeutf8的方法

    publicstaticintencodeutf8(byte[]src,intoffset,intlen,byte[]dst,intdp){intsl=offset len;while(offset=0){dst[dp  ]=b0;}else{charc=(char)(((b0&0xff)<<0)|((b1&0xff)<<8));if(c<0x800){//2bytes,11bitsdst[dp  ]=(byte)(0xc0|(c>>6));dst[dp  ]=(byte)(0x80|(c&0x3f));}elseif(c>='\ud800'&&c<('\udfff' 1)){//character.issurrogate(c)but1.7finalintuc;intip=offset-1;if(c>='\ud800'&&c<('\udbff' 1)){//character.ishighsurrogate(c)if(sl-ip<2){uc=-1;}else{b0=src[ip 1];b1=src[ip 2];chard=(char)(((b0&0xff)<<0)|((b1&0xff)<<8));//d>='\udc00'&&d<('\udfff' 1)if(d>='\udc00'&&d<('\udfff' 1)){//character.islowsurrogate(d)uc=((c<<10) d) (0x010000-('\ud800'<<10)-'\udc00');//character.tocodepoint(c,d)}else{return-1;}}}else{//if(c>='\udc00'&&c<('\udfff' 1)){//character.islowsurrogate(c)return-1;}else{uc=c;}}if(uc<0){dst[dp  ]=(byte)'?';}else{dst[dp  ]=(byte)(0xf0|((uc>>18)));dst[dp  ]=(byte)(0x80|((uc>>12)&0x3f));dst[dp  ]=(byte)(0x80|((uc>>6)&0x3f));dst[dp  ]=(byte)(0x80|(uc&0x3f));offset  ;//2chars}}else{//3bytes,16bitsdst[dp  ]=(byte)(0xe0|((c>>12)));dst[dp  ]=(byte)(0x80|((c>>6)&0x3f));dst[dp  ]=(byte)(0x80|(c&0x3f));}}}returndp;}

    使用encodeutf8方法举例

    bytecoder=unsafe.getobject(str,coderfieldoffset);byte[]value=unsafe.getobject(str,coderfieldoffset);if(coder==0){//asciiarraycopy}else{//ensurecapacity(chars.length*3)byte[]bytes=...;//intbyteslength=ioutils.encodeutf8(value,0,value.length,bytes,bytesoffset);}

    这样encodeutf8操作,不会有多余的arraycopy操作,性能会得到提升。

    以上就是关于“java字符串编码解码性能怎么提升”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注恰卡编程网行业资讯频道。

    展开全文
    内容来源于互联网和用户投稿,文章中一旦含有米乐app官网登录的联系方式务必识别真假,本站仅做信息展示不承担任何相关责任,如有侵权或涉及法律问题请联系米乐app官网登录删除

    最新文章

    网站地图