目标:充分理解string与[]bytes零拷贝转换的实现
先回顾下string与[]byte的基本知识
1. string与[]byte的数据结构
reflect包中关于字符串的数据结构
Go
// StringHeader is the runtime representation of a string.
type StringHeader struct {
Data uintptr
Len int
}
Data指向的是某个数组的首地址
len代表数组的长度。
uintptr是一种特殊指针,下文会具体介绍
说明
- string是一个8位的byte的集合,通常代表utf-8文本(但不一定都是)
- string可以为empty但不能是nil
- string的值是不能改变的(因为底层是数组)
reflect包中关于[]bytes的数据结构
Go
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Data指向的就是byte数组
[]byte是一个指向byte类型数组的slice
可以看到stringStruct与slice区别是cap,说明[]bytes的值是可变的,因为底层是切片。而string的值是不能改变的(因为底层是数组)
2. 基本数据结构的空间大小以及内存对齐
Go
bl := true
fmt.Println("size of bool:", unsafe.Sizeof(bl))// 1
i := 10
fmt.Println("size of int:", unsafe.Sizeof(i)) // 8
i32 := int32(10)
fmt.Println("size of int32:", unsafe.Sizeof(i32)) // 4
i64 := int64(10)
fmt.Println("size of int64:", unsafe.Sizeof(i64)) // 8
str := "xxx"
fmt.Println("size of str:", unsafe.Sizeof(str)) // 16
type xstruct struct {
a bool
b int32
c string
}
xx := xstruct{true, 10, "hello"}
fmt.Println("size of xx.a:", unsafe.Sizeof(xx.a)) // 1
fmt.Println("size of xx.b:", unsafe.Sizeof(xx.b)) // 4
fmt.Println("size of xx.c:", unsafe.Sizeof(xx.c)) // 16
fmt.Println("size of xx:", unsafe.Sizeof(xx)) // 不是1+4+16=21,而是24,为什么呢?由于字节对齐
// xx.a为一个字节,而实际存储时会占用一个"对齐系数"也就是8字节(对于64位机器,"对齐系数"是8字节);
// xx.b为四个字节,所以1+4=5,放一个对齐系数(8字节),其余剩余部分用0补充
// xx.c为16个字节,刚好放满2个对齐系数(16字节)。所以8+16=24字节
从代码中总结常见类型变量占用空间:
bool占1个字节
int32占4个字节
int与int64 占4字节(64位机器)
string占16个字节,其中包含2部分,第一部分unsafe.pointer占8字节,第二部分len int占8字节
struct结构体占用的空间,计算时需要考虑字节对齐,字节对齐的好处是减少cpu访问memory的此次,cpu读取memory最小单位是一个字长(8字节),从而提供访问内存性能。(具体细节请问google)
3. unsafe.pointer与uintptr
string与[]byte结构的定义出现了uintptr,并且unsafe.pointer通常用于类型转换,下面具体介绍2中指针类型。
- unsafe.pointer与uintptr都是指针,但又不是普通指针,经查阅指针分为三种类型,分别有:
-
*类型: 这是最常用的指针,名叫普通指针类型,用于传递对象地址,不能进行指针运算。
-
unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。
-
uintptr:用于指针运算. 本质是存储 `指针地址` 的int类型
- unsafe.Pointer类型有四个重要描述:
(1)任何类型的指针都可以被转化为Pointer
(2)Pointer可以被转化为任何类型的指针
(3)uintptr可以被转化为Pointer
(4)Pointer可以被转化为uintptr
简而言之,unsafe.Pointer可以实现指针类型的转换,uintptr用于指针计算
下面看看Pointer的内部结构:
Go
type Pointer *ArbitraryType
// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int
ArbitraryType是int的一个别名,在Go中对ArbitraryType赋予特殊的意义。代表一个任意Go表达式类型。
- unsafe.Pointer的使用示例:
Go
value1 := int32(10)
value2 := int64(12)
p := &value1
fmt.Println(reflect.TypeOf(p)) // *int32
fmt.Println(*p) // 10
//p = &value2 // 错误。 无法将p指向&value2地址,因为p是*int32类型,&value2是*ini64类型
unsPtr := unsafe.Pointer(&value2) // 将*int64先转换为unsafe.Pointer类型指针,此时unsPtr指向&value2(也就是value2的地址)
p = (*int32)(unsPtr) //转换为*int32指针类型
fmt.Println(reflect.TypeOf(p)) // *int32
fmt.Println(*p) // 11
- uintptr的使用示例
Go
type student struct {
name string
age uint8
}
s := student{
name: "tom", // string类型,16字节
age: 18, // int8类型,1字节
}
uptr := (uintptr)(unsafe.Pointer(&s)) // uptr指向结构体的首地址
uptr = uptr + 16 // uptr移动16字节,指向age的地址
age := *(*int8)(unsafe.Pointer(uptr)) // 转换为*int8类型的指针
fmt.Printf("age=%d\n", age) // 18
- 对uinitptr与unsafe.Pointer有简单了解后,再看结构体内存对齐示例
Go
type student struct {
name string
age uint8
city string
}
s := student{
name: "tom", // string类型,16字节
age: 18, // int8类型,1字节; 需要做内存对齐,独占一个字长,本身占一个字节,其余7个字节填充
city: "shenzhen", // string类型,16字节
}
结构体的内存空间占用情况:
代码验证结构体占用空间的总大小,以及每个成员占用空间大小:
Go
// 结构体占用空间
fmt.Println("size of s:", unsafe.Sizeof(s)) // 40
// 计算变量的地址
fmt.Printf("address of s.name: %p\n", &s.name) // 0xc00008c030--->转换为十进制824634294320
fmt.Printf("address of s.age: %p\n", &s.age) // 0xc00008c040--->转换为十进制824634294336
fmt.Printf("address of s.city: %p\n", &s.city) // 0xc00008c048--->转换为十进制824634294344
// 打印内部变量的相对字符串首地址的偏移量
fmt.Printf("offset of s.age:%d\n", unsafe.Offsetof(s.name)) // 0
fmt.Printf("offset of s.age:%d\n", unsafe.Offsetof(s.age)) // 16
fmt.Printf("offset of s.age:%d\n", unsafe.Offsetof(s.city)) // 24,计算方法是24=16+8
下面我们把s字符串再进一步"打开",探索一下字符串内部
先将s字符串转换为*reflect.StringHeader, 并查看字符串内部Data,Len的值
Go
x := (*reflect.StringHeader)(unsafe.Pointer(&s)) // 转换为*reflect.StringHeader
fmt.Printf("x.Data: %v\n", x.Data) // 17603737,这就是Data变量保存的具体值,其实是一个内存地址
fmt.Println("type of x.Data:", reflect.TypeOf(x.Data)) // uintptr
fmt.Printf("&x.Data: %p\n", &x.Data) // 0xc000100030
fmt.Printf("x.Len: %v\n", x.Len) // 3, 字符串"tom"的长度
fmt.Printf("&x.Len: %p\n", &x.Len) // 0xc000100038, 说明38-30=8,表示x.Data占8个字节
- 使用uintptr,计算出x.city的地址,再获取改地址的值
Go
uptr := (uintptr)(unsafe.Pointer(&s)) // uptr指向结构体的首地址
uptr = uptr + 16 + 8 // uptr移动16+8字节,指向address的地址
city := *(*string)(unsafe.Pointer(uptr)) // 转换为*string类型的指针,再用*获取改地址的值,也就是x.city的值
fmt.Printf("city=%s\n", city) // shenzhen
4.string与[]bytes零拷贝的实现
最后,有了基础知识后,我们再看看string与[]bytes零拷贝的实现
string转换为[]byte
Go
// 内存零拷贝方式类型转换
func stringtobyte(s string) []byte {
// &s转换为*reflect.StringHeader
var sptr *reflect.StringHeader
sptr = (*reflect.StringHeader)(unsafe.Pointer(&s))
var b []byte
// &b转换为*reflect.SliceHeader
bptr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
// 填充*reflect.SliceHeader内的Data,Len,Cap
bptr.Data = sptr.Data
bptr.Len = sptr.Len
bptr.Cap = sptr.Len
return b
}
为了编译理解,将转换过去用图示表示
[]byte转换为string
Go
// 内存零拷贝方式类型转换
func bytetostring(b []byte) string {
var bptr *reflect.SliceHeader
bptr = (*reflect.SliceHeader)(unsafe.Pointer(&b))
var s string
sptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
sptr.Data = bptr.Data
sptr.Len = bptr.Len
return s
}
// 转换方法二
Go
// 转换方法二
func String2Bytes(s string) []byte {
sh := (*[2]uintptr)(unsafe.Pointer(&s))
bh := [3]uintptr{sh[0], sh[1], sh[1]}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func String2Bytes2(s string) []byte {
// &s转换为unsafe.Pointer类型的指针,再转换为指向定长为2的uintptr数组的指针
sh := (*[3]uintptr)(unsafe.Pointer(&s))
// sh是一个指针,不能直接转换为*[]byte的指针,先转换为unsafe.Pointer,再转换为*[]byte指针;最后*取出指针指向的内容
return *(*[]byte)(unsafe.Pointer(sh))
}