[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进行内存的运算,以及对内存的读写。

原文地址:https://www.cnblogs.com/iiiiiher/p/12165898.html