内存对齐
内存对齐
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个字节。