• [go]Sizeof及内存对齐


    应该从变量说起

    //var 变量名 变量类型
    
    变量名是内存地址的别名, 起名要避讳关键字.
    
    变量类型:
    1.内容编址模型; 内存中只能存放01
    2.数值型: 如果是无符号数,使用源码存放; 如果是有符号数, 使用补码存放.
      字符型: 会通过ascii表映射为01存储.
    3.不同类型, 占用内存大小不一样. int占4byte,
    
    4.go支持强制类型转换, 但是没有隐式类型转换.
    

    下面是c语言中内存, 如果在栈上连续开辟, c中可以看到地址连续变小. 如果在堆上开辟内存, 可以看到内存地址变大.

    字长屏蔽了操作系统bit,使代码支持跨平台

    func main() {
    	var a int = 10
    	fmt.Println(unsafe.Sizeof(a)) //8
    }
    

    字长和操作系统位数有关.

    字长:CPU一次操作可以处理的二进制比特数(0或1), 1字长 = 1 bit
    
        一个字长是8的cpu,  一次能进行不大于 1111,1111 (8位) 的运算
        一个字长是16的cpu ,一次能进行不大于 1111,1111,1111,1111(16位)的运算
    
    //整型
    
    // wordSize代表是一个word里包含多少字节
    // 64bit: 8 bytes
    // 32bit: 4 bytes
    
    
    // The word size is the number of bytes in a word, which matches our address size.
    // For example, in 64-bit architecture, the word size is 64 bit (8 bytes), address size is 64
    // bit then our integer should be 64 bit.
    
    //字符串
    
    // Strings are a series of uint8 types.
    // A string is a two word data structure: first word represents a pointer to a backing array, the
    // second word represents its length.
    
    // If it is a zero value then the first word is nil, the second word is 0.
    
    //字符串是2个wordSize的数据结构
    type stringStruct struct {
    	str unsafe.Pointer
    	len int
    }
    
    
    
    type slice struct {
    	array unsafe.Pointer
    	len   int
    	cap   int
    }
    
    
    type hmap struct {
    	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
    	// Make sure this stays in sync with the compiler's definition.
    	count     int // # live cells == size of map.  Must be first (used by len() builtin)
    	flags     uint8
    	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
    	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    	hash0     uint32 // hash seed
    
    	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
    
    	extra *mapextra // optional fields
    }
    

    在 Go 中,指针就是 uintptr 类型。同样地,基于操作系统的体系结构,它将映射为 uint32 或者 uint64。
    Go 为指针创建了一个特殊的类型。
    

    //x 表示占位符, 1个字节
    
    struct {byte,int32,int64} //16
    bxxx|iiii|jjjj|jjjj
    
    struct {byte,int64,int32} //24
    bxxx|xxxx|jjjj|jjjj|iiii|xxxx
    
    
    struct {int32,byte,int64} //16
    iiii|bxxx|jjjj|jjjj
    
    struct {int32,int64,byte} //24
    iiii|xxxx|jjjj|jjjj|bxxx|xxxx
    
    
    struct {int64,byte,int32} //16
    jjjj|jjjj|bxxx|iiii
    
    struct {int64,int32,byte} //16
    jjjj|jjjj|iiii|bxxx
    
    //unsafe.Sizeof 丈量的是该类型在内存中开辟的大小
    
    Sizeof takes an expression x of any type and returns the size in bytes of a hypothetical variable v as if v was declared via var v = x. 
    
    The size does not include any memory possibly referenced by x. 
    
    
    For instance, if x is a slice, Sizeof returns the size of the slice descriptor, 
    not the size of the memory referenced by the slice. 
    The return value of Sizeof is a Go constant.
    
    //如果x为一个切片,sizeof返回的大小是切片的描述符(该type在内存中开辟了多大空间),而不是切片所指向的内存的大小。
    if x is a slice, Sizeof returns the size of the slice descriptor, not the size of the memory referenced by the slice.
    

    参考
    unsafe,顾名思义,是不安全的. 但是它也有它的优势,那就是可以绕过Go的内存安全机制,直接对内存进行读写,所以有时候因为性能的需要,会冒一些风险使用该包,对内存进行操作。

    Sizeof函数: Sizeof到底量的是什么的尺寸?

    Sizeof函数可以返回一个类型所占用的内存大小
    
    这个大小只与类型有关,和类型对应的变量存储的内容大小无关,比如bool型占用一个字节、int8也占用一个字节。
    
    func main() {
        fmt.Println(unsafe.Sizeof(true))                  //1
        fmt.Println(unsafe.Sizeof(int8(0)))    			  //1
        fmt.Println(unsafe.Sizeof(int16(10))) 			  //2
        fmt.Println(unsafe.Sizeof(int32(10000000)))		  //4
        fmt.Println(unsafe.Sizeof(int64(10000000000000))) //8
        fmt.Println(unsafe.Sizeof(int(10000000000000000)))//8
    }
    

    对于整型来说,占用的字节数意味着这个类型存储数字范围的大小,

        比如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
    

    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)) //16
    	fmt.Println("u2 size is ",unsafe.Sizeof(u2)) //24
    	fmt.Println("u3 size is ",unsafe.Sizeof(u3)) //16 
    	fmt.Println("u4 size is ",unsafe.Sizeof(u4)) //24
    	fmt.Println("u5 size is ",unsafe.Sizeof(u5)) //16
    	fmt.Println("u6 size is ",unsafe.Sizeof(u6)) //16
    }
    

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

    内存对齐影响struct的大小
    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、int32、int64,我们先使用第1条内存对齐规则进行内存对齐,其内存结构如下。

    //x 表示占位符, 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
    

    以上给出了答案,推到过程大家可以参考user1和user2试试。下一篇我们介绍通过unsafe.Pointer进行内存的运算,以及对内存的读写。

  • 相关阅读:
    Zookeeper Acl权限 超级用户权限 怎么跳过ACL密码/账户验证
    白名单与黑名单
    通过mstsc复制粘贴失败需要重新启动RDP剪切板监视程序rdpclip.exe
    go命令帮助
    go build 与go install
    1.Python编程基础
    使用 JMeter 进行压力测试
    js控制手机保持亮屏的库,解决h5移动端,自动息屏问题
    Linux安装配置redis 、启动redis、redis设置密码
    Linux安装部署FTP服务器
  • 原文地址:https://www.cnblogs.com/iiiiiher/p/12165898.html
Copyright © 2020-2023  润新知