什么是内存对齐
为保证程序顺利高效的运行,编译器会把各种类型的数据安排到合适的地址,并占用合适的长度,这就是内存对齐。
每种类型的对齐值就是它的对齐边界,内存对齐要求数据存储地址以及占用的字节数都要是它的对齐边界的倍数。所以下述的int32要错开两个字节,从4开始存,却不能紧接着从2开始。
也可以这样解释:
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)。
如果不进行内存对齐
比如我们想从地址1开始读8字节的数据:
CPU会分两次读:
- 第一次从
0 - 7
但只取后7
字节。 - 第二次从
8 - 15
但只取第1
字节。
分两次读,这样势必会对性能造成影响。
为什么要内存对齐
原因主要有两点:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
对齐边界
那该怎么确定每种数据的对齐边界呢?这和平台有关,go语言支持这些平台:
可以看到常见的32位平台,指针宽度和寄存器宽度都是4字节,64位平台上都是8字节。而被go语言称为寄存器宽度的这个值,就可以理解为机器字长,也是平台对应的最大对齐边界。
而数据类型的对齐边界,是取类型大小与平台最大对齐边界中较小的那个。不过要注意,同一个类型在不同平台上,大小可能不同,对齐边界也可能不同。
为什么不统一使用平台最大对齐边界呢?或者统一按各类型大小来对齐呢?
我们来试一下,假设目前是64位平台,最大对齐边界为8字节。int8只有1字节,按照1字节对齐的话,它可以放在任何位置,因为总能通过一次读取把它完整拿出来。如果统一对齐到8字节,虽然同样只要读取一次,但每个int8的变量都要浪费7字节,所以对齐到1。
int16占2字节,按照2字节对齐,可以从这些地址开始存,而且能保证只用读取一次。
如果按1字节对齐就可能存成这样,那就要读取两次再截取拼接,会影响性能。
如果按8字节对齐,会与int8一样浪费内存,所以对齐到2。
这是小于最大对齐边界的情况,再来看看大于的情况。
假设要在32位的平台下存储一个int64类型的数据,在0和1位置被占用的情况下,就要从位置8开始存。而如果对齐到4,就可以从位置4开始,内存浪费更少,所以选择对齐到4。
因此类型对齐边界会这样选择,依然是为了减少浪费提升性能。
GO 计算对齐边界函数
在go语言中可以调用 unsafe.Alignof
来返回相应类型的对齐边界:
func main() {
fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))
}
运行结果:
bool align: 1
int32 align: 4
int8 align: 1
int64 align: 8
byte align: 1
string align: 8
map align: 8
确定结构体的对齐边界
对结构体而言,首先要确定每个成员的对齐边界,然后取其中最大的,这就是这个结构体的对齐边界。
然后来存储这个结构体变量:
内存对齐要求一:
- 存储这个结构体的起始地址,是对齐边界的倍数。
假设从0开始存,结构体的每个成员在存储时,都要把这个起始地址当作地址0,然后再用相对地址来决定自己该放在哪里。
内存对齐要求2:
- 结构体整体占用字节数需要是类型对齐边界的倍数,不够的话要往后扩张一下。
所以最终上述结构体类型的大小就是24字节。
案例
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}
type Part2 struct {
a bool
c int8
e byte
b int32 // 4个字节
d int64
}
分别求以上两个结构体占用的字节:
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2))
这里我们直接调用函数求得:
part1 size: 32, align: 8
part2 size: 16, align: 8
原因请读者来思考。
参考资料:
https://blog.csdn.net/u010853261/article/details/102557188
https://www.bilibili.com/video/BV1Ja4y1i7AF?from=search&seid=16213689667007976568&spm_id_from=333.337.0.0
到此这篇关于golang内存对齐的文章就介绍到这了,更多相关golang内存对齐内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!