Go Unsafe Sizeof explained

Last Modified: 2023/08/11

概述

本文介绍 golang unsafe 包中的 Sizeof 方法的用法,提醒一下,不要小看 Sizeof,它没那么简单,本文将着重介绍一些常用类型的 Sizeof 计算方法。

常见类型的 Sizeof

整形和浮点型类似,这里以整形为例:

// sizeof a1 is:  1
println("sizeof a1 is: ", unsafe.Sizeof(int8(0)))
// sizeof a2 is:  2
println("sizeof a2 is: ", unsafe.Sizeof(int16(0)))
// sizeof a3 is:  4
println("sizeof a3 is: ", unsafe.Sizeof(int32(0)))
// sizeof a4 is:  8
println("sizeof a4 is: ", unsafe.Sizeof(int64(0)))

Sizeof 返回的单位是字节,int8、int16、int32 和 int64 类型的值对应的 bit 位分别为 8、16、32 和 64,换算为字节数就是 1、2、4 和 8。

Sizeof 函数可以接受任意表达式:

x := 1
// sizeof x+1 is:  8
println("sizeof x+1 is: ", unsafe.Sizeof(x+1))

int 类型有点特别,在 64 位系统上,int 使用 64 bit,因此 Sizeof 返回的值为 8,而在 32 位系统上,int 使用 32 bit,Sizeof 返回的值为 4。

Go Struct Sizeof

对于结构体而言,Sizeof 返回的值为各个字段的 Sizeof 之和以及由字段对齐(field alignment)而引入的 padding。先不用担心什么是字段对齐,先看一个不需要考虑对齐的例子:

type M struct {
	x int64
	y int64
}
m := M{x: 10, y: 10}
// sizeof m is:  16
println("sizeof m is: ", unsafe.Sizeof(m))
// sizeof m.x is:  8
println("sizeof m.x is: ", unsafe.Sizeof(m.x))
// sizeof m.y is:  8
println("sizeof m.y is: ", unsafe.Sizeof(m.y))

这里的 Sizeof(m) == Sizeof(m.x) + Sizeof(m.y) + padding,上面说到这个例子不需要考虑对齐,严格来说不是不需要考虑对齐,而是因为这里的字段恰好自动对齐了,不需要任何 padding,因此 padding 为 0。下面我们看一个需要对齐的例子:

type M struct {
	x int8
	y int64
}
m := M{x: 10, y: 10}
// sizeof m is:  16
println("sizeof m is: ", unsafe.Sizeof(m))
// sizeof m.x is:  1
println("sizeof m.x is: ", unsafe.Sizeof(m.x))
// sizeof m.y is:  8
println("sizeof m.y is: ", unsafe.Sizeof(m.y))

根据 Sizeof(m) == Sizeof(m.x) + Sizeof(m.y) + padding,可以计算出这里的 padding 为 7。那问题来了,什么是对齐(alignment),为什么需要对齐?

字段对齐保证

为了获得最佳性能,为指定类型的值分配的内存块起始地址必须对一个整数 N 进行对齐,即 N 的整数倍。Go 语言规范中,只规定了以下最小的对齐保证:

  • For a variable x of any type: unsafe.Alignof(x) is at least 1.
  • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.

简单翻译就是:任何类型变量 x, 对齐要求至少为 1;对于 struct 类型的变量 x,x 中包含的字段为 f1 到 fn,那么 unsafe.Alignof(x) = max(unsafe.Alignof(x.f1), unsafe.Alignof(x.f2), ..., unsafe.Alignof(x.fn)),换句话说 struct 类型的变量 x 的对齐要求为 x 的所有字段的对齐要求的最大值;数组类型的变量 x 的对齐要求是由数组中元素类型的对齐要求决定的。

以下是 Go 标准编译器(1.20)的对齐保证:

type 对齐保证
bool, uint8, int8 1
uint16, int16 2
uint32, int32 4
float32, complex64 4
arrays 由元素类型决定
structs 由字段类型决定
其他类型 由平台 WORD 大小决定,即 64 位系统为 8,32 位系统为 4

另外需要记住一点,编译器将会保证一个类型的值的大小一定为该类型对齐保证的倍数,即 Sizeof(x) = N * Alignof(x)。

现在让我们回到上面的例子,为了方便大家阅读,这里将代码再贴一便:

type M struct {
	x int8
	y int64
}
m := M{x: 10, y: 10}

如果不考虑对齐,Sizeof(m) = Sizeof(m.x) + Sizeof(m.y) = 1 + 8 = 9。从上面对齐要求我们知道 m 的对齐要求为 max(unsafe.Alignof(m.x), unsafe.Alignof(m.y)) = max(1, 8) = 8。x 的对齐要求为 1, y 的对齐要求为 8。

一个结构体的值分配的内存是连续的,第一个字段总是对齐的,由于 y 的对齐要求为 8,为了对齐 y, x 到 y 之间的 7 个字节的空间被用作 padding。最终 Sizeof(m) = Sizeof(m.x) + Sizeof(m.y) + padding = 1 + 8 + 7 = 16。根据 Sizeof(x) = N * Alignof(x),可以得出这里的 N 为 2。

画个示意图供大家参考:

再看一个例子:

type M struct {
	x int8
	y int64
	z int8
}

这里的 Sizeof(M{}) = 24,为什么?为了对齐 y,x 和 y 之间需要填充 7 个 padding。z 是对齐的但是为了保证 Sizeof(x) = N * Alignof(x) 仍然需要 7 个 padding。现在将 y 和 z 调个位置,结果是会有不同吗?

type M struct {
	x int8
	z int8
	y int64
}
m := M{}
println(unsafe.Sizeof(m)) // 16

现在 Sizeof(m) = 16,x 和 z 都是 1 字节对齐的,因此 x 和 z 之间不要任何 padding,y 是 8 字节对齐的,因此 z 和 y 之间需要 6 个 padding。最终 Sizeof(m) = Sizeof(m.x) + Sizeof(m.z) + Sizeof(m.y) + padding = 1 + 1 + 8 + 6 = 16。

Array Sizeof

println(unsafe.Sizeof([2]int8 {1, 2})) // 2
println(unsafe.Sizeof([2]int32 {1, 2})) // 8
println(unsafe.Sizeof([2]int64 {1, 2})) // 16

上面说到数组的 Sizeof 由数组的元素类型决定,Sizeof(array) = n * Sizeof(element),n 是数组的元素个数。

String Sizeof

println(unsafe.Sizeof("")) // 16
println(unsafe.Sizeof("abc")) // 16

不论多长的 string,输出总是 16。这又是为什么呢?要理解这个需要知道 string 类型的数据结构,在 runtime/string.go 源码中:

type stringStruct struct {
	str unsafe.Pointer //字符串的首地址
	len int // 字符串的长度
}

根据这个 struct 定义,我们知道任何 string 在 64 位系统中的 Sizeof 的值一定是 Sizeof(str) + Sizeof(len) = 8 + 8 = 16。注:unsafe.Pointer 类型是指针类型,在 64 位系统上,占 8 个字节,32 系统上占 4 字节。

通过 string 的 Sizeof,我们可以得出一个结论:Sizeof 的返回值仅包含变量自身占用的内存空间,不包含其引用的其他数据的内存空间。

Slice SizeOf

同样的,runtime/slice.go 中,slice 的结构体定义如下:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

Sizeof(aSlice) = Sizeof(array) + Sizeof(len) + Sizeof(cap) = 8 + 8 + 8 = 24。

Pointer Sizeof

在 string 的 Sizeof 中已经介绍,不再赘述。

有问题吗?点此反馈!

温馨提示:反馈需要登录