内存中的字符串类型详细描述了字符串在内存中的结构及其类型信息。
本文主要研究字符串的各种操作(语法糖),在内存中实际的样子。
环境
- 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函数中。
出乎意料的是,!=与==编译后的几乎一致,只是两处指令对结果进行了相反的操作:
字符串连接(相加)
在本文的代码清单中,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」