内存对齐和填充规则
-
对齐要求:每个数据类型的起始地址必须是其大小的倍数。
int8
(1字节):不需要对齐。int16
(2字节):起始地址必须是2的倍数。int32
(4字节):起始地址必须是4的倍数。int64
(8字节):起始地址必须是8的倍数。
-
填充规则:如果当前偏移量不是下一个成员变量对齐要求的倍数,则编译器会在前一个成员后插入“填充字节”,以使下一个成员的起始地址满足对齐要求。
-
结构体总大小:结构体的总大小必须是其最大成员对齐大小的倍数,必要时会在结构体末尾添加额外的填充字节。
示例解析
示例 1:未优化的结构体
type Unoptimized struct {a int8 // 1 byteb int32 // 4 bytes, 需要4字节对齐c int16 // 2 bytes, 需要2字节对齐
}
- a 占用 1 字节,起始地址为 0。
- b 需要 4 字节对齐,但
a
只占用了 1 字节,因此在a
后面需要填充 3 字节,使得b
的起始地址为 4。 - c 需要 2 字节对齐,
b
占用 4 字节,所以c
的起始地址为 8,不需要额外填充。 - 结构体总大小为 10 字节(1 + 3 + 4 + 2),但为了使结构体大小为 4 字节对齐(最大成员
b
是 4 字节对齐),需要在末尾再填充 2 字节。
最终结构体大小为 12 字节。
示例 2:优化后的结构体
type Optimized struct {b int32 // 4 bytes, 需要4字节对齐c int16 // 2 bytes, 需要2字节对齐a int8 // 1 byte, 不需要对齐
}
- b 占用 4 字节,起始地址为 0,符合 4 字节对齐。
- c 需要 2 字节对齐,
b
占用 4 字节,所以c
的起始地址为 4,不需要额外填充。 - a 占用 1 字节,
c
占用 2 字节,所以a
的起始地址为 6,不需要额外填充。 - 结构体总大小为 7 字节(4 + 2 + 1),但为了使结构体大小为 4 字节对齐(最大成员
b
是 4 字节对齐),需要在末尾再填充 1 字节。
最终结构体大小为 8 字节。
图解填充规则
假设我们有一个结构体:
type Example struct {a int8 // 1 byteb int16 // 2 bytesc int32 // 4 bytes
}
我们可以用图来表示内存布局:
Offset: 0 1 2 3 4 5 6 7 8 9 10 11+--+--+--+--+--+--+--+--+--+--+--+--+| a| P| P| P| b| b| P| P| c| c| c| c|+--+--+--+--+--+--+--+--+--+--+--+--+
a
占用 1 字节,后面填充 3 字节(P 表示填充字节)。b
占用 2 字节,后面填充 2 字节。c
占用 4 字节。
调整顺序后:
type Example struct {c int32 // 4 bytesb int16 // 2 bytesa int8 // 1 byte
}
内存布局变为:
Offset: 0 1 2 3 4 5 6 7+--+--+--+--+--+--+--+--+| c| c| c| c| b| b| a| P|+--+--+--+--+--+--+--+--+
c
占用 4 字节。b
占用 2 字节。a
占用 1 字节,后面填充 1 字节。
最终结构体大小为 8 字节,比原来的 12 字节更紧凑。
总结
通过将占用较大内存空间的成员放在前面,可以减少编译器为了对齐而插入的填充字节数量,从而使结构体更加紧凑,节省内存。你可以使用 unsafe.Sizeof()
和 unsafe.Alignof()
来验证这些结构体的实际大小和对齐方式。