关于内存管理:Go看源码必会知识之unsafe包

3次阅读

共计 9978 个字符,预计需要花费 25 分钟才能阅读完成。

原文链接:戳这里

前言

有看源码的敌人应该会发现,Go规范库中大量应用了 unsafe.pointer,要想更好的了解源码实现,就要晓得unsafe.pointer 到底是什么?所以明天就与大家来聊一聊 unsafe 包。

什么是unsafe

家喻户晓,Go 语言 被设计成一门强类型的动态语言,那么他的类型就不能扭转了,动态也是意味着类型查看在运行前就做了。所以在 Go 语言中是不容许两个指针类型进行转换的,应用过 C 语言的敌人应该晓得这在 C 语言中是能够实现的,Go中不容许这么应用是处于平安思考,毕竟强制转型会引起各种各样的麻烦,有时这些麻烦很容易被觉察,有时他们却又暗藏极深,难以觉察。大多数读者可能不明确为什么类型转换是不平安的,这里用 C 语言举一个简略的例子:

int main(){
  double pi = 3.1415926;
  double *pv = π
     void *temp = pd;
  int *p = temp;
}

在规范 C 语言 中,任何非 void 类型的指针都能够和 void 类型的指针互相指派,也能够通过 void 类型指针作为中介,实现不同类型的指针间接互相转换。下面示例中,指针 pv 指向的空间本是一个双精度数据,占 8 个字节,然而通过转换后,p指向的是一个 4 字节的 int 类型。这种产生内存截断的设计缺点会在转换后进行内存拜访是存在安全隐患。我想这就是 Go 语言被设计成强类型语言的起因之一吧。

尽管类型转换是不平安的,然而在一些非凡场景下,应用了它,能够突破 Go 的类型和内存平安机制,能够绕过类型零碎低效,进步运行效率。所以 Go 规范库中提供了一个 unsafe 包,之所以叫这个名字,就是不举荐大家应用,然而不是不能用,如果你把握的特地好,还是能够实际的。

unsafe 实现原理

在应用之前咱们先来看一下 unsafe 的源码局部,规范库 unsafe 包中只提供了 3 种办法,别离是:

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
  • Sizeof(x ArbitrayType)办法次要作用是用返回类型 x 所占据的字节数,但并不蕴含 x 所指向的内容的大小,与 C 语言规范库中的 Sizeof() 办法性能一样,比方在 32 位机器上,一个指针返回大小就是 4 字节。
  • Offsetof(x ArbitraryType)办法次要作用是返回构造体成员在内存中的地位离构造体起始处 (构造体的第一个字段的偏移量都是 0) 的字节数,即偏移量,咱们在正文中看一看到其入参必须是一个构造体,其返回值是一个常量。
  • Alignof(x ArbitratyType)的次要作用是返回一个类型的对齐值,也能够叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐无关的值,正当的内存对齐能够进步内存读写的性能。个别对齐值是 2^n,最大不会超过8(受内存对齐影响). 获取对齐值还能够应用反射包的函数,也就是说:unsafe.Alignof(x) 等价于 reflect.TypeOf(x).Align()。对于任意类型的变量xunsafe.Alignof(x) 至多为 1。对于 struct 构造体类型的变量 x,计算x 每一个字段 funsafe.Alignof(x,f)unsafe.Alignof(x)等于其中的最大值。对于 array 数组类型的变量 xunsafe.Alignof(x) 等于形成数组的元素类型的对齐倍数。没有任何字段的空 struct{} 和没有任何元素的 array 占据的内存空间大小为 0,不同大小为0 的变量可能指向同一块地址。

仔细的敌人会发发现这三个办法返回的都是 uintptr 类型,这个目标就是能够和 unsafe.poniter 类型互相转换,因为 *T 是不能计算偏移量的,也不能进行计算,然而 uintptr 是能够的,所以能够应用 uintptr 类型进行计算,这样就能够能够拜访特定的内存了,达到对不同的内存读写的目标。三个办法的入参都是 ArbitraryType 类型,代表着任意类型的意思,同时还提供了一个 Pointer 指针类型,即像 void * 一样的通用型指针。

type ArbitraryType int
type Pointer *ArbitraryType
// uintptr 是一个整数类型,它足够大,能够存储
type uintptr uintptr

下面说了这么多,可能会有点懵,在这里对三种指针类型做一个总结:

  • *T:一般类型指针类型,用于传递对象地址,不能进行指针运算。
  • unsafe.poniter:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(需转换到某一类型的一般指针)
  • uintptr:用于指针运算,GC不把 uintptr 当指针,uintptr无奈持有对象。uintptr类型的指标会被回收。

三者关系就是:unsafe.Pointer是桥梁,能够让任意类型的指针实现互相转换,也能够将任意类型的指针转换为 uintptr 进行指针运算,也就说 uintptr 是用来与 unsafe.Pointer 打配合,用于指针运算。画个图示意一下:

基本原理就说到这里啦,接下来咱们一起来看看如何应用~

unsafe.Pointer根本应用

咱们在上一篇剖析 atomic.Value 源码时,看到 atomic/value.go 中定义了一个 ifaceWords 构造,其中 typdata字段类型就是 unsafe.Poniter,这里应用unsafe.Poniter 类型的起因是传入的值就是 interface{} 类型,应用 unsafe.Pointer 强转成 ifaceWords 类型,这样能够把类型和值都保留了下来,不便前面的写入类型查看。截取局部代码如下:

// ifaceWords is interface{} internal representation.
type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}
// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (x interface{}) {vp := (*ifaceWords)(unsafe.Pointer(v))
  for {typ := LoadPointer(&vp.typ) // 读取曾经存在值的类型
    /**
    ..... 两头省略
    **/
    // First store completed. Check type and overwrite data.
        if typ != xp.typ { // 以后类型与要存入的类型做比照
            panic("sync/atomic: store of inconsistently typed value into Value")
        }
}

下面就是源码中应用 unsafe.Pointer 的一个例子,有一天当你筹备读源码时,unsafe.pointer的应用到处可见。好啦,接下来咱们写一个简略的例子,看看 unsafe.Pointer 是如何应用的。

func main()  {
    number := 5
    pointer := &number
    fmt.Printf("number:addr:%p, value:%d\n",pointer,*pointer)

    float32Number := (*float32)(unsafe.Pointer(pointer))
    *float32Number = *float32Number + 3

    fmt.Printf("float64:addr:%p, value:%f\n",float32Number,*float32Number)
}

运行后果:

number:addr:0xc000018090, value:5
float64:addr:0xc000018090, value:3.000000

由运行可知应用 unsafe.Pointer 强制类型转换后指针指向的地址是没有扭转,只是类型产生了扭转。这个例子自身没什么意义,失常我的项目中也不会这样应用。

总结一下根本应用:先把 *T 类型转换成 unsafe.Pointer 类型,而后在进行强制转换转成你须要的指针类型即可。

Sizeof、Alignof、Offsetof三个函数的根本应用

先看一个例子:

type User struct {
    Name string
    Age uint32
    Gender bool // 男:true 女:false 就是举个例子别吐槽我这么用。。。。}

func func_example()  {
    // sizeof
    fmt.Println(unsafe.Sizeof(true))
    fmt.Println(unsafe.Sizeof(int8(0)))
    fmt.Println(unsafe.Sizeof(int16(10)))
    fmt.Println(unsafe.Sizeof(int(10)))
    fmt.Println(unsafe.Sizeof(int32(190)))
    fmt.Println(unsafe.Sizeof("asong"))
    fmt.Println(unsafe.Sizeof([]int{1,3,4}))
    // Offsetof
    user := User{Name: "Asong", Age: 23,Gender: true}
    userNamePointer := unsafe.Pointer(&user)

    nNamePointer := (*string)(unsafe.Pointer(userNamePointer))
    *nNamePointer = "Golang 梦工厂"

    nAgePointer := (*uint32)(unsafe.Pointer(uintptr(userNamePointer) + unsafe.Offsetof(user.Age)))
    *nAgePointer = 25

    nGender := (*bool)(unsafe.Pointer(uintptr(userNamePointer)+unsafe.Offsetof(user.Gender)))
    *nGender = false

    fmt.Printf("u.Name: %s, u.Age: %d,  u.Gender: %v\n", user.Name, user.Age,user.Gender)
    // Alignof
    var b bool
    var i8 int8
    var i16 int16
    var i64 int64
    var f32 float32
    var s string
    var m map[string]string
    var p *int32

    fmt.Println(unsafe.Alignof(b))
    fmt.Println(unsafe.Alignof(i8))
    fmt.Println(unsafe.Alignof(i16))
    fmt.Println(unsafe.Alignof(i64))
    fmt.Println(unsafe.Alignof(f32))
    fmt.Println(unsafe.Alignof(s))
    fmt.Println(unsafe.Alignof(m))
    fmt.Println(unsafe.Alignof(p))
}

为了省事,把三个函数的应用示例放到了一起,首先看 sizeof 办法,咱们能够晓得各个类型所占字节大小,这里重点说一下 int 类型,Go 语言 中的 int 类型的具体大小是跟机器的 CPU 位数相干的。如果 CPU 32 位的,那么 int 就占 4 字节,如果 CPU 64 位的,那么 int 就占 8 字节,这里我的电脑是64 位的,所以后果就是8 字节。

而后咱们在看 Offsetof 函数,我想要批改构造体中成员变量,第一个成员变量是不须要进行偏移量计算的,间接取出指针后转换为 unsafe.pointer,在强制给他转换成字符串类型的指针值即可。如果要批改其余成员变量,须要进行偏移量计算,才能够对其内存地址批改,所以Offsetof 办法就可返回成员变量在构造体中的偏移量,也就是返回构造体初始地位到成员变量之间的字节数。看代码时大家应该要住 uintptr 的应用,不能够用一个长期变量存储 uintptr 类型,后面咱们提到过用于指针运算,GC不把 uintptr 当指针,uintptr无奈持有对象。uintptr类型的指标会被回收,所以你不晓得他什么时候会被 GC 掉,那样接下来的内存操作会产生什么样的谬误,咱也不晓得。比方这样一个例子:

// 切记不要这样应用
p1 := uintptr(userNamePointer)
nAgePointer := (*uint32)(unsafe.Pointer(p1 + unsafe.Offsetof(user.Age)))

最初看一下 Alignof 函数,次要是获取变量的对齐值,除了 int、uintptr 这些依赖 CPU 位数的类型,根本类型的对齐值都是固定的,构造体中对齐值取他的成员对齐值的最大值,构造体的对齐波及到内存对齐,咱们在上面具体介绍。

经典利用:string 与[]byte 的互相转换

实现 stringbyte的转换,失常状况下,咱们可能会写出这样的规范转换:

// string to []byte
str1 := "Golang 梦工厂"
by := []byte(s1)

// []byte to string
str2 := string(by)

应用这种形式进行转换都会波及底层数值的拷贝,所以想要实现零拷贝,咱们能够应用 unsafe.Pointer 来实现,通过强转换间接实现指针的指向,从而使 string[]byte指向同一个底层数据。在 reflect 包中有·stringslice 对应的构造体,他们的别离是:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

StringHeader代表的是 string 运行时的表现形式 (SliceHeader 同理),通过比照stringslice运行时的表白能够看出,他们只有一个 Cap 字段不同,所以他们的内存布局是对齐的,所以能够通过 unsafe.Pointer 进行转换,因为能够写出如下代码:

func stringToByte(s string) []byte {header := (*reflect.StringHeader)(unsafe.Pointer(&s))

    newHeader := reflect.SliceHeader{
        Data: header.Data,
        Len:  header.Len,
        Cap:  header.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&newHeader))
}

func bytesToString(b []byte) string{header := (*reflect.SliceHeader)(unsafe.Pointer(&b))

    newHeader := reflect.StringHeader{
        Data: header.Data,
        Len:  header.Len,
    }

    return *(*string)(unsafe.Pointer(&newHeader))
}

下面的代码咱们通过从新结构 slice headerstring header实现了类型转换,其实 []byte 转换成 string 能够省略掉本人结构 StringHeader 的形式,间接应用强转就能够,因为 string 的底层也是[]byte,强转会主动结构,省略后的代码如下:

func bytesToString(b []byte) string {return *(* string)(unsafe.Pointer(&b))
}

尽管这种形式更高效率,然而不举荐大家应用,后面也进步到了,这要是不平安的,应用当不当会呈现极大的隐患,一些重大的状况 recover 也不能捕捉。

内存对齐

当初计算机中内存空间都是依照 byte 划分的,从实践上讲仿佛对任何类型的变量的拜访能够从任何地址开始,然而理论状况是在拜访特定类型变量的时候常常在特定的内存地址拜访,这就须要各种类型数据依照肯定的规定在空间上排列,而不是程序的一个接一个的排放,这就对齐。

对齐的作用和起因:CPU拜访内存时,并不是一一字节拜访,而是以字长(word size)单位拜访。比方 32 位的 CPU,字长为4 字节,那么 CPU 拜访内存的单位也是 4 字节。这样设计能够缩小 CPU 拜访内存的次数,加大 CPU 拜访内存的吞吐量。假如咱们须要读取 8 个字节的数据,一次读取 4 个字节那么就只需读取 2 次就能够。内存对齐对实现变量的原子性操作也是有益处的,每次内存拜访都是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的拜访就是原子的,这个个性在并发场景下至关重要。

咱们来看这样一个例子:

// 64 位平台,对齐参数是 8
type User1 struct {
    A int32 // 4 
  B []int32 // 24 
  C string // 16 
  D bool // 1 
}

type User2 struct {B []int32
    A int32
    D bool
    C string
}

type User3 struct {
    D bool
    B []int32
    A int32
    C string
}
func main()  {
    var u1 User1
    var u2 User2
    var u3 User3

    fmt.Println("u1 size is",unsafe.Sizeof(u1))
    fmt.Println("u2 size is",unsafe.Sizeof(u2))
    fmt.Println("u3 size is",unsafe.Sizeof(u3))
}
// 运行后果 MAC: 64 位
u1 size is  56
u2 size is  48
u3 size is  56

从后果能够看出,字段搁置不同的程序,占用内存也不一样,这就是因为内存对齐影响了 struct 的大小,所以有时候正当的字段能够缩小内存的开销。上面咱们就一起来剖析一下内存对齐,首先要明确什么是内存对齐的规定, C 语言 的对齐规定与 Go 语言一样,所以 C 语言 的对齐规定对 Go 同样实用:

  • 对于构造的各个成员,第一个成员位于偏移为 0 的地位,构造体第一个成员的 偏移量(offset)为 0,当前每个成员绝对于构造体首地址的 offset 都是 该成员大小与无效对齐值中较小那个 的整数倍,如有须要编译器会在成员之间加上填充字节。
  • 除了构造成员须要对齐,构造自身也须要对齐,构造的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

好啦,晓得规定了,咱们当初来剖析一下下面的例子,依据我的 mac 应用的 64 位 CPU, 对齐参数是 8 来剖析,int32[]int32stringbool 对齐值别离是4881,占用内存大小别离是424161,咱们先依据第一条对齐规定剖析User1

  • 第一个字段类型是int32,对齐值是 4,大小为 4,所以放在内存布局中的第一位.
  • 第二个字段类型是 []int32,对齐值是 8,大小为 24,所以他的内存偏移值必须是 8 的倍数,所以在以后user1 中,就不能从第 4 位开始了,必须从第 5 位开始,也就偏移量为 8。第4,5,6,7 位由编译器进行填充,个别为 0 值,也称之为空洞。第 9 位到第 32 位为第二个字段B.
  • 第三个字段类型是 string,对齐值是8,大小为16,所以他的内存偏移值必须是 8 的倍数,因为user1 前两个字段就曾经排到了第 32 位,所以下一位的偏移量正好是 32,正好是字段C 的对齐值的倍数,不必填充,能够间接排列第三个字段,也就是从第 32 位到 48 位第三个字段C.
  • 第三个字段类型是 bool,对齐值是1,大小为1,所以他的内存偏移值必须是1 的倍数,因为 user1 前两个字段就曾经排到了第 48 位,所以下一位的偏移量正好是 48。正好是字段D 的对齐值的倍数,不必填充,能够间接排列到第四个字段,也就是从 48 到第 49 位是第三个字段D.
  • 好了当初第一条内存对齐规定后,内存长度曾经为 49 字节,咱们开始应用内存的第 2 条规定进行对齐。依据第二条规定,默认对齐值是 8,字段中最大类型水平是24,取最小的那一个,所以求出构造体的对齐值是8,咱们目前的内存长度是49,不是8 的倍数,所以须要补齐,所以最终的后果就是 56,补了7 位。

说了这么多,画个图看一下吧:

当初你们应该懂了吧,依照这个思路再去剖析其余两个 struct 吧,这里就不再剖析了。

对于内存对齐这里还有一最初须要留神的知识点,空 struct 不占用任何存储空间,空 struct{} 大小为 0,作为其余 struct 的字段时,个别不须要内存对齐。然而有一种状况除外:即当 struct{} 作为构造体最初一个字段时,须要内存对齐。因为如果有指针指向该字段, 返回的地址将在构造体之外,如果此指针始终存活不开释对应的内存,就会有内存泄露的问题(该内存不因构造体开释而开释)。来看一个例子:

func main()  {fmt.Println(unsafe.Sizeof(test1{})) // 8
    fmt.Println(unsafe.Sizeof(test2{})) // 4
}
type test1 struct {
    a int32
    b struct{}}

type test2 struct {a struct{}
    b int32
}

简略来说,对于任何占用 0 大小空间的类型,像 struct {} 或者 [0]byte 这些,如果该类型呈现在构造体开端,那么咱们就假如它占用 1 个字节的大小。因而对于 test1 构造体,他看起来就是这样:`

type test1 struct {
    a int32
//    b struct{}
  b [1]byte
}

因而在内存对齐时,最初构造体占用的字节就是 8 了。

重点要留神的问题:不要在构造体定义的最初增加零大小的类型

总结

好啦,终于又到文章的开端了,咱们来简略的总结一下,unsafe 包绕过了 Go 的类型零碎,达到间接操作内存的目标,应用它有肯定的风险性。然而在某些场景下,应用 unsafe 包提供的函数会晋升代码的效率,Go 源码中也是大量应用 unsafe 包。

unsafe 包定义了 Pointer 和三个函数:

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

uintptr 能够和 unsafe.Pointer 进行互相转换,uintptr 能够进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的联合就解决了 Go 指针不能进行数学运算的限度。通过 unsafe 相干函数,能够获取构造体公有成员的地址,进而对其做进一步的读写操作,冲破 Go 的类型平安限度。

最初咱们又学习了 内存对齐 的常识,这样设计能够缩小 CPU 拜访内存的次数,加大 CPU 拜访内存的吞吐量,所以构造体中字段正当的排序能够更节俭内存,留神:不要在构造体定义的最初增加零大小的类型。

好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者继续创作更多优质内容的能源!

创立了一个 Golang 学习交换群,欢送各位大佬们踊跃入群,咱们一起学习交换。入群形式:加我 vx 拉你入群,或者公众号获取入群二维码

结尾给大家发一个小福利吧,最近我在看 [微服务架构设计模式] 这一本书,讲的很好,本人也收集了一本 PDF,有须要的小伙能够到自行下载。获取形式:关注公众号:[Golang 梦工厂],后盾回复:[微服务],即可获取。

我翻译了一份 GIN 中文文档,会定期进行保护,有须要的小伙伴后盾回复 [gin] 即可下载。

翻译了一份 Machinery 中文文档,会定期进行保护,有须要的小伙伴们后盾回复 [machinery] 即可获取。

我是 asong,一名普普通通的程序猿,让咱们一起缓缓变强吧。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • machinery-go 异步工作队列
  • 源码分析 panic 与 recover,看不懂你打我好了!
  • 详解并发编程根底之原子操作(atomic 包)
  • 详解 defer 实现机制
  • 真的了解 interface 了嘛
  • Leaf—Segment 分布式 ID 生成零碎(Golang 实现版本)
  • 十张动图带你搞懂排序算法(附 go 实现代码)
  • go 参数传递类型
  • 手把手教姐姐写音讯队列
  • 常见面试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解 Context 包,看这一篇就够了!!!
  • 详解并发编程之 sync.Once 的实现(附上三道面试题)
  • 面试官:go 中 for-range 应用过吗?这几个问题你能解释一下起因吗
正文完
 0