字节对齐 #
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