郑文峰的博客 郑文峰的博客
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)

zhengwenfeng

穷则变,变则通,通则久
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)
  • python

  • go语言

    • go简单使用grpc
    • gin中validator模块的源码分析
    • 优化gin表单的错误提示信息
    • go中如何处理error
    • tcp缓存引起的日志丢失
    • 使用etcd分布式锁导致的协程泄露与死锁问题
    • go语言高性能编程

      • Go协程池深度解析:原理、实现与最佳实践
      • Go语言Interface Boxing原理与性能优化指南
      • Go语言遍历性能深度解析:从原理到优化实践
      • Go语言零拷贝技术完全指南
      • Go语言不可变数据共享:无锁并发编程实践
      • Go语言内存预分配完全指南
      • Go语言原子操作完全指南
      • Go语言堆栈分配与逃逸分析深度解析
      • Go语言空结构体:零内存消耗的高效编程
      • Go语言结构体内存对齐完全指南
        • 1. 简介
        • 2. 使用结构体内存对齐效果是怎么样的?
        • 3. 什么是结构体内存对齐?
        • 4. 针对结构体是否对齐的基准测试
        • 5. 兼容硬件平台与架构
        • 6. 内存对齐规则
          • 6.1 对齐系数
          • 6.2 字段偏移量
          • 6.3 struct 内存对齐规则
          • 6.4 struct{} 作为结构体最后一个字段
        • 7. fieldalignment linter
        • 8. 最佳实践
      • Go语言字符串拼接性能对比与优化指南
      • Go语言延迟初始化(Lazy Initialization)最佳实践
      • Go语言高效IO缓冲技术详解
  • linux

  • 其他

  • 编程
  • go语言
  • go语言高性能编程
zhengwenfeng
2025-06-14
目录

Go语言结构体内存对齐完全指南

# 1. 简介

在Go语言开发中,结构体内存对齐是影响程序性能和内存效率的关键因素。本文将从底层原理出发,详细解析内存对齐的工作机制及其对程序性能的实际影响。

# 2. 使用结构体内存对齐效果是怎么样的?

创建两个结构体,其中的成员变量数量和类型完成一致,只有顺序摆放的不一样。

import (
	"fmt"
	"unsafe"
)

type PoorlyAligned struct {
	flag  bool
	count int64
	id    byte
}

type WellAligned struct {
	count int64
	flag  bool
	id    byte
}

func main() {
	fmt.Println(unsafe.Sizeof(PoorlyAligned{}))
	fmt.Println(unsafe.Sizeof(WellAligned{}))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行结果如下,可以看到 PoorlyAligned 的内存大小是24,而 WellAligned 的内存大小是 16

24
16
1
2

为什么改了变量的顺序,却在内存中的大小则不一样了呢?

# 3. 什么是结构体内存对齐?

CPU 访问内存时是根据字长来访问而不是字节。比如 32 位的 CPU 字长是 4 字节,64 位的 CPU 时 8 字节。这么设计的目的是减少 CPU 访问内存的次数,提高吞吐量。

这个时候我们看到结构体 PoorlyAligned,bool是 4 字节,int64 是 8 字节,byte是 4 字节,CPU 读取 bool 变量 flag 时,因为字长是 8 字节,必须得读 8 个字节,所以必须得补上空的 4 个字节。而 int64 的 count 变量本身是 8 字节的不需要补位,读byte 的 id 时和 flag 一样也需要补 4 个字节,最终是 24 个字节。

17499021159591749902115421.png

再看到结构体 WellAligned,顺序是 count、flag、id,count 可以一次取走,而 bool 可以和 id 在同一次一起取走,所以最终只需要在内存存储 16 个字节。

17499021329591749902132218.png

# 4. 针对结构体是否对齐的基准测试

func BenchmarkPoorlyAligned(b *testing.B) {
	for range b.N {
		var items = make([]PoorlyAligned, 10_000_000)
		for j := range items {
			items[j].count = int64(j)
		}
	}
}

func BenchmarkWellAligned(b *testing.B) {
	for range b.N {
		var items = make([]WellAligned, 10_000_000)
		for j := range items {
			items[j].count = int64(j)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果如下,内存对齐在速度上要快上约 35%,内存分配要少上约 50%

$ go test -bench=. -benchmem . 
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkPoorlyAligned-12            248           4575658 ns/op        240001038 B/op         1 allocs/op
BenchmarkWellAligned-12              356           3392792 ns/op        160006157 B/op         1 allocs/op
PASS
ok      main/demo       5.126s
1
2
3
4
5
6
7
8
9

# 5. 兼容硬件平台与架构

接下来看这么一种情况,f1 int8占用 1 个字节,f2 int16占用 2 个字节,f3 int8 也占用 1 个字节,但是输出的字节大小却是 6 而不是 4,因为这里也做了内存对齐。

type S1 struct { 
	f1 int8
	f2 int16
	f3 int8
}

func main() {
	s1 := S1{}
	fmt.Println(unsafe.Sizeof(s1))
}
1
2
3
4
5
6
7
8
9
10

运行输出:

6
1

问题来了,操作系统一个字长 8 个字节,那么无论是 4 还是 6 CPU 都能够一次拿到整体数据,效率是一样的呀。

这是因为,内存对齐不仅仅是为了保证 CPU 的读取效率,还有就是为了兼容硬件平台和架构做的限制,比如在 CPU 是 ARM架构 时强制要做内存对齐的,不做的话则会直接报错。

那么这里的对齐规则是怎么样的呢?

# 6. 内存对齐规则

# 6.1 对齐系数

字段类型不仅有类型大小,还有类型对齐系数, 其有以下两个规则

  • Go原始类型的对齐系数与类型长度相等。

可以使用unsafe.Alignof来查看类型的对齐系数,可以发现两者是一致的。

func main() {
	fmt.Println(unsafe.Sizeof(int64(1)))      // 8
	fmt.Println(unsafe.Sizeof(float32(32)))   // 4

	fmt.Println(unsafe.Alignof(int64(1)))     // 8
	fmt.Println(unsafe.Alignof(float32(32)))  //4
}
1
2
3
4
5
6
7
  • Go结构体类型的对齐系数是最长字段的对齐系数和系统对齐系数两者中的最小的那个。

可以看到结构体的对齐系数是 2,这是因为最长字段是 f2 其对齐系数是 2,操作系统是 64 位,其对齐系数是 8,取最小的也就是 2

type S1 struct { 
	f1 int8
	f2 int16
	f3 int8
}

func main() {
	s1 := S1{}
	fmt.Println(unsafe.Sizeof(s1))    // 6
	fmt.Println(unsafe.Alignof(s1))   // 2
}
1
2
3
4
5
6
7
8
9
10
11

# 6.2 字段偏移量

字段偏移量是指在结构体内存对齐后字段从起始位置开始的偏移量,也就是从起始位置开始计算的字节数,第一个字段的偏移量是 0。

# 6.3 struct 内存对齐规则

对齐规则如下:

  • 结构体中的每个字段所在的偏移量必须是其对齐系数的整数倍,如果不能保证则需要添加空的字节来保证内存对齐。
  • 结构体的长度一定要是其本身对齐系数的整数倍。

知道规则后,再回到之前的 S1 的例子:

  • f1 的偏移量为0,占用 1 个字节,默认是对齐的。
  • f2 的对齐系数是2,如果从偏移量 1 开始存储,则不是2的倍数,必须前面空出一个,从偏移量2开始,才是对齐系数的倍数,所以从偏移量从2开始存储,并存储2个字节
  • f3 的对齐系数是1,从偏移量 5 开始存储,存储 1 个字节,是对齐的。
  • 本结构体的对齐系数是 2,而大小需要时对齐系数的整数倍,而根据前面几步的长度是 5,不能对齐,所以还需要空出一个字节才能使结构体对齐,也就是长度为 6 字节
type S1 struct {   // 长度:6, 对齐系数: 2
	f1 int8        // 长度:1, 对齐系数: 1
	f2 int16       // 长度:2, 对齐系数: 2
	f3 int8        // 长度:1, 对齐系数: 1
}
1
2
3
4
5

17499021449591749902144432.png

# 6.4 struct{} 作为结构体最后一个字段

struct{}这样的空结构体的大小是为 0 的,如果其作为其他结构体字段的时候,是不占用内存的,也不需要考虑内存对齐的问题,但只有一种情况例外,就是struct{}作为结构体的最后一个字段时,是会进行内存对齐的,而其对齐规则是:会被填充对齐到前一个字段的大小。

这是因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)

具体案例如下,demo1、demo2、demo3中的struct{}的都会按照前一个字段的大小进行填充对齐。

type demo1 struct {
	c int8
	a struct{}
}

type demo2 struct {
	c int16
	a struct{}
}

type demo3 struct {
	c int32
	a struct{}
}

type demo4 struct {
	a struct{}
	c int32
}

func main() {
	fmt.Println(unsafe.Sizeof(demo1{})) // 2
	fmt.Println(unsafe.Sizeof(demo2{})) // 4
	fmt.Println(unsafe.Sizeof(demo3{})) // 8
	fmt.Println(unsafe.Sizeof(demo4{})) // 4
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 7. fieldalignment linter

通过使用fieldalignment linter工具可以提前发现内存对齐问题,可以帮助我们来对结构体中的字段顺序进行正确的排列,减少内存的占用,提高运行的效率。

该结构体通过上面的分析,长度为 6,为了内存对齐是补了 2 个字节的。

type S1 struct {
	f1 int8
	f2 int16
	f3 int8
}
1
2
3
4
5

在项目中新增文件.golangci.yaml,内容如下,主要是将 fieldalignmeng 功能开启

version: 2
linters:
  default: all
  enable:
    - govet
  settings:
    govet:
      enable:
        - fieldalignment
1
2
3
4
5
6
7
8
9

然后运行golangci-lint会有提示,结构体的大小可以被优化,从 6 到 4

$ golangci-lint run
...
main.go:9:9: fieldalignment: struct of size 6 could be 4 (govet)
type S1 struct {
...
1
2
3
4
5

修改结构体中字段顺序为不需要补齐内存的方式,再次运行上面的命令后,上面的提示错误信息就没有了。

type S1 struct {
	f1 int8
	f3 int8
	f2 int16
}
1
2
3
4
5

# 8. 最佳实践

  • 通过对字段的排序来减少内部无意义的填充。
  • 尽可能将类型大小相同的字段放在一起,避免大小交替。
  • 使用 fieldalignment linter 对代码进行校验,通过工具自动捕获
#go语言#go语言高性能编程
上次更新: 2025/06/14, 16:16:07
Go语言空结构体:零内存消耗的高效编程
Go语言字符串拼接性能对比与优化指南

← Go语言空结构体:零内存消耗的高效编程 Go语言字符串拼接性能对比与优化指南→

最近更新
01
Go语言高效IO缓冲技术详解
06-14
02
Go语言延迟初始化(Lazy Initialization)最佳实践
06-14
03
Go语言字符串拼接性能对比与优化指南
06-14
更多文章>
Theme by Vdoing | Copyright © 2022-2025 zhengwenfeng | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式