golang内存对齐

为什么要内存对齐?

CPU访问内存时,以CPU的位数为单位进行访问。

如果访问未对齐的内存,处理器需要做两次内存访问,对齐的内存的访问可能仅需要一次,利用内存对齐后提升读取速度。

golang结构体内存对齐规则

在代码编译阶段,编译器会对数据的存储布局进行对齐优化。

对于golang结构体来说,在编译后,就已经确定好了结构体的大小以及各成员相对首部的偏移量。

如下两个结构体T1和T2,成员变量相同,但是成员位置不同

go 复制代码
type T1 struct {
   a int8
   b int64
   c int16
}

type T2 struct {
   a int8
   c int16
   b int64
}

func TestAlign(t *testing.T) {
   var u1 T1
   var u2 T2
   println(unsafe.Sizeof(u1)) // 24
   println(unsafe.Sizeof(u2)) // 16
}

打印内存地址

go 复制代码
u1->a的地址 0xc0002d7e40 // 占用一个字节
u1->b的地址 0xc0002d7e48 // 占用两个字节,前面填充一个字节
u1->c的地址 0xc0002d7e50 // 占用八个字节,前面填充四个字节

u2->a的地址 0xc0002d7e30 // 占用一个字节
u2->c的地址 0xc0002d7e32 // 占用两个字节,前面填充一个字节
u2->b的地址 0xc0002d7e38 // 占用八个字节,前面填充四个字节

在64位机器上执行,T2的结构体内存对齐为如下所示:

规则

结构成员需要对齐

第一个的成员相对于结构体首地址的offset = 0

非第一个成员相对于结构体首地址的 offset = min(该成员大小, 对齐值)*N倍

如有需要,编译器会在成员间加上填充字节

结构体间需要对齐

结构体的长度 = 成员中最大对齐值 * M倍

示例

rust 复制代码
bool大小占用1B,对齐值为1B
int32大小占用4B,对齐值为4B
int64大小占用8B,对齐值为8B
string大小占用16B,对齐值为8B
complex128大小占用16B,对齐值为8B

// 结构体的总大小为:max(1, 8, 1) * 4 = 32
type T1 struct {
   a bool         // 0xc000042750,offset=0,占用1字节
   b complex128   // 0xc000042758,offset=min(16,8)*1 = 8,占用16字节
   c bool         // 0xc000042768,offset=min(1,1)*24 = 24,占用1字节
}

// 结构体的总大小为:max(8, 1) * 3 = 24
type T2 struct {
   a complex128   // offset=0,占用16字节
   b bool         // offset=min(1,1)*16 = 16,占用1字节
}

// 结构体的总大小为:max(1, 2) * 2 = 4
type T3 struct {
   a bool         // offset=0,占用1字节
   b int16        // offset=min(2,2)*1 = 2,占用2字节
}

特殊字段的内存对齐

空结构体被广泛作为各种场景下的占位符使用。一是节省资源(比如利用map实现set),二是空结构体本身就具备很强的语义。

rust 复制代码
type T1 struct {
   a struct{}
   b bool
}

type T2 struct {
   a bool
   b struct{}
}

func main() {
   var u1 T1
   println(unsafe.Sizeof(u1))   // 1
   println("u1->a的地址", &u1.a) // 0xc00004276d
   println("u1->b的地址", &u1.b) // 0xc00004276d

   var u2 T2
   println(unsafe.Sizeof(u2))   // 2
   println("u2->a的地址", &u2.a) // 0xc00004276e
   println("u2->b的地址", &u2.b) // 0xc00004276f
}

空结构体字段放最后会额外占用1B内存,放在非最后位置不占用内存。

原因:

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

Hot path

hot path 是指执行非常频繁的指令序列。

访问结构体的第一个字段时,可以直接使用结构体的指针来访问第一个字段。

访问结构体的其他字段,除了结构体指针外,还需要计算偏移量。

在机器码中,偏移量是随指令传递的附加值,带上偏移量的指令序列会更长。同时CPU 还需要做一次偏移量与指针的加法运算,才能获取要访问的值的实际地址。

因此,访问第一个字段与访问其它字段相比,机器代码会更紧凑(长度短),执行速度也会更快(没有指针与偏移量的加法运算)。

示例

// sync.once中,官方解释可见英文,done使用很频繁,所以被放在结构体第一位

rust 复制代码
type Once struct {
    // done indicates whether the action has been performed.
    // It is first in the struct because it is used in the hot path.
    // The hot path is inlined at every call site.
    // Placing done first allows more compact instructions on some architectures (amd64/x86),
    // and fewer instructions (to calculate offset) on other architectures.
    done uint32
    m    Mutex
}

总结

  • 定义结构体时可以把类型相同的字段定义放一块,同时按照占用空间从小到大(或者从大到小)的顺序定义字段。
  • 结构体内嵌套空结构体时,不要放在最后一位。
  • 定义结构体时,可以考虑把常使用的字段放在第一位。
相关推荐
烛阴13 分钟前
Express + Prisma + MySQL:一站式搭建高效 Node.js 后端服务
javascript·后端·express
豌豆花下猫21 分钟前
Python 潮流周刊#97:CUDA 终于原生支持 Python 了!(摘要)
后端·python·ai
头发尚存的猿小二28 分钟前
2024年第十五届蓝桥杯C&C++大学A组--成绩统计
c语言·开发语言·c++·蓝桥杯
yumuing1 小时前
融合动态权重与抗刷机制的网文评分系统——基于优书网、IMDB与Reddit的混合算法实践
后端·算法·架构
Json_181790144801 小时前
亚马逊Amazon商品详情API接口概述,json数据示例返回(测试)
开发语言·python
bingbingyihao1 小时前
通过代码获取接口文档工具
开发语言·前端·javascript
cyz1410011 小时前
树莓派4B配置wifi热点,可访问http协议
linux·网络·windows·后端·网络协议·http·树莓派
橘子青衫1 小时前
并发编程难题:死锁、活锁、饥饿深度剖析
java·后端
DevangLic1 小时前
下【STL 之速通pair vector list stack queue set map 】
开发语言·c++·list
你又食言了哦1 小时前
qt上设置 WebAssembly显示中文
开发语言·qt·wasm