内存对齐

CPU从内存中获取数据,不是一个字节一个字节的读取,而是根据机器字读取的。比如32位的机器每次获取内存数据,一次性读取4个字节,64位机器则是8个字节

为什么需要内存对齐

如果不进行内存对齐,CPU可能需要两个时钟周期获取相应的数据。主要原因是CPU无法直接访问非对齐内存起始地址的内存块。

例如: CPU访问一个8字节的数据

  • 如果对齐内存,那么CPU访问的起始位置将会是8的倍数,直接一次性访问就可以了。
  • 如果不对齐内存,假设变量的起始地址我为0x13,由于CPU无法访问一个没有对齐边界的内存块起始地址也就是无法从0x13开始读数据,那么CPU只能先访问0x10读取8字节数据,然后再访问0x18读取8字节数据。所以就导致了两个时钟周期访问数据,并且还需要拼接两个数据。

变量的对齐边界

变量的对齐边界:变量在内存的起始地址,必须是某个固定数值的整数倍,这个固定数值就是变量的对齐边界。

比如一个变量的对齐边界是2字节,那么该变量的起始位置必须是0x2,0x4,0x6,0x8,0xa,0xc...

Go语言的对齐边界,Go会自动处理,无需手动干预。

基础类型的对齐边界:

数据类型 大小(字节) 对齐边界(字节) 示例起始地址(合法)
bool 1 1 0x01、0x02、0x03…
int8/uint8 1 1 任意地址
int16/uint16 2 2 0x02、0x04、0x06…
int32/uint32 4 4 0x04、0x08、0x0C…
int64/uint64 8 8 0x08、0x10、0x18…
float32 4 4 同上
float64 8 8 同上
pointer(指针) 8 8 同上(64 位架构)
string/slice 16 8 同上(因为底层是结构体,按最大字段对齐)

使用Go来查看变量的对齐边界。

package main

import (
	"fmt"
	"unsafe"
)

// 定义测试结构体
type TestStruct struct {
	a int8  // 大小1,对齐边界1
	b int64 // 大小8,对齐边界8
	c int16 // 大小2,对齐边界2
}

func main() {
	// 1. 查看基础类型的对齐边界
	fmt.Println("int8 对齐边界:", unsafe.Alignof(int8(0)))   // 输出1
	fmt.Println("int16 对齐边界:", unsafe.Alignof(int16(0))) // 输出2
	fmt.Println("int64 对齐边界:", unsafe.Alignof(int64(0))) // 输出8
	fmt.Println("指针 对齐边界:", unsafe.Alignof(&TestStruct{})) // 输出8(64位架构)

	// 2. 查看结构体的对齐边界(等于内部最大字段的对齐边界)
	var ts TestStruct
	fmt.Println("TestStruct 对齐边界:", unsafe.Alignof(ts)) // 输出8(因为b是int64,对齐边界8)

	// 3. 查看结构体字段的内存地址(验证对齐)
	fmt.Printf("a的地址:%p\n", &ts.a) // 比如0x140000a8000(8的整数倍,符合结构体对齐边界)
	fmt.Printf("b的地址:%p\n", &ts.b) // 比如0x140000a8008(8的整数倍,对齐)
	fmt.Printf("c的地址:%p\n", &ts.c) // 比如0x140000a8010(8的整数倍,因为结构体对齐边界是8)

	// 4. 查看结构体总大小(包含填充字节)
	fmt.Println("TestStruct 总大小:", unsafe.Sizeof(ts)) // 输出24(不是1+8+2=11,因为有填充)
}

结构体的内存对齐

结构体的每个字段都要符合内存对齐的规则。结构体的对齐边界为结构体字段的最大对齐边界。结构体内部字段的存储顺序不同可能导致结构体大小会不同。

计算结构体内存大小,需要满足两个条件:

  • 所有字段需要对齐
  • 结构体整体大小也要对齐,即为对齐边界的整数倍

案例:

// 定义测试结构体
type TestStruct struct {
	a int8  // 大小1,对齐边界1
    b int64 // 大小8,对齐边界8
	c int16 // 大小2,对齐边界2
}

上述结构体的大小为24,我们详细分析一下为什么是24

  • a对齐边界是1,所以任意起始地址都说是可以。
  • b对齐边界是8,所以它的起始位置必须是8的倍数。所以此时需要在a后面填充7个字节,才能让b对齐。
  • c的对齐边界是2,所以此时不需要填充。

理论上来说结构的内存大小是 1+7+8+2=18 但是结构体的总大小也必是对齐边界的整数倍。所以之后会在c之后填充6个字节。