Go语言模型:string的底层数据结构与高效操作详解
Golang的string类型底层数据结构简单,本质也是一个结构体实例,且是const不可变。
string的底层数据结构
通过下面一个例子来看:
packagemain import( "fmt" "unsafe" ) //from:string.go在GoLandIDE中双击shift快速找到 typestringStructstruct{ arrayunsafe.Pointer//指向一个[len]byte的数组 lengthint//长度 } funcmain(){ test:="hello" p:=(*str)(unsafe.Pointer(&test)) fmt.Println(&p,p)//0xc420070018&{0xa3f715} c:=make([]byte,p.length) fori:=0;istring,"hello" test2:=test+"world"//字符串是不可变类型,会生成一个新的string实例 p2:=(*str)(unsafe.Pointer(&test2)) fmt.Println(&p2,p2)//0xc420028030&{0xc42000a2e511} fmt.Println(test2)//hello,world }
string的拼接与修改
+操作
string类型是一个不可变类型,那么任何对string的修改都会新生成一个string的实例,如果是考虑效率的场景就要好好考虑一下如何修改了。先说一下最长用的+操作,同样上面的例子,看一下+操作拼接字符串的反汇编:
25 test2:=test+"world" 0x00000000004824d7<+1127>: lea0x105a2(%rip),%rax#0x492a80 0x00000000004824de<+1134>: mov%rax,(%rsp) 0x00000000004824e2<+1138>: callq0x40dda0#调用newobject函数 0x00000000004824e7<+1143>: mov0x8(%rsp),%rax 0x00000000004824ec<+1148>: mov%rax,0xa0(%rsp) 0x00000000004824f4<+1156>: mov0xa8(%rsp),%rax 0x00000000004824fc<+1164>: mov0x8(%rax),%rcx 0x0000000000482500<+1168>: mov(%rax),%rax 0x0000000000482503<+1171>: mov%rax,0x8(%rsp) 0x0000000000482508<+1176>: mov%rcx,0x10(%rsp) 0x000000000048250d<+1181>: movq$0x0,(%rsp) 0x0000000000482515<+1189>: lea0x30060(%rip),%rax#0x4b257c 0x000000000048251c<+1196>: mov%rax,0x18(%rsp) 0x0000000000482521<+1201>: movq$0x6,0x20(%rsp) 0x000000000048252a<+1210>: callq0x43cc00 #调用concatstring2函数
因为当前go[2018.11version:go1.11]的不是遵循默认的x86callingconvention用寄存器传参,而是通过stack进行传参,所以go的反汇编不像c的那么容易理解,不过大概看懂+背后的操作还是没问题的,看一下runtime源码的拼接函数:
funcconcatstring2(buf*tmpBuf,a[2]string)string{ returnconcatstrings(buf,a[:]) } //concatstringsimplementsaGostringconcatenationx+y+z+... //Theoperandsarepassedintheslicea. //Ifbuf!=nil,thecompilerhasdeterminedthattheresultdoesnot //escapethecallingfunction,sothestringdatacanbestoredinbuf //ifsmallenough. funcconcatstrings(buf*tmpBuf,a[]string)string{ idx:=0 l:=0 count:=0 fori,x:=rangea{ n:=len(x) ifn==0{ continue } ifl+n分析runtime的concatstrings实现,可以看出+最后新申请buf,拷贝原来的string到buf,最后返回新实例。那么每次的+操作,都会涉及新申请buf,然后是对应的copy。如果反复使用+,就不可避免有大量的申请内存操作,对于大量的拼接,性能就会受到影响了。
bytes.Buffer
通过看源码,bytes.Buffer增长buffer时是按照2倍来增长内存,可以有效避免频繁的申请内存,通过一个例子来看:
funcmain(){ varbufbytes.Buffer fori:=0;i<10;i++{ buf.WriteString("hi") } fmt.Println(buf.String()) }对应的byte包库函数源码
//@file:buffer.go func(b*Buffer)WriteString(sstring)(nint,errerror){ b.lastRead=opInvalid m,ok:=b.tryGrowByReslice(len(s)) if!ok{ m=b.grow(len(s))//高效的增长策略->letcapacitygettwiceaslarge } returncopy(b.buf[m:],s),nil } //@file:buffer.go //letcapacitygettwiceaslarge!!! func(b*Buffer)grow(nint)int{ m:=b.Len() //Ifbufferisempty,resettorecoverspace. ifm==0&&b.off!=0{ b.Reset() } //Trytogrowbymeansofareslice. ifi,ok:=b.tryGrowByReslice(n);ok{ returni } //Checkifwecanmakeuseofbootstraparray. ifb.buf==nil&&n<=len(b.bootstrap){ b.buf=b.bootstrap[:n] return0 } c:=cap(b.buf) ifn<=c/2-m{ //Wecanslidethingsdowninsteadofallocatinganew //slice.Weonlyneedm+n<=ctoslide,but //weinsteadletcapacitygettwiceaslargesowe //don'tspendallourtimecopying. copy(b.buf,b.buf[b.off:]) }elseifc>maxInt-c-n{ panic(ErrTooLarge) }else{ //Notenoughspaceanywhere,weneedtoallocate. buf:=makeSlice(2*c+n) copy(buf,b.buf[b.off:]) b.buf=buf } //Restoreb.offandlen(b.buf). b.off=0 b.buf=b.buf[:m+n] returnm }string.join
这个函数可以一次申请最终string的大小,但是使用得预先准备好所有string,这种场景也是高效的,一个例子:
funcmain(){ varstrs[]string fori:=0;i<10;i++{ strs=append(strs,"hi") } fmt.Println(strings.Join(strs,"")) }对应库的源码:
//Joinconcatenatestheelementsofatocreateasinglestring.Theseparatorstring //sepisplacedbetweenelementsintheresultingstring. funcJoin(a[]string,sepstring)string{ switchlen(a){ case0: return"" case1: returna[0] case2: //Specialcaseforcommonsmallvalues. //Removeifgolang.org/issue/6714isfixed returna[0]+sep+a[1] case3: //Specialcaseforcommonsmallvalues. //Removeifgolang.org/issue/6714isfixed returna[0]+sep+a[1]+sep+a[2] } //计算好最终的string的大小 n:=len(sep)*(len(a)-1)// fori:=0;istrings.Builder(go1.10+)
看到这个名字,就想到了Java的库,哈哈,这个Builder用起来是最方便的,不过是在1.10后引入的。其高效也是体现在2倍速的内存增长,WriteString函数利用了slice类型对应append函数的2倍速增长。
一个例子:
funcmain(){ varsstrings.Builder fori:=0;i<10;i++{ s.WriteString("hi") } fmt.Println(s.String()) }对应库的源码
@file:builder.go //WriteStringappendsthecontentsofstob'sbuffer. //Itreturnsthelengthofsandanilerror. func(b*Builder)WriteString(sstring)(int,error){ b.copyCheck() b.buf=append(b.buf,s...) returnlen(s),nil }总结
Golang的字符串处理还是挺方便的,有垃圾回收和一些内置的语言级写法支持,让复杂字符串操作没有那么繁琐了,比起C/C++高效了不少。
补充:gostring的内部实现
gostring内部实现
这个string的探索
来来个例子
funcboo(aint,bint)(int,string){ returna+b,"abcd" }81079000000000044dfa0: 8108044dfa0:>------48c74424180000>--movq$0x0,0x18(%rsp) 8108144dfa7:>------0000- 8108244dfa9:>------0f57c0>--xorps%xmm0,%xmm0 8108344dfac:>------0f11442420>--movups%xmm0,0x20(%rsp) 8108444dfb1:>------488b442408>--mov0x8(%rsp),%rax 8108544dfb6:>------4803442410>--add0x10(%rsp),%rax 8108644dfbb:>------4889442418>--mov%rax,0x18(%rsp) 8108744dfc0:>------488d05d4eb0100>--lea0x1ebd4(%rip),%rax#46cb9b 8108844dfc7:>------4889442420>--mov%rax,0x20(%rsp) 8108944dfcc:>------48c74424280400>--movq$0x4,0x28(%rsp) 8109044dfd3:>------0000- 8109144dfd5:>------c3>--retq--- 其中
8108744dfc0:>------488d05d4eb0100>--lea0x1ebd4(%rip),%rax#46cb9b8108844dfc7:>------4889442420>--mov%rax,0x20(%rsp) 8108944dfcc:>------48c74424280400>--movq$0x4,0x28(%rsp) 8109044dfd3:>------0000- 8109144dfd5:>------c3>--retq--- lea0x1ebd4(%rip),%rax得到char*,mov%rax,0x20(%rsp)复制给返回值,movq$0x4,0x28(%rsp)把长度也填进去, 其实可以看到string就是c里面的char*和len的组合
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。