字节对齐

字节对齐 #

unsafe.Sizeof #

Sizeof 函数返回操作数在内存中的字节大小(返回该类型所占用的内存大小)。

 import "unsafe"
 

func main() {
    fmt.Println(unsafe.Sizeof("true"))                // 16
	fmt.Println(unsafe.Sizeof(true))                  // 1
	fmt.Println(unsafe.Sizeof(int8(0)))               // 1
	fmt.Println(unsafe.Sizeof(int16(10)))             // 2
	fmt.Println(unsafe.Sizeof(int32(10000000)))       // 4
	fmt.Println(unsafe.Sizeof(int64(10000000000000))) // 8
	fmt.Println(unsafe.Sizeof(int(1)))                // 8
	fmt.Println(unsafe.Sizeof(float64(0)))            // 8
}

string 的 Sizeof 始终是 16。实际上字符串类型对应一个结构体,该结构体有两个域,第一个域是指向该字符串的指针,第二个域是字符串的长度,每个域占8个字节,但是并不包含指针指向的字符串的内容。

str := "hello"
fmt.Println(unsafe.Sizeof(str)) //16

声明一个 slice,然后打印出 Sizeof 的值为24,但是不管 slice 里的元素为多少,Sizeof 返回的数据都是24。Sizeof 返回的大小是切片的描述符,而不是切片所指向的内存的大小。

slice := []int{1,2,3}
fmt.Println(unsafe.Sizeof(slice)) //24

如果换成一个数组,则指向数组占用的内存大小。

arr := [...]int{1,2,3,4,5}
fmt.Println(unsafe.Sizeof(arr)) //40
arr2 := [...]int{1,2,3,4,5,6}
fmt.Println(unsafe.Sizeof(arr2)) //48

unsafe.Alignof #

Alignof 返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。

func main() {
    var b bool
	var i8 int8
	var i16 int16
	var i64 int64

	var f32 float32

	var s string

	var m map[string]string

	var p *int32

	fmt.Println(unsafe.Alignof(b))   // 1
	fmt.Println(unsafe.Alignof(i8))  // 1
	fmt.Println(unsafe.Alignof(i16)) // 2
	fmt.Println(unsafe.Alignof(i64)) // 8
	fmt.Println(unsafe.Alignof(f32)) // 4
	fmt.Println(unsafe.Alignof(s))   // 8
	fmt.Println(unsafe.Alignof(m))   // 8
	fmt.Println(unsafe.Alignof(p))   // 8
}

结构体占用内存大小 #

import (
	"fmt"
	"unsafe"
)

type Part1 struct {
	a bool
	b int32
	c int8
	d int64
	e byte
}

func main() {
	fmt.Printf("bool size: %d\n", unsafe.Sizeof(bool(true)))
	fmt.Printf("int32 size: %d\n", unsafe.Sizeof(int32(0)))
	fmt.Printf("int8 size: %d\n", unsafe.Sizeof(int8(0)))
	fmt.Printf("int64 size: %d\n", unsafe.Sizeof(int64(0)))
	fmt.Printf("byte size: %d\n", unsafe.Sizeof(byte(0)))
	
	part1 := Part1{}
	fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}

output:

bool size: 1
int32 size: 4
int8 size: 1
int64 size: 8
byte size: 1
part1 size: 32, align: 8

Part1 这一个结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。但是实际答案却是占用 32 个字节。原因是因为 内存对齐

为什么要做内存对齐 #

  • 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况

  • 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作

对齐规则 #

  • 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取min作为当前类型的对齐值。其偏移量必须为对齐值的整数倍

  • 结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取min作为对齐值

默认对齐系数 #

在不同平台上的编译器都有自己默认的 “对齐系数”,可通过预编译命令 #pragma pack(n) 进行变更,n 就是代指 “对齐系数”。一般来讲,32 位操作系统的对齐系数是 4,64 位的对齐系数是 8

分析流程 #

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}
  • 第一个成员 a

    • 类型为 bool
    • 对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 b

    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 根据规则 1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
  • 第三个成员 c

    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c…
  • 第四个成员 d

    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 根据规则 1,其偏移量必须为 8 的整数倍。确定偏移量为 16,因此 9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五个成员 e

    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e…
  • 整体对齐:

    • 在每个成员变量进行对齐后,根据规则 2,整个结构体本身也要进行字节对齐,因为可发现它可能并不是 2^n,不是偶数倍。显然不符合对齐的规则
    • 根据规则 2,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐
  • 结果: Part1 内存布局:

    axxx|bbbb
    cxxx|xxxx
    dddd|dddd
    exxx|xxxx
        ^    ^
        32   64
    

struct 字段顺序不同,最终大小可能不同 #

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

type Part2 struct {
    e byte
    c int8
    a bool
    b int32
    d int64
}

func main() {
    part1 := Part1{}
    part2 := Part2{}

    fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
    fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2))
}

Output:

part1 size: 32, align: 8
part2 size: 16, align: 8