数据库

Go内存中的字符串操作

时间:2010-12-5 17:23:32  作者:数据库   来源:IT科技类资讯  查看:  评论:0
内容摘要:内存中的字符串类型详细描述了字符串在内存中的结构及其类型信息。本文主要研究字符串的各种操作(语法糖),在内存中实际的样子。环境OS:Ubuntu20.04.2LTS;x86_64Go:goversio

内存中的内存字符串类型详细描述了字符串在内存中的结构及其类型信息。

本文主要研究字符串的中的字符作各种操作(语法糖),在内存中实际的串操样子。

环境

OS : Ubuntu 20.04.2 LTS; x86_64 Go : go version go1.16.2 linux/amd64 

声明

操作系统、内存处理器架构、中的字符作Go版本不同,串操均有可能造成相同的内存源码编译后运行时的寄存器值、内存地址、中的字符作数据结构不同。串操

本文仅保证学习过程中的内存分析数据在当前环境下的准确有效性。

操作类型

比较

相等性比较 不等性比较

连接(相加)

与[]byte的中的字符作转换

与[]byte的拷贝

代码清单

package main import (   "fmt" ) func main() {    var array [20]byte   var s = "copy hello world"   string2slice(s)   copyString(array[:], s)   slice2string(array[:])   compare()   concat() } //go:noinline func copyString(slice []byte, s string) {    copy(slice, s)   PrintSlice(slice) } //go:noinline func string2slice(s string) {    PrintSlice([]byte(s)) } //go:noinline func slice2string(slice []byte) {    PrintString(string(slice)) } //go:noinline func compare() {    var h = "hello"   var w = "world!"   PrintBool(h > w)   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:noinline func testEqual(h, w string) bool {    return h == w } //go:noinline func testNotEqual(h, w string) bool {    return h != w } //go:noinline func concat() {    hello := "hello "   world := "world"   jack := "Jack"   rose := " Rose "   lucy := "Lucy"   lily := " Lily "   ex := "!"   PrintString(concat2(hello, world))   PrintString(concat3(hello, jack, ex))   PrintString(concat4(hello, jack, rose, ex))   PrintString(concat5(hello, jack, rose, lucy, lily))   PrintString(concat6(hello, jack, rose, lucy, lily, ex)) } //go:noinline func concat2(a, b string) string {    return a + b } //go:noinline func concat3(a, b, c string) string {    return a + b + c } //go:noinline func concat4(a, b, c, d string) string {    return a + b + c + d } //go:noinline func concat5(a, b, c, d, e string) string {    return a + b + c + d + e } //go:noinline func concat6(a, b, c, d, e, f string) string {    return a + b + c + d + e + f } //go:noinline func PrintBool(v bool) {    fmt.Println("v =", v) } //go:noinline func PrintString(v string) {    fmt.Println("s =", v) } //go:noinline func PrintSlice(s []byte) {    fmt.Println("slice =", s) }  添加go:noinline注解避免内联,方便指令分析 定义PrintBool/PrintSlice/PrintString函数避免编译器插入runtime.convT*函数调用

深入内存

字符串转[]byte

代码清单中的串操string2slice函数代码非常简单,用于观察[]byte(s)具体实现逻辑,内存编译之后指令如下:

可以清晰地看到,中的字符作我们在代码中的串操[]byte(s),被Go编译器替换为runtime.stringtoslicebyte函数调用。

runtime.stringtoslicebyte函数定义在runtime/string.go源码文件中,云服务器提供商Go编译器传递给该函数的buf参数值为nil。

func stringtoslicebyte(buf *tmpBuf, s string) []byte {    var b []byte   if buf != nil && len(s) <= len(buf) {      *buf = tmpBuf{ }     b = buf[:len(s)]   } else {      b = rawbyteslice(len(s))   }   copy(b, s)   return b } 

rawbyteslice函数的功能是申请一块内存用于存储拷贝后的数据。

[]byte转字符串

代码清单中的slice2string函数代码非常简单,用于观察string(slice)具体实现逻辑,编译之后指令如下:

可以清晰地看到,我们在代码中的string(slice),被Go编译器替换为runtime.slicebytetostring函数调用。

runtime.slicebytetostring函数定义在runtime/string.go源码文件中,Go编译器传递给该函数的buf参数值为nil。

拷贝字符串到[]byte

代码清单中的copyString函数代码非常简单,用于观察copy(slice, s)具体实现逻辑,编译之后指令如下:

这个逻辑稍微复杂一点点,将以上指令再次翻译为Go伪代码如下:

func copyString(slice reflect.SliceHeader, s reflect.StringHeader) {      n := slice.Len     if slice.Len > s.Len {          n = s.Len     }     if slice.Data != s.Data {          runtime.memmove(slice.Data, s.Data, n)     }     PrintSlice(*(*[]byte)(unsafe.Pointer(&slice))) } 

可以看到,Go编译器在copy(slice, s)这个简单易用语法糖背后做了很多的工作。

经过比较,以上伪代码与runtime/slice.go源码文件中的slicecopy函数非常相似,但又不完全一致。

不等性比较

代码清单中的compare函数测试了两个字符串的各种比较操作。

查看该函数的指令,发现Go编译器将以下四种比较操作全部转换为runtime.cmpstring函数调用:

> < >= <=

runtime.cmpstring函数是亿华云一个编译器函数,不会被直接调用,声明在cmd/compile/internal/gc/builtin/runtime.go源码文件中,由汇编语言实现。

GOARCH=amd64的实现位于internal/bytealg/compare_amd64.s源码文件中。

该函数返回值可能是:

然后使用cmp汇编指令将返回值与0进行比较,再使用以下汇编指令保存最终的比较结果(true / false):

在本例中,有两个特殊的比较,分别被编译为单条指令:

h != w 被编译为 movb $0x1,(%rsp) h == w 被编译为 movb $0x0,(%rsp)

这是因为在本例中编译器知道"hello"与"world"两个字符串不相等,所以直接在编译的时候直接把比较结果编译到机器指令中。

所以,在代码定义了testEqual和testNotEqual函数用于比较字符串变量。

相等性比较

关于相等性比较,在 内存中的字符串类型 中已经做了非常详细的分析和说明。

在本文的代码清单中,testEqual函数指令如下,与runtime.strequal函数一致,是因为编译器将runtime.strequal函数内联(inline)到了testEqual函数中。

出乎意料的是,!=与==编译后的几乎一致,云南idc服务商只是两处指令对结果进行了相反的操作:

字符串连接(相加)

在本文的代码清单中,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 In Memory」

copyright © 2025 powered by 益强资讯全景  滇ICP备2023006006号-31sitemap