一. 什么是内存对齐(Memory alignment),也叫字节对齐
在计算机中,内存是按 字节(byte, 1byte = 8bit) 划分的,而cpu在读取内存数据时,并不是一字节一字节读取的。实际上是按 块 来读取的。
块的大小可以是1,2,4,8,16等等,这块的大小也称为 内存访问粒度
而 内存对齐 是将特定的数据类型按照一定的规则摆放在内存上,以此 提高cpu访问内存的速度
看如下示例:
如上图,内存访问颗粒度为4,数据大小为4字节的场景:
场景1:做了内存对齐,index 2-3 补充空字节,这样 数据1,数据2 都刚好只存在一个内存块中,
读取数据时,一次就能将数据读取完毕
场景2:没有内存对齐,数据2 一部分存在内存块1中,一部分存在内存块2中。
读取数据2时,需要将块1的数据0-3读取出来,丢弃0-1字节数据,
再读取块2的数据4-7,丢弃6-7字节的数据,
再组合 2,3,4,5字节才能得到数据2
总结:很明显,场景2读取数据比场景1繁琐许多。
如果不做内存对齐,cpu在读取内存数据时,会增加许多耗时的动作。
而做了内存对齐后,虽然会产生一部分内存碎片,但极大提高了cpu访问内存数据的速度,属于空间换时间的做法
提高访问速度是内存对齐的原因之一,另外一个原因是某些平台(arm)不支持未内存对齐的访问
了解了内存对齐的原理以及优缺点后,下面将内存对齐在实际编程中的应用。
用下面的例子来说明:
// 写法1
type Part1 struct {
a int8
b int64
c int16
}
// 写法2
type Part2 struct {
a int8
b int16
c int64
}
func main() {
var (
pBool bool = false
pByte byte = '0'
pSting string = "hello"
pInt8 int8 = 1
pInt16 int16 = 1
pInt32 int32 = 1
pInt64 int64 = 1
pfloat32 float32 = 1
pfloat64 float64 = 1
pComplex64 complex64 = 1
pComplex128 complex128 = 1
part1 = Part1{}
part2 = Part2{}
)
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pBool, unsafe.Sizeof(pBool), unsafe.Alignof(pBool))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pByte, unsafe.Sizeof(pByte), unsafe.Alignof(pByte))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pSting, unsafe.Sizeof(pSting), unsafe.Alignof(pSting))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pInt8, unsafe.Sizeof(pInt8), unsafe.Alignof(pInt8))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pInt16, unsafe.Sizeof(pInt16), unsafe.Alignof(pInt16))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pInt32, unsafe.Sizeof(pInt32), unsafe.Alignof(pInt32))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pInt64, unsafe.Sizeof(pInt64), unsafe.Alignof(pInt64))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pfloat32, unsafe.Sizeof(pfloat32), unsafe.Alignof(pfloat32))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pfloat64, unsafe.Sizeof(pfloat64), unsafe.Alignof(pfloat64))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pComplex64, unsafe.Sizeof(pComplex64), unsafe.Alignof(pComplex64))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", pComplex128, unsafe.Sizeof(pComplex128), unsafe.Alignof(pComplex128))
fmt.Println()
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", part1, unsafe.Sizeof(part1), unsafe.Alignof(part1))
fmt.Printf("%10T 类型 所占字节数量 =%2d, 对齐保证系数=%2d\n", part2, unsafe.Sizeof(part2), unsafe.Alignof(part2))
}
----------------------------------------------------------------------------------------------------------
运行结果如下:
bool 类型 所占字节数量 = 1, 对齐保证系数= 1
uint8 类型 所占字节数量 = 1, 对齐保证系数= 1
string 类型 所占字节数量 =16, 对齐保证系数= 8
int8 类型 所占字节数量 = 1, 对齐保证系数= 1
int16 类型 所占字节数量 = 2, 对齐保证系数= 2
int32 类型 所占字节数量 = 4, 对齐保证系数= 4
int64 类型 所占字节数量 = 8, 对齐保证系数= 8
float32 类型 所占字节数量 = 4, 对齐保证系数= 4
float64 类型 所占字节数量 = 8, 对齐保证系数= 8
complex64 类型 所占字节数量 = 8, 对齐保证系数= 4
complex128 类型 所占字节数量 =16, 对齐保证系数= 8
main.Part1 类型 所占字节数量 =24, 对齐保证系数= 8
main.Part2 类型 所占字节数量 =16, 对齐保证系数= 8
----------------------------------------------------------------------------------------------------------
总结:
如果一个类型 'T' 的对齐保证系数为 'N(正整数)',则该类型 'T' 的内存占用字节数必须是 'N' 的整数倍
在golang语言中,可以用 'unsafe.Sizeof(T)' 来获取类型T的所占字节数量;
用 ' unsafe.Alignof(T)' 来获取类型T的对齐保证系数;
golang的内存对齐保证系数有1,2,4,8,最大为8
如上示例:结构体part1,part2 拥有相同的字段,而字段顺序不同。
结果占用的字节数 part1为24,part2为16。part2更省内存空间,为什么会这样?
原因是 part1 花费了更多的空间去对齐内存,如下图示:
如上图所示:
由于 类型 'T' 的内存字节数必须是 内存对齐系数'N' 的整数倍
part1 用了 13 个字节去对齐内存,1+2+8+13 = 24
part2 用了 5 个字节去对齐内存,1+2+8+5 = 16
因此,在实际工作中定义结构体时,应调整好字段类型的先后顺序,使得结构体占用更小的内存空间,
这样既减少了内存碎片,还提高了服务性能。
至于具体的优化规则我没能总结出来,从大往小排列或者从小往大排列都不能涵盖所有情况,
感觉要具体情况具体分析