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

原文链接:戳这里

前言

有看源码的敌人应该会发现,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应用过吗?这几个问题你能解释一下起因吗

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据