为什么要内存对齐?

一. 什么是内存对齐(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

因此,在实际工作中定义结构体时,应调整好字段类型的先后顺序,使得结构体占用更小的内存空间,
这样既减少了内存碎片,还提高了服务性能。

至于具体的优化规则我没能总结出来,从大往小排列或者从小往大排列都不能涵盖所有情况,
感觉要具体情况具体分析

你可能感兴趣的:(为什么要内存对齐?)