当前位置: 首页 > 科技观察

StringOperationsinGoMemory

时间:2023-03-12 01:00:26 科技观察

StringTypesinMemory详细介绍了字符串在内存中的结构和类型信息。这篇文章主要研究字符串的各种操作(语法糖)以及它们在内存中实际是什么样子的。环境操作系统:Ubuntu20.04.2LTS;x86_64Go:goversiongo1.16.2linux/amd64声明了不同的操作系统、处理器架构和Go版本,可能会导致相同源码编译运行时寄存器值、内存地址、数据结构等存在差异。本文仅保证当前环境下学习过程中分析数据的准确性和有效性。操作类型比较相等比较不等比较连接(加法)和[]byte转换和[]byte复制代码列表packagemainimport("fmt")funcmain(){vararray[20]bytevars="copyhelloworld"string2slice(s)copyString(array[:],s)slice2string(array[:])compare()concat()}//go:noinlinefunccopyString(slice[]byte,sstring){copy(slice,s)PrintSlice(slice)}//go:noinlinefuncstring2slice(sstring){PrintSlice([]byte(s))}//go:noinlinefuncslice2string(slice[]byte){PrintString(string(slice))}//go:noinlinefunccompare(){varh="hello"varw="world!"PrintBool(h>w)PrintBool(h=w)PrintBool(h<=w)PrintBool(h!=w)//PrintBool(true)PrintBool(h==w)//PrintBool(false)PrintBool(testEqual(h,w))PrintBool(testNotEqual(h,w))}//go:noinlinefunctestEqual(h,wstring)bool{returnh==w}//go:noinlinefunctestNotEqual(h,wstring))bool{returnh!=w}//go:noinlinefuncconcat(){hello:="hello"world:="world"jack:="Jack"rose:="Rose"lucy:="Lucy"lily:="百合“前:=”!lucy,lily))PrintString(concat6(hello,jack,rose,lucy,lily,ex))}//go:noinlinefuncconcat2(a,bstring)string{returna+b}//go:noinlinefuncconcat3(a,b,cstring)string{returna+b+c}//go:noinlinefuncconcat4(a,b,c,dstring)string{returna+b+c+d}//go:noinlinefuncconcat5(a,b,c,d,estring)字符串{returna+b+c+d+e}//go:noinlinefuncconcat6(a,b,c,d,e,fstring)string{returna+b+c+d+e+f}//go:noinlinefuncPrintBool(vbool){fmt.Println("v=",v)}//go:noinlinefuncPrintString(vstring){fmt.Println("s=",v)}//go:noinlinefuncPrintSlice(s[]byte){fmt.Println("slice=",s)}添加go:noinline注解,避免内联,方便指令分析定义PrintBool/PrintSlice/PrintString函数,避免运行时编译器插入,string2slice函数代码非常简单,用于观察[]byte(s)的具体实现逻辑,com之后pilation,说明如下:可以清楚的看到,我们代码中的[]byte(s)被Go编译器替换成了runtime。stringtoslicebyte函数调用runtime/string.go源码文件中定义了runtime.stringtoslicebyte函数,Go编译器传递给该函数的buf参数值为nil。funcstringtoslicebyte(buf*tmpBuf,sstring)[]byte{varb[]byteifbuf!=nil&&len(s)<=len(buf){*buf=tmpBuf{}b=buf[:len(s)]}else{b=rawbyteslice(len(s))}copy(b,s)returnb}rawbyteslice函数的作用是申请一块内存来存放复制的数据。[]byte-to-string代码列表中的slice2string函数代码很简单。用于观察字符串(切片)的具体实现逻辑。编译器替换了runtime.slicebytetostring函数调用。runtime.slicebytetostring函数定义在runtime/string.go源码文件中,Go编译器传递给该函数的buf参数值为nil。将字符串复制到[]byte代码列表中copyString函数的代码非常简单。用来观察copy(slice,s)的具体实现逻辑。编译后指令如下:这个逻辑稍微复杂一点,将上面的指令翻译成Gopseudo代码如下:Len>s.Len{n=s.Len}ifslice.Data!=s.Data{runtime.memmove(slice.Data,s.Data,n)}PrintSlice(*(*[]byte)(unsafe.Pointer(&slice)))}可见,Go编译器在copy(slice,s)工作简单易用的语法糖背后做了很多工作。经过对比,上述伪代码与runtime/slice.go源码文件中的slicecopy函数非常相似,但并不完全相同。不等式比较代码清单中的比较函数测试两个字符串的各种比较。查看该函数的说明,发现Go编译器将以下4个比较操作全部转化为runtime.cmpstring函数调用:><>=<=runtime.cmpstring函数为编译器函数,不会直接调用。它声明在cmd/compile/internal/gc/builtin/runtime.go源码文件中,用汇编语言实现。GOARCH=amd64的实现位于源代码文件internal/bytealg/compare_amd64.s中。这个函数的返回值可能是:然后用cmp汇编指令将返回值和0进行比较,然后用下面的汇编指令保存最后的比较结果(true/false):在这个例子中,有两个特殊的compilestoasingleinstruction:h!=wiscompiledtomovb$0x1,(%rsp)h==wiscompiledtomovb$0x0,(%rsp)这是因为在这个例子中编译器知道即“hello”和“world”两个字符串不相等,所以编译时直接将比较结果编译成机器指令。因此,代码中定义了testEqual和testNotEqual函数,用于比较字符串变量。相等比较关于相等对比一下,StringTypesinMemory中已经做了很详细的分析和描述,本文代码清单中,testEqual函数指令如下,与runtime.strequal函数一致,因为编译r将runtime.strequal函数内联到testEqual函数中。没想到,!=和编译后的==几乎一样,只是两条指令对结果进行了相反的操作:字符串拼接(加法)在本文的代码清单中,concat函数用于观察字符串的拼接(+)运行,测试结果显示:添加了2个字符串,实际调用了runtime.concatstring2函数添加了3个字符串,实际调用了runtime.concatstring3函数添加了4个字符串,实际调用了runtime.concatstring4函数添加5个字符串,实际调用runtime.concatstring5函数添加5个以上的字符串,实际调用runtime.concatstrings函数以上函数调用是Go编译器的代码生成和插入工作。在插入runtime.concatstring*函数期间,编译器传递给这些函数的buf参数的值为nil。runtime.concatstring*函数的实现非常简单,这里不再赘述。小结从上面的详细分析可以看出,在开发过程中,所有对字符串的简单操作都会被Go编译器编码成复杂的指令和函数调用。很多开发者喜欢使用Go进行开发,因为Go语言非常简洁明了。是的,我们都喜欢这种甜美的语法糖。而且,发现语法糖背后的秘密也很有趣。本文转载自微信公众号「记忆中的Golang」