1. Golang 优化之内存对齐

1.1. 前文

话说今天在用 uintptr 进行指针运算的时候, 突然想起来有个内存对齐的东西, 那么对这个 uintptr 计算是否会有影响?

带着疑问, 开始吧。

你将获得以下知识点:

  1. 什么是内存对齐?
  2. 为什么需要内存对齐?
  3. 如何进行内存对齐?
  4. golang 的内存对齐如何体现?
  5. 如何利用内存对齐来优化 golang?

1.2. 正文

1.2.1. 什么是内存对齐?

在想象中内存应该是一个一个独立的字节组成的。像这样:

事实上, 人家是这样的:

内存是按照成员的声明顺序, 依次分配内存, 第一个成员偏移量是 0, 其余每个成员的偏移量为指定数的整数倍数 (图中是 4)。像这样进行内存的分配叫做内存对齐。

1.2.2. 为什么需要内存对齐?

原因有两点:

1.2.2.1. 平台原因

1/5/6/7

1.2.2.2. 性能原因

访问未对齐的内存, 需要访问两次; 如果对齐的话就只需要一次了。
(解释: 比如取 int64, 按照 8 个位对齐好了, 那获取的话直接就是获取 8 个字节就好了, 边界好判断)

1.3. 如何进行内存对齐?

二个原则:

  1. 具体类型, 对齐值 = min(编译器默认对齐值, 类型大小 Sizeof 长度)
  2. struct 每个字段内部对齐, 对齐值 = min(默认对齐值, 字段最大类型长度)

1.4. golang 的内存对齐如何体现?

1.4.1. 结构体的相同成员不同顺序

结构体是平时写代码经常用到的。相同的成员, 不同的排列顺序, 会有什么区别吗?

举个例子:

func main() {
	fmt.Println(unsafe.Sizeof(struct {
		i8  int8
		i16 int16
		i32 int32
	}{}))
	fmt.Println(unsafe.Sizeof(struct {
		i8  int8
		i32 int32
		i16 int16
	}{}))
}

输出:

1   8
2   12

what? 竟然不一样。

分析一波: 需要内存对齐的话, 因为最大是 int32, 所以最终记过必须是 4 个字节的倍数才能对齐。

8-16-32|x-xx|xxxx|
8-32-16|x—|xxxx|xx–|

一眼就看出了大小了。

x-xxxxx-

1.4.2. 指针运算

现在对结构体 Test 通过指针计算的方式进行赋值。

|x-xx|xxxx|-
type Test struct {
	i8  int8
	i16 int16
	i32 int32
}

func main() {
	var t = new(Test)
	// 从 0 开始
	var i8 = (*int8)(unsafe.Pointer(t))
	*i8 = int8(10)
	
	// 偏移 int8+1 的字节数, 注意这里有个 1! ! !  
	var i16 = (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(t))+ uintptr(1)  + uintptr(unsafe.Sizeof(int8(0)))))
	*i16 = int16(10)

	// 偏移 int8+1+int16 + 的字节数, 注意这里有个 1! ! !  
	var i32 = (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(t)) + uintptr(1) + uintptr(unsafe.Sizeof(int8(0))+uintptr(unsafe.Sizeof(int16(0))))))
	*i32 = int32(10)
	fmt.Println(*t)
}

输出:

1 | {10 10 10}

附上两个神器:

unsafe.Alignof(t.i16)unsafe.Offsetof(t.i16)

1.5. 如何利用内存对齐来优化 golang

1.5.1. 结构体占用内存过大的问题

根据计算对齐值进行成员顺序的拼凑, 可以一定程度上缩小结构体占用的内存。

1.5.2. 指针运算的坑

通过分析偏移量和对齐值, 准确计算每个成员所偏移的位数, 避免算错。