开心一刻
刚才在公园散步,听见前面有个美女一直在喊:“宝贝你在哪,快出来啊,小宝贝,快出来!”
喊的那叫一个心酸,后来突然从草丛里钻出一只小狗,美女上前就把小狗抱起来对着狗嘴一顿猛亲。。。
突然给狗一个嘴巴子:“你TM是不是又吃屎了?”
写在前面
了解Go语言内存对齐, 有助于我们在写代码时候合理利用内存空间, 有效避免内存空间的浪费。Go语言内存对齐的知识能让我们在写结构体时让结构体占用内存空间更加合理。
什么是内存对齐
内存对齐是按照成员的声明顺序,依次分配内存,第一个成员偏移量是0,其余每个成员的偏移量为指定数的整数倍数。
操作系统是32位的,一次就能访问4字节的内存,同样,64位系统能访问8字节的内存。在各大操作系统中,指针宽度和寄存器宽度都是一样的,恰好对应32位操作系统下为4字节,64位系统下为8字节。在Go语言中寄存器宽度就可以理解为机器字长,也是平台的最大对齐边界,在Go语言中,数据类型非常丰富,每种类型都有其规定的字节长度,例如int8占一字节,int32占4字节,int在32位系统占4字节,64位系统占8字节。
Go语言中,每种类型的对齐边界是按照平台最大边界和类型长度的较小值确定的(也就是说每种类型的对齐边界不可能超过平台最大对齐边界),这样做的目的就是减少内存浪费以及提高性能。特别的,在Go的结构体类型中,选择结构体中每种类型的对齐边界的最大值作为该结构体的对齐边界,所以在确定结构体对齐边界之前,要首先确定结构体中每种类型的对齐边界,也不要搞错了,否则数据类型的对齐边界搞错,那么结构体的对齐边界可能也会搞错。下面的图片中是64位平台和32位平台下每种类型的对齐边界。
Go中内存对齐时要注意两点:
起始地址是类型对齐边界的倍数(避免多次访问内存)
类型占用内存的长度是类型对齐边界的倍数(避免使用数组时出现的内存不对齐的情况)
为什么要有内存对齐
平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。这么设计的目的,是减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。比如同样读取 8 个字节的数据,一次读取 4 个字节那么只需要读取 2 次。
Go中如何计算结构体占用字节数
在 Go 语言中,我们可以使用 unsafe.Sizeof 计算出一个数据类型实例需要占用的字节数。
package main
import (
"fmt"
"unsafe"
)
type A struct {
a int16
b string
c int32
}
func main() {
fmt.Println(unsafe.Sizeof(A{})) //输出为 32
}
用这个例子举例说明为什么这个结构体占32字节。首先可以确定该结构体的对齐边界是8,因为64位平台下string占用16字节,而平台最大边界是8字节,所以string的对齐边界是8字节,而string的对齐边界是最大的,所以结构体的对齐边界就是8字节。首先a占用2字节,直接从下标0开始存储,占用2字节,下一个下标为2;然后再安排变量b,下标2不是8的倍数,所以需要向后延伸,可以从下标8开始,然后占用16字节;最后安排变量c,下标24正好是4的倍数,所以c从下标24存储,直接占用4字节,此时总共为28字节。但是28并不是8的整数倍,所以还需要延伸,再占用4个字节,所以总共是32个字节。
对于上面的例子,改变变量的顺序能使内存使用更加合理,例如把变量b和变量c交换位置,实际占用的字节数就是24字节。
空struct{}的对齐
空 struct{} 大小为 0,作为其他 struct 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{} 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。
因此,当 struct{} 作为其他 struct 最后一个字段时,需要填充额外的内存保证安全。我们做个试验,验证下这种情况。
type demo3 struct {
c int32
a struct{}
}
type demo4 struct {
a struct{}
c int32
}
func main() {
fmt.Println(unsafe.Sizeof(demo3{})) // 8
fmt.Println(unsafe.Sizeof(demo4{})) // 4
}
可以看到,demo4{} 的大小为 4 字节,与字段 c 占据空间一致,而 demo3{} 的大小为 8 字节,即额外填充了 4 字节的空间。
注意两点:
- 空struct的嵌套还是为空struct
- 只有空struct{}放在结构体最后一个位置时该空struct{}类型的变量才会占用内存,且占用内存与它的上一个元素占用内存保持一致。
空struct{}不放在结构体最后一个位置时,如果要返回空struct{}的地址,那么返回的时它的下一个元素的地址,如果放在最后一个位置,此时如果没有这个特殊处理,如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。
参考资料: