为什么要内存对齐
CPU访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如32位的CPU,字长为4字节,那么CPU访问内存的单位也是4字节。
这么设计的目的,是减少CPU访问内存的次数,加大CPU访问内存的吞吐量。比如同样读取8个字节的数据,一次读取4个字节那么只需要读取2次
- 某些处理器只能存取对齐的数据,存取非对齐的数据可能会引发异常
- 某些处理不能保证在存取非对齐数据的时候的操作是原子操作
Golang内存对齐
分析下面结构体
type S struct {
A uint32
B uint64
C uint64
D uint64
E struct{}
}
func main() {
fmt.Println(unsafe.Offsetof(S{}.E)) // 32
fmt.Println(unsafe.Sizeof(S{}.E)) // 0
fmt.Println(unsafe.Sizeof(S{})) // 40
}
8字节对齐
一个非空结构体包含有尾部 size 为 0 的变量(字段),如果不给它分配内存,那么该变量(字段)的指针地址将指向一个超出该结构体内存范围的内存空间。这可能会导致内存泄漏,或者在内存垃圾回收过程中,程序 crash 掉
类型大小
类型 | 大小 | go官方要求 |
---|---|---|
bool | 1 | 无 |
byte unit8 int8 | 1 | 1 |
unit16 int16 | 2 | 2 |
unit32 int32 float32 | 4 | 4 |
unit64 int64 float64 complex64 | 8 | 8 |
complex128 | 16 | 16 |
int uint | 1 word | 32位架构为4字节,64位架构为8字节 |
uintptr | 1 word | 必须能够存在任一内存地址 |
string | 2 words | 无 |
指针 | 1 word | 无 |
切片 | 3 words | 无 |
map | 1 word | 无 |
chan | 1 word | 无 |
func | 1 word | 无 |
interface | 2 words | 无 |
struct | 所有字段大小之和+所有填充的字节数 | 空结构体大小为0 |
array | 元素类型大小*元素数量 | 没有元素的数组大小为0 |
struct{} [0]T{} | 0 | 0 |
64位安全访问保证
在 32 位系统上想要原子操作 64 位字(如 uint64)的话,需要由调用方保证其数据地址是 64 位对齐的,否则原子访问会有异常。
拿uint64来说,大小为 8bytes,32 位系统上按 4 字节 对齐,64 位系统上按 8 字节对齐。在 64 位系统上,8bytes 刚好和其字长相同,所以可以一次完成原子的访问,不被其他操作影响或打断。
而 32 位系统,4byte 对齐,字长也为 4bytes,可能出现uint64的数据分布在两个数据块中,需要两次操作才能完成访问。如果两次操作中间有可能别其他操作修改,不能保证原子性。这样的访问方式也是不安全的
在 32 位系统上,开发者有义务使 64 位字长的数据的原子访问是 64 位(8 字节)对齐的。在全局变量,结构体和切片的的第一个字长数据可以被认为是 64 位对齐的
如何保证呢?
变量或开辟的结构体、数组和切片值中的第一个 64 位字可以被认为是 8 字节对齐
开辟的意思是通过声明,make,new 方式创建的,就是说这样创建的 64 位字可以保证是 64 位对齐的。
References
https://www.jianshu.com/p/67600a8ddb8c
https://xie.infoq.cn/article/594a7f54c639accb53796cfc7