c 的字节对齐很重要,因为c 支持指针运算。在golang 里面一般是慎用指针运算的,所以,这部分很少用,但是有些场景为了性能不得不用到指针运算,这个时候,知道golang 的内存分配就很重要了。但是基本很少有相关的参考资料,很多也不靠谱,这里借鉴c 的规则验证golang 的内存对齐规则。
http://www.dmwan.cc/?p=154
首先,有个问题,为什么 函数 unsafe.Offsetof(A.a1) 的参数怪怪的,非得把结构体类型也传进去?
c中字节对齐规则:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的地址必须是它本身大小或对齐参数两者中较小的一个的倍数。
2、整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,整体长度必须是对齐参数和结构体最长的元素长度中较小的一个的倍数。
参考规则我们看下面这些结构的数据分配,注:64位平台,对齐参数8.
package main
import (
"fmt"
"unsafe"
)
type A struct {
a1 int8 // offset = 0
a2 int16 // offset = 1 / min(8, sizeof(int16)=2 )=2
}
// 4 / min(8, 2) 大小为4
type B struct {
b1 int16 // offset = 0
b2 int8 // offset = 2 / min(8, 1) = 2
b3 int // offset = 3 / min(8, 1) = 3
}
// [0, 1] + 2 + [3, 10]=11 / min(8, 8) 不尽,不符合规则2,大小为16,内存圆整
type C struct {
c1 int8 // offset = 0
c2 float32 // offset = 1 /min(8, 4) = 4
c3 int // offset = 8 / min(8, 8)=8
}
// 16/ min(8, 8) 大小为16
type D struct {
d1 float32 // offset = 0
d2 int // offset = 4 / min(8, 8) = 8
d3 int8 // offset = 16 / min(8, 1) = 16
}
// 17 /min(8, 8), 大小为17,数据圆整,Padding 为24 /8 = 3能整除
func main() {
var a = A{}
var b = B{}
var c = C{}
var d = D{}
// size of A = 4
// a1: 1 字节 + 1 字节padding
// a2: 2 字节
fmt.Printf("a.a1 offset %v \n", unsafe.Offsetof(a.a1))
fmt.Printf("a.a2 offset %v \n", unsafe.Offsetof(a.a2))
fmt.Printf("size of A = %d\n", unsafe.Sizeof(a))
// size of B = 16
// b1: 2 字节
// b2: 1 字节 + 5 字节padding
// b3: 8 字节
fmt.Printf("b.b1 offset %v \n", unsafe.Offsetof(b.b1))
fmt.Printf("b.b2 offset %v \n", unsafe.Offsetof(b.b2))
fmt.Printf("b.b3 offset %v \n", unsafe.Offsetof(b.b3))
fmt.Printf("size of B = %d\n", unsafe.Sizeof(b))
// size of c = 16
// c1: 1 字节 + 3 字节padding
// c2: 4 字节
// c3: 8 字节
fmt.Printf("c.c1 offset %v \n", unsafe.Offsetof(c.c1))
fmt.Printf("c.c2 offset %v \n", unsafe.Offsetof(c.c2))
fmt.Printf("c.c3 offset %v \n", unsafe.Offsetof(c.c3))
fmt.Printf("size of C = %d\n", unsafe.Sizeof(c))
// size of d = 24
// d1: 4字节 + 4字节padding
// d2: 8 字节
// d3: 1字节 + 7 字节padding
// d1的尾部padding的原因是要保证是结构体自身也是对齐的
// 因为这样可以确保实现结构体数组时候里面每个元素也是对齐的
fmt.Printf("d.d1 offset %v \n", unsafe.Offsetof(d.d1))
fmt.Printf("d.d2 offset %v \n", unsafe.Offsetof(d.d2))
fmt.Printf("d.d3 offset %v \n", unsafe.Offsetof(d.d3))
fmt.Printf("size of D = %d\n", unsafe.Sizeof(d))
// 由于有补齐,两个结构体即便有相同类型的字段,但前后顺序不同也可导致size不同
}
发现c 的内存分配规则其实和golang 是一致的。最后其实这里是不是一致对于使用来说不会有太大影响,因为指针计算,转换之前,会提前用offsetof 计算,这个偏移,golang 会将padding 都算进去, 和c 用的时候不太一样,这样做,不会出错。
这里特别提下就是不同平台和对齐参数的不一致,会导致结果完全不同。