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{constructor constructor=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{constructor constructor=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{constructor constructor=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字符串编码解码性能怎么提升”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注恰卡编程网行业资讯频道。