Go语言之 unsafe 包以内存布局

unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让咱们尽量的不要使用它,若是你使用它,看到了这个名字,也会想到尽量的不要使用它,或者更当心的使用它。express

虽然这个包不安全,可是它也有它的优点,那就是能够绕过Go的内存安全机制,直接对内存进行读写,因此有时候由于性能的须要,会冒一些风险使用该包,对内存进行操做。安全

Sizeof函数

Sizeof函数能够返回一个类型所占用的内存大小,这个大小只有类型有关,和类型对应的变量存储的内容大小无关,好比bool型占用一个字节、int8也占用一个字节。ide

func main() {
    fmt.Println(unsafe.Sizeof(true))
    fmt.Println(unsafe.Sizeof(int8(0)))
    fmt.Println(unsafe.Sizeof(int16(10)))
    fmt.Println(unsafe.Sizeof(int32(10000000)))
    fmt.Println(unsafe.Sizeof(int64(10000000000000)))
    fmt.Println(unsafe.Sizeof(int(10000000000000000)))
}

对于整型来讲,占用的字节数意味着这个类型存储数字范围的大小,好比int8占用一个字节,也就是8bit,因此它能够存储的大小范围是-128~~127,也就是−2^(n-1)到2^(n-1)−1,n表示bit,int8表示8bit,int16表示16bit,其余以此类推。函数

对于和平台有关的int类型,这个要看平台是32位仍是64位,会取最大的。好比我本身测试,以上输出,会发现int和int64的大小是同样的,由于个人是64位平台的电脑。布局

func Sizeof(x ArbitraryType) uintptr

以上是Sizeof的函数定义,它接收一个ArbitraryType类型的参数,返回一个uintptr类型的值。这里的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

Alignof 函数

Alignof返回一个类型的对齐值,也能够叫作对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐能够提升内存读写的性能,关于内存对齐的知识能够参考相关文档,这里不展开介绍。测试

func main() {
   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)) }

从以上例子的输出,能够看到,对齐值通常是2^n,最大不会超过8(缘由见下面的内存对齐规则)。Alignof的函数定义和Sizeof基本上同样。这里须要注意的是每一个人的电脑运行的结果可能不同,大同小异。ui

func Alignof(x ArbitraryType) uintptr

此外,获取对齐值还可使用反射包的函数,也就是说:unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()spa

Offsetof 函数

Offsetof函数只适用于struct结构体中的字段相对于结构体的内存位置偏移量。结构体的第一个字段的偏移量都是0.code

func main() {
   var u1 user1    fmt.Println(unsafe.Offsetof(u1.b))    fmt.Println(unsafe.Offsetof(u1.i))    fmt.Println(unsafe.Offsetof(u1.j)) }
type user1 struct {    b byte    i int32    j int64
}

字段的偏移量,就是该字段在struct结构体内存布局中的起始位置(内存位置索引从0开始)。根据字段的偏移量,咱们能够定位结构体的字段,进而能够读写该结构体的字段,哪怕他们是私有的,***的感受有没有。偏移量的概念,咱们会在下一小结详细介绍。

此外,unsafe.Offsetof(u1.i)等价于reflect.TypeOf(u1).Field(i).Offset

有意思的struct大小

咱们定义一个struct,这个struct有3个字段,它们的类型有byte,int32以及int64,可是这三个字段的顺序咱们能够任意排列,那么根据顺序的不一样,一共有6种组合。

type user1 struct {
    b byte
    i int32
    j int64
}
type user2 struct {    b byte    j int64    i int32
}
type user3 struct {    i int32    b byte    j int64
}
type user4 struct {    i int32    j int64    b byte
}
type user5 struct {    j int64    b byte    i int32
}
type user6 struct {    j int64    i int32    b byte
}

根据这6种组合,定义了6个struct,分别位user1,user2,…,user6,那么如今你们猜想一下,这6种类型的struct占用的内存是多少,就是unsafe.Sizeof()的值。

你们可能猜想1+4+8=13,由于byte的大小为1,int32大小为4,int64大小为8,而struct其实就是一个字段的组合,因此猜想struct大小为字段大小之和也很正常。

可是,可是,我能够明确的说,这是错误的。

为何是错误的,由于有内存对齐存在,编译器使用了内存对齐,那么最后的大小结果就不同了。如今咱们正式验证下,这几种struct的值。

func main() {
   var u1 user1    
   var u2 user2    
   var u3 user3    
   var u4 user4    
   var u5 user5    
   var u6 user6    fmt.Println("u1 size is ",unsafe.Sizeof(u1))    fmt.Println("u2 size is ",unsafe.Sizeof(u2))    fmt.Println("u3 size is ",unsafe.Sizeof(u3))    fmt.Println("u4 size is ",unsafe.Sizeof(u4))    fmt.Println("u5 size is ",unsafe.Sizeof(u5))    fmt.Println("u6 size is ",unsafe.Sizeof(u6)) }

从以上输出能够看到,结果是:

u1 size is  16
u2 size is  24
u3 size is  16
u4 size is  24
u5 size is  16
u6 size is  16

结果出来了(个人电脑的结果,Mac64位,你的可能不同),4个16字节,2个24字节,既不同,又不相同,这说明:

  1. 内存对齐影响struct的大小

  2. struct的字段顺序影响struct的大小

综合以上两点,咱们能够得知,不一样的字段顺序,最终决定struct的内存大小,因此有时候合理的字段顺序能够减小内存的开销

内存对齐会影响struct的内存占用大小,如今咱们就详细分析下,为何字段定义的顺序不一样会致使struct的内存占用不同。

在分析以前,咱们先看下内存对齐的规则:

  1. 对于具体类型来讲,对齐值=min(编译器默认对齐值,类型大小Sizeof长度)。也就是在默认设置的对齐值和类型的内存占用大小之间,取最小值为该类型的对齐值。个人电脑默认是8,因此最大值不会超过8.

  2. struct在每一个字段都内存对齐以后,其自己也要进行对齐,对齐值=min(默认对齐值,字段最大类型长度)。这条也很好理解,struct的全部字段中,最大的那个类型的长度以及默认对齐值之间,取最小的那个。

以上这两条规则要好好理解,理解明白了才能够分析下面的struct结构体。在这里再次提醒,对齐值也叫对齐系数、对齐倍数,对齐模数。这就是说,每一个字段在内存中的偏移量是对齐值的倍数便可

咱们知道byte,int32,int64的对齐值分别为1,4,8,占用内存大小也是1,4,8。那么对于第一个structuser1,它的字段顺序是byte、int3二、int64,咱们先使用第1条内存对齐规则进行内存对齐,其内存结构以下,内存布局中有竖线(|),用于每四个字节的分割,下同。

bxxx|iiii|jjjj|jjjj

user1类型,第1个字段byte,对齐值1,大小1,因此放在内存布局中的第1位。

第2个字段int32,对齐值4,大小4,因此它的内存偏移值必须是4的倍数,在当前的user1中,就不能从第2位开始了,必须从第5位开始,也就是偏移量为4。第2,3,4位由编译器进行填充,通常为值0,也称之为内存空洞。因此第5位到第8位为第2个字段i。

第3字段,对齐值为8,大小也是8。由于user1前两个字段已经排到了第8位,因此下一位的偏移量正好是8,是第3个字段对齐值的倍数,不用填充,能够直接排列第3个字段,也就是从第9位到第16位为第3个字段j。

如今第一条内存对齐规则后,内存长度已经为16个字节,咱们开始使用内存的第2条规则进行对齐。根据第二条规则,默认对齐值8,字段中最大类型长度也是8,因此求出结构体的对齐值位8,咱们目前的内存长度为16,是8的倍数,已经实现了对齐。

因此到此为止,结构体user1的内存占用大小为16字节。

如今咱们再分析一个user2类型,它的大小是24,只是调换了一下字段i和j的顺序,就多占用了8个字节,咱们看看为何?仍是先使用咱们的内存第1条规则分析。

bxxx|xxxx|jjjj|jjjj|iiii

按对齐值和其占用的大小,第1个字段b偏移量为0,占用1个字节,放在第1位。

第2个字段j,是int64,对齐值和大小都是8,因此要从偏移量8开始,也就是第9到16位为j,这也就意味着第2到8位被编译器填充。

目前整个内存布局已经偏移了16位,正好是第3个字段i的对齐值4的倍数,因此不用填充,能够直接排列,第17到20位为i。

如今全部字段对齐好了,整个内存大小为1+7+8+4=20个字节,咱们开始使用内存对齐的第2条规则,也就是结构体的对齐,经过默认对齐值和最大的字段大小,求出结构体的对齐值为8。

如今咱们的整个内存布局大小为20,不是8的倍数,因此咱们须要进行内存填充,补足到8的倍数,最小的就是24,因此对齐后整个内存布局为

bxxx|xxxx|jjjj|jjjj|iiii|xxxx

因此这也是为何咱们最终得到的user2的大小为24的缘由。
基于以上办法,咱们能够得出其余几个struct的内存布局。

user3

iiii|bxxx|jjjj|jjjj

user4

iiii|xxxx|jjjj|jjjj|bxxx|xxxx

user5

jjjj|jjjj|bxxx|iiii

user6

jjjj|jjjj|iiii|bxxx

以上给出了答案,推到过程你们能够参考user1user2试试。

相关文章
相关标签/搜索