go语言结构体内存对齐
go语言结构体内存对齐
前言:例子引入
我们在编写代码时有时会用到结构体,在结构体中又会用到不同的数据类型,那我们在编排这些数据类型时是否考虑过顺序呢,也许很多新手在写结构体时对其所用到的数据类型都是想到什么就先写什么
比如以下两个例子:
结构体S1
type S1 struct {A int8 // size=1 align=1B int64 // size=8 align=8C int8 // size=1 align=1
}
结构体S2
type S2 struct {B int64 // size=8 align=8A int8 // size=1 align=1C int8 // size=1 align=1
}
这两个结构体的大小时一样的吗? 我们可以写一段简单的代码验证一下
package mainimport ("fmt""unsafe"
)type S1 struct {A int8 // size=1, align=1B int64 // size=8, align=8C int8 // size=1, align=1
}type S2 struct {B int64 // size=8, align=8A int8 // size=1, align=1C int8 // size=1, align=1
}func main() {// 获取 S1 和 S2 结构体的大小fmt.Println("Size of S1:", unsafe.Sizeof(S1{})) fmt.Println("Size of S2:", unsafe.Sizeof(S2{}))
}
输出结果
Size of S1: 24
Size of S2: 16
可见,他们的大小是不一样的,那为什么就一个位置不一样就会导致大小不一样呢,这个大小又会引起什么问题呢,我们就需要了解go语言中内存对齐的概念
内存对齐的简单概念
内存对齐是指在计算机内存中,为了提高访问效率和避免硬件异常,数据存储的起始地址需要满足特定的对齐规则。
这个规则要求某些类型的数据(例如 int32
、int64
等)只能存放在特定地址上,比如必须是 4 字节、8 字节等的倍数。主要目的是提高内存访问效率和减少 CPU 的内存访问延迟。大多数现代 CPU 都有缓存和高速缓存机制,如果数据没有按对齐要求存放,CPU 可能需要额外的内存访问操作,导致性能下降。
例如,如果一个 4 字节的数据被存储在非 4 字节对齐的位置,处理器可能需要两次内存访问来获取该数据,降低了效率。
内存对齐的基本规则与在go中对齐的特殊规则
通用的基本规则
1. 对齐要求
每种数据类型有一个特定的对齐要求,通常是该类型大小的倍数。常见的对齐规则如下:
- 1 字节类型(如
int8
,byte
)通常是 1 字节对齐。 - 2 字节类型(如
int16
)通常是 2 字节对齐。 - 4 字节类型(如
int32
,float32
)通常是 4 字节对齐。 - 8 字节类型(如
int64
,float64
)通常是 8 字节对齐。
2. 结构体对齐
结构体的对齐要求是其最大对齐字段的对齐值。即结构体会根据其中对齐要求最大的字段来决定对齐方式。例如,若结构体中有 int8
和 int64
,则结构体的对齐要求是 8 字节。
3. 字段对齐
- 每个字段会根据其类型的对齐要求放置在合适的位置。
- 如果字段前面有足够的空间,可以放在当前位置,否则会插入 padding(填充字节),以保证该字段在内存中按对齐要求对齐。
4. 内存布局
- 内存布局是按对齐要求从字段到字段依次排列的,如果有字段未对齐,编译器会**填充空洞(padding)**来满足对齐要求。
- 结构体的总大小会根据最大对齐值进行调整,确保整个结构体的大小是对齐值的倍数。
5. 内存对齐大小
- 结构体的 总大小 会是最大对齐值的倍数,即使有些字段没有占用整个对齐空间,也会填充 padding 来保证结构体总大小对齐。
6. 对齐与内存浪费
- 对齐通常会导致内存的 浪费,因为填充字节会导致实际使用的内存大于字段本身的大小。
Go语言中对齐的特殊规则与其他语言的不同点
除了上述讲到的默认的对齐规则,还有以下几点特殊之处
1. 自动内存对齐
Go 语言对结构体的内存对齐完全由编译器自动管理。程序员不需要显式地控制字段的对齐或插入 padding,编译器会根据字段类型自动进行对齐和内存布局。这与 C/C++ 中可能需要使用 #pragma pack
或 alignas
等指令来手动控制内存对齐的方式不同。
2. 结构体对齐与最大对齐字段相关
Go 语言的结构体对齐是基于其中 最大对齐字段的对齐要求。也就是说,结构体的对齐值是该结构体中对齐要求最大的字段的对齐值。例如:
- 如果结构体中有
int8
(1 字节对齐)和int64
(8 字节对齐),那么整个结构体的对齐值将是 8 字节。 - 这意味着,即使某些字段的对齐要求较小,结构体也会根据最大对齐值来对齐,确保字段在内存中是对齐的。
3. 不允许手动控制对齐
Go 语言没有类似于 C/C++ 中的 #pragma pack
或 alignas
等语法或指令来手动调整字段的对齐方式。这意味着 Go 在内存对齐上的行为是隐式的,由编译器决定,而程序员无法控制这一过程。这种设计使得 Go 编程更简洁,但也牺牲了一些灵活性。
4. unsafe
包的使用
尽管 Go 语言无法直接通过语法手动控制内存对齐,但可以通过 unsafe
包来访问和检查内存对齐情况。通过 unsafe.Sizeof()
,unsafe.Alignof()
,unsafe.Alignof()
等函数,开发者可以查看数据类型和结构体的大小与对齐信息,但 Go 并没有提供显式的对齐控制机制。
这三个函数能让我们查看我们定义的结构体的实际内存布局,深入了解内存对齐和字段顺序对结构体大小的影响。
1. unsafe.Sizeof()
- 功能:
unsafe.Sizeof()
用于返回一个数据类型或对象的 内存大小,单位是字节。它考虑了所有字段的实际大小,以及由于内存对齐和填充(padding)而增加的额外字节。 - 使用场景:常用于获取类型或结构体在内存中占用的字节数,帮助你分析内存布局。
2.unsafe.Offsetof()
- 功能:
unsafe.Offsetof()
用于返回结构体字段相对于结构体起始位置的 内存偏移量,单位是字节。 - 使用场景:用于分析字段在结构体中的位置,可以帮助你了解字段如何按对齐要求分配内存。
3.unsafe.Alignof()
- 功能:
unsafe.Alignof()
用于返回一个类型或结构体的 对齐要求,即该类型或结构体需要按照多少字节对齐。它返回的是类型的对齐大小(单位是字节)。 - 使用场景:用于检查一个类型的对齐要求,特别是在结构体设计和性能优化时有用。
5. 内存对齐与内存浪费
Go 语言会通过 自动填充 padding 来确保字段正确对齐。字段之间的填充可能导致内存浪费,特别是对于结构体中的小型数据类型。尽管这有助于提高内存访问效率,但在某些情况下会导致结构体占用更多内存。
6. 结构体优化
结构体字段顺序的影响:字段的顺序会影响内存布局。Go 语言推荐根据字段的对齐要求来优化结构体的字段顺序。将对齐要求大的字段放在前面,可以减少不必要的 padding,提高内存利用率。例如,将 int64
放在结构体的前面,而将 int8
放在后面,以减少字段之间的填充字节。
核心结论
- 对齐是让每个字段的起始地址满足其对齐需求。
- 每个类型有一个 对齐值(align)和 大小(size)。字段放置时会补填 padding(空洞)直到满足对齐。
- 结构体的对齐值 = 其所有字段对齐值的最大值;结构体的总大小会被向上圆整为该对齐值的倍数。
- 字段顺序会影响 padding,合理重排字段能显著减少结构体大小。
- 使用
unsafe.Sizeof/Alignof/Offsetof
可以在运行时查看实际布局。
常见类型(以 amd64 / 64-bit 为主)
注:下面的
size
/align
是在 64-bit (amd64) 平台的典型值。32-bit 下int
、uintptr
、指针、string/slice/header 大小会减半,align 上限变为 4。
bool
,int8
,uint8
,byte
— size=1, align=1int16
,uint16
— size=2, align=2int32
,uint32
,float32
,rune
— size=4, align=4int64
,uint64
,float64
— size=8, align=8complex64
— size=8, align=4 (由两个 float32 组成,对齐通常是 4)complex128
— size=16, align=8int
/uint
/uintptr
/ 指针 — size=8, align=8(在 64-bit)string
header — size=16, align=8(实际字符串数据在堆上另有地址)slice
header — size=24, align=8(ptr,len,cap (指针,长度,容量三个字段))interface{}
— size=16, align=8(type word(类型字) + data word(数据字)
(可以用 unsafe.Sizeof
/ unsafe.Alignof
验证)
例子分析
结合我们上面讲到的点,我们可以使用unsafe包来分析我们之前的例子
package mainimport ("fmt""unsafe"
)type S1 struct {A int8 // size=1, align=1B int64 // size=8, align=8C int8 // size=1, align=1
}type S2 struct {B int64 // size=8, align=8A int8 // size=1, align=1C int8 // size=1, align=1
}func main() {// 获取 S1 和 S2 结构体的大小fmt.Println("Size of S1:", unsafe.Sizeof(S1{})) // 24fmt.Println("Size of S2:", unsafe.Sizeof(S2{})) // 16// 获取 S1 和 S2 字段的偏移量fmt.Println("Offsets for S1:")fmt.Printf("A: %d\n", unsafe.Offsetof(S1{}.A)) // 0fmt.Printf("B: %d\n", unsafe.Offsetof(S1{}.B)) // 8fmt.Printf("C: %d\n", unsafe.Offsetof(S1{}.C)) // 16fmt.Println("\nOffsets for S2:")fmt.Printf("B: %d\n", unsafe.Offsetof(S2{}.B)) // 0fmt.Printf("A: %d\n", unsafe.Offsetof(S2{}.A)) // 8fmt.Printf("C: %d\n", unsafe.Offsetof(S2{}.C)) // 9// 进一步分析字段对齐fmt.Println("\nAlignment for S1:", unsafe.Alignof(S1{})) // 8fmt.Println("Alignment for S2:", unsafe.Alignof(S2{})) // 8
}
输出分析:
-
S1
的大小是 24 字节,这包括了:-
字段
A
(int8
):int8
是 1 字节,所以它被存放在结构体的第 0 个字节,地址偏移量是 0。A
完全占据了 1 字节。
字段
B
(int64
):int64
是 8 字节,并且它需要 8 字节对齐,即它的起始地址必须是 8 的倍数。- 因为结构体中的
A
占用了 1 字节,所以为了满足B
的对齐要求,结构体会在A
和B
之间插入 7 字节的 padding(填充字节),使得B
从偏移量 8 开始。 - 这样,
B
会占据从偏移量 8 到 15 的 8 字节,确保B
以 8 字节对齐。
字段
C
(int8
):C
是 1 字节,它紧接着B
后面。由于B
占用了 8 字节,C
会被放在偏移量 16 处。占用 1 字节,从偏移量 16 开始,结束位置是 16 + 1 = 17。- 然而,结构体的 总大小 需要是 8 字节对齐的倍数,因此
C
后面还会插入 7 字节的 padding,使得整个结构体的大小变为 24 字节(即对齐到 8 字节的倍数)。
-
-
S2
的大小是 16 字节,这个结构体没有额外的 padding:-
B
(8 字节)放在偏移量 0。 -
A
(1 字节)偏移量为(0~7) 8,C
(1 字节)偏移量为9 (8~16)。 -
结构体总大小为10,为了满足 8 字节的对齐要求,编译器会在结构体的末尾插入 6 字节的填充,使得结构体的总大小为 16 字节
-
所以可以得到结论:
S1 的内存布局不够紧凑,字段之间需要填充字节,导致总大小为 24 字节。
S2 的内存布局更加紧凑,字段顺序经过优化,无需填充,导致总大小为 16 字节。