一文掌握 Go 内存对齐

embedded/2024/10/18 12:33:01/

往期精选文章推荐:

  1. 深入理解 go map

  2. go 常用关键字

  3. 深入理解 Go 数组、切片、字符串

  4. 深入理解channel

  5. 深入理解 go context

  6. 深入 go interface 底层原理

  7. 深入理解 go reflect

  8. 深入理解 go unsafe

前言

在前面的文章 《深入理解 go reflect》和 《深入理解 go unsafe》 中都提到内存对齐,由于篇幅原因没有展开详细介绍,后面有同学私聊我想了解一下内存对齐的细节,今天和大家简单聊聊内存对齐。

为什么要内存对齐

下面是维基百科对内存对齐的描述:

现代计算机CPU一般是以32位元或64位元大小作地址对齐,以32位元架构的计算机举例,每次以连续的4字节为一个区间,第一个字节的位址位在每次CPU抓取资料大小的边界上,除此之外,如果要访问的变量没有对齐,可能会触发总线错误。

总的来说,内存对齐有两点好处:

  1. 方便CPU读写内存(性能更好)

    现代计算机体系结构通常以特定的字节边界来访问内存。如果数据的存储地址没有按照合适的边界对齐,可能会导致多次内存访问,降低程序的执行效率。例如,对于一个 32 位的整数,如果存储在非 4 字节对齐的地址上,可能需要两次内存读取操作才能获取完整的数据,而在对齐的地址上,一次读取操作即可。

如果没有内存对齐变量 b 需要两次读取

  1. 平台兼容性

    不同的硬件平台和操作系统对内存对齐的要求可能不同。通过遵循合适的内存对齐规则,可以确保程序在不同的环境下都能正确运行,提高程序的可移植性。

内存对齐规则

内存对齐是一种将数据在内存中进行排列的方式,目的是提高内存访问的效率和保证数据的完整性。数据的内存排列方式最直观的体现就是数据的内存地址,说白了就是数据的内存地址要符合一定规则,以便于CPU读取,这个规则就是下面要讲的内存对齐规则。

默认对齐值

在不同的平台上的编译器有自己默认的"对齐值", 可以通过预编指令 #pragmapack(n) 进行变更,n 就是代指"对齐系数"。一般来讲,常用的平台对齐系数是: 32位是 4,64位是 8。

基本类型的对齐

在 Go 语言中,基本数据类型通常按默认对齐值和数据自身大小的最小值进行对齐,也就是说数据的内存地址必须是默认对齐值和数据大小的最小值的整数倍。

使用伪代码表示一下:

// 64 位平台,默认对齐值 8
const defaultAlign int = 8func GetAlign(a any) int {return min(defaultAlign, sizeOf(a))
}

常见的基本类型对齐值:

数据类型自身大小32位平台对齐值64位平台对齐值
int、uint4 or 848
int32、uint32444
int64、uint64848
int8、uint8111
int16、uint16222
float32444
float64848
bool111

基本类型的对齐示例:

package mainimport ("unsafe""fmt"
)func main() {var a int16 = 1var b bool = truevar c int = 1var d bool = truevar e float32 = 3.14fmt.Printf("a 的对齐值:%d, 地址: %d\n", unsafe.Alignof(a), uintptr(unsafe.Pointer(&a)))fmt.Printf("b 的对齐值:%d, 地址: %d\n", unsafe.Alignof(b), uintptr(unsafe.Pointer(&b)))fmt.Printf("c 的对齐值:%d, 地址: %d\n", unsafe.Alignof(c), uintptr(unsafe.Pointer(&c)))fmt.Printf("d 的对齐值:%d, 地址: %d\n", unsafe.Alignof(d), uintptr(unsafe.Pointer(&d)))fmt.Printf("e 的对齐值:%d, 地址: %d\n", unsafe.Alignof(e), uintptr(unsafe.Pointer(&e)))
}$ go run main.go
a 的对齐值:2, 地址: 824634150562
b 的对齐值:1, 地址: 824634150561
c 的对齐值:8, 地址: 824634150568
d 的对齐值:1, 地址: 824634150560
e 的对齐值:4, 地址: 824634150564

上面的输出和我们的结论是契合的,内存地址都是对齐值的倍数,另外还有一点,为了减少内存开销,编译器还会优化内存布局,减少内存碎片。

数组的对齐

  1. 如果数组中的元素是基本类型,那么数组的对齐值通常与元素类型的对齐值相同。例如,如果数组中的元素是int16类型,int16通常是 2 字节对齐,那么这个数组的对齐值也是 2 字节。
package mainimport ("unsafe""fmt"
)func main() {var arr [3]int16 = [3]int16{1,2,3}fmt.Printf("arr 的对齐值:%d\n", unsafe.Alignof(arr))fmt.Printf("arr 的大小: %d\n", unsafe.Sizeof(arr))fmt.Printf("arr 的地址: %d\n", uintptr(unsafe.Pointer(&arr)))
}$ go run main.go
arr 的对齐值:2
arr 的大小: 6
arr 的地址: 824634150682
  1. 如果数组中的元素是结构体类型,那么数组的对齐值通常是结构体中最大字段对齐值的倍数。例如,如果结构体中有一个int64字段和一个bool字段,在 64 位系统上,int64通常是 8 字节对齐,bool通常是 1 字节对齐,那么这个结构体的对齐值可能是 8 字节。如果这个结构体组成的数组,其对齐值也会是 8 字节。
package mainimport ("unsafe""fmt"
)type s struct {a boolb int64
}func main() {var arr [3]s = [3]s{s{},s{},s{}}fmt.Printf("arr 的对齐值:%d\n", unsafe.Alignof(arr))fmt.Printf("arr 的大小: %d\n", unsafe.Sizeof(arr))fmt.Printf("arr 的地址: %d\n", uintptr(unsafe.Pointer(&arr)))
}$ go run main.go
arr 的对齐值:8
arr 的大小: 48
arr 的地址: 824634216176

结构体的对齐

  1. 结构体中的字段会按照各自的类型进行对齐。每个字段的起始地址必须是其类型对齐值的整数倍。
package mainimport ("unsafe""fmt"
)type S struct {a boolb int32
}func main() {bar := S{}fmt.Printf("bar.b 的对齐值:%d\n", unsafe.Alignof(bar.b))fmt.Printf("bar.b 的偏移量: %d\n", unsafe.Offsetof(bar.b))
}$ go run main.go
bar.b 的对齐值:4
bar.b 的偏移量: 4
  1. 结构体本身也有一个对齐值,这个对齐值是结构体中最大字段对齐值的倍数。整个结构体的大小必须是其对齐值的整数倍。
package mainimport ("unsafe""fmt"
)type S struct {a int64b bool
}func main() {bar := S{}fmt.Printf("bar 的对齐值: %d\n", unsafe.Alignof(bar))fmt.Printf("bar 的大小: %d\n", unsafe.Sizeof(bar))
}$ go run main.go
bar 的对齐值: 8
bar 的大小: 16

数据结构优化

为了实现内存对齐,编译器有时候会在结构体的字段之间或者结构体末尾进行填充,这样会带来两个问题:

  1. 影响性能:由于字段之间填充了空置的内存,会导致CPU读取结构体数据时访问内存的次数会增加。

  2. 浪费内存:浪费内存就更好理解,填充的空置内存是无法被程序利用的,就白白浪费掉了。

所以我们设计结构体时应该注意内存对齐对性能的影响,尽量减少填充空置内存,下面两个结构体拥有相同类型的字段,只因字段顺序的不同就造成很大性能差异:

// 需要填充 9 个字节
type S1 struct {a boolb int64c int16d int32
}// 只需要填充 1 个字节
type S2 struct {a boolb int16c int32d int64
}func Benchmark_S1(b *testing.B) {b.ResetTimer()for i := 0; i < b.N; i++ {_ = make([]S1, b.N)}
}func Benchmark_S2(b *testing.B) {b.ResetTimer()for i := 0; i < b.N; i++ {_ = make([]S2, b.N)}
}>>> go test -run='^$' -bench=. -count=1 -benchtime=100000x -benchmem
goos: darwin
goarch: amd64
pkg: go_test/unsafe/align
Benchmark_S1-12           100000             66230 ns/op         2400267 B/op          1 allocs/op
Benchmark_S2-12           100000             45750 ns/op         1605655 B/op          1 allocs/op
PASS
ok      go_test/unsafe/align    11.736s

http://www.ppmy.cn/embedded/100336.html

相关文章

粘包和抓包工具

在网络传输中&#xff0c;"粘包"&#xff08;TCP粘包&#xff09;是指使用TCP协议进行数据传输时&#xff0c;发送方发送的多个数据包被接收方一次性接收或者接收方一次读取操作获取到了发送方多次发送的数据。这种情况通常发生在使用TCP协议的socket编程中。 TCP是…

pikachu-ssrf_redis

目录 SSRF 1、SSRF漏洞介绍&#xff1a; 2、SSRF漏洞原理&#xff1a; 3、SSRF漏洞利用手段&#xff1a; 4、SSRF漏洞绕过方法&#xff1a; SSRF(curl)用法 1、通过网址访问链接 2、利用file协议查看本地文件 3、dict协议扫描内网主机开放端口 4.gopher&#xff1a;威…

《javaEE篇》--定时器

定时器概念 当我们不需要某个线程立刻执行&#xff0c;而是在指定时间点或指定时间段之后执行&#xff0c;假如我们要定期清理数据库里的一些信息时&#xff0c;如果每次都手动清理的话就太麻烦&#xff0c;所以就可以使用定时器。定时器就可以比作一个闹钟&#xff0c;可以让…

Image Stride(内存图像行跨度)

When a video image is stored in memory, the memory buffer might contain extra padding bytes after each row of pixels. The padding bytes affect how the image is store in memory, but do not affect how the image is displayed. 当视频图像存储在内存时&#xff0…

object.defineProperty用法

Object.defineProperty 是 JavaScript 中一个用于定义对象属性的静态方法。它允许你精确控制对象的属性&#xff0c;包括属性的值、可写性、可枚举性和可配置性等特性。这个方法在需要定义特殊的属性行为时非常有用&#xff0c;例如&#xff0c;在 Vue.js 中&#xff0c;它常用…

OD C卷 - Wonderland游乐园

Wonderland游乐园&#xff08;200&#xff09; 游乐园有四种售票方式&#xff0c;分别为一日票&#xff0c;三日票&#xff0c;周票&#xff08;7天&#xff09;、月票&#xff08;30天&#xff09;&#xff1b;每种票据在时限内可以无限制游玩&#xff0c;如第10天买了一个三…

通过建模走出人工智能寒冬

很多人对 GenAI 是否会产生商业影响持怀疑态度&#xff0c;但我认为他们不仅错了&#xff0c;而且犯了 2001 年人们在互联网上犯下的错误。他们认为硅谷的炒作是无稽之谈&#xff0c;因此其背后的想法也是无稽之谈。 这是很危险的&#xff0c;我认为&#xff0c;这比大多数零售…

DC-DC开关电源稳压电路设计——7- 40V转换5V和3.3V

本篇文章记录分享DC-DC开关电源稳压&#xff08;7-40V转换5V和3.3V&#xff09;电路设计的思路及原理图。 目录 一、电路稳压原理图 二、开关稳压芯片 1、BUCK降压电路 2.LM2596 &#xff08;1&#xff09;、LM2596简介 &#xff08;2&#xff09;、LM2596原理 1. 基…