前文

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

带着疑问,开始吧。

你将获得以下知识点:
1.什么是内存对齐?
2.为什么需要内存对齐?
3.如何进行内存对齐?
4.golang的内存对齐如何体现?
5.如何利用内存对齐来优化golang?

正文

1.什么是内存对齐?

在想象中内存应该是一个一个独立的字节组成的。像这样:
在这里插入图片描述

事实上,人家是这样的:
在这里插入图片描述

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

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

原因有两点:

平台原因

并不是所有的硬件平台都能访问任意地址上的任意数据,会直接报错的!
(解释:比如说有的cpu读取4个字节数据,要是没有内存对齐,从1开始那么内存就需要把0-7字节的全部取出来,再剔除掉1/5/6/7,增加了额外的操作,cpu不一定能这么搞,自然就报错了)

性能原因

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

3.如何进行内存对齐?

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

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

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
	}{}))
}

输出:

8
12

what?竟然不一样。
分析一波:需要内存对齐的话,因为最大是int32,所以最终记过必须是4个字节的倍数才能对齐。
当8-16-32的时候,类似这样|x-xx|xxxx|。
当8-32-16的时候,类似这样|x—|xxxx|xx–|。
一眼就看出了大小了。

这里的为什么是x-xx而不是xxx-需要说明下。因为当int8放入内存的时候,其占坑1个字节,对齐值为1,而int16占坑2个字节,对齐值为2,所以说会先偏移2个字节从第三个字节才开始放int16的数

4.2指针运算

现在对结构体Test通过指针计算的方式进行赋值。
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)
}

输出:

{10 10 10}

附上两个神器:

功能函数
获取对齐值unsafe.Alignof(t.i16)
获取偏移值unsafe.Offsetof(t.i16)

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

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

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

5.2 指针运算的坑

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

延伸阅读

如果你觉得有收获~可以关注我的公众号【咖啡色的羊驼】~第一时间收到我的分享和知识梳理~
在这里插入图片描述