go内存对齐的那些事儿(代码片段)

qcrao qcrao     2023-01-08     267

关键词:

在讨论内存对齐前我们先看一个思考题,我们都知道Go的结构体在内存中是由一块连续的内存表示的,那么下面的结构体占用的内存大小是多少呢?

type ST1 struct 
 A byte
 B int64
 C byte

在64位系统下 byte 类型就只占1字节,int64 占用的是8个字节,按照数据类型占的字节数推理,很快就能得出结论:这个结构体的内存大小是10个字节 (1 + 8 +1 )。这个推论到底对不对呢?我们让 Golang 自己揭晓一下答案。

package main

import (
 "fmt"
 "unsafe"
)

type ST1 struct 
 A byte
 B int64
 C byte



func main() 
 fmt.Println("ST1.A 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.A)))
 fmt.Println("ST1.A 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.A)))
 fmt.Println("ST1.B 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.B)))
 fmt.Println("ST1.B 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.B)))
 fmt.Println("ST1.C 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.C)))
 fmt.Println("ST1.C 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.C)))
 fmt.Println("ST1结构体 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1)))
 fmt.Println("ST1结构体 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1)))


## 输出
ST1.A 占用的字节数是:1
ST1.A 对齐的字节数是:1
ST1.B 占用的字节数是:8
ST1.B 对齐的字节数是:8
ST1.C 占用的字节数是:1
ST1.C 对齐的字节数是:1
ST1结构体 占用的字节数是:24
ST1结构体 对齐的字节数是:8

Golang 告诉我们 ST1 结构体占用的字节数是24。但是每个字段占用的字节数总共加起来确实是只有10个字节,这是怎么回事呢?

因为字段B占用的字节数是8,内存对齐的字节数也是8,A字段所在的8个字节里不足以存放字段B,所以只好留下7个字节的空洞,在下一个 8 字节存放字段B。又因为结构体ST1是8字节对齐的(可以理解为占的内存空间必须是8字节的倍数,且起始地址能够整除8),所以 C 字段占据了下一个8字节,但是又留下了7个字节的空洞。

这样ST1结构体总共占用的字节数正好是 24 字节。

既然知道了 Go 编译器在对结构体进行内存对齐的时候会在字段之间留下内存空洞,那么我们把只需要 1 个字节对齐的字段 C 放在需要 8 个字节内存对齐的字段 B 前面就能让结构体 ST1 少占 8 个字节。下面我们把 ST1 的 C 字段放在 B 的前面再观察一下 ST1 结构体的大小。

package main

import (
 "fmt"
 "unsafe"
)

type ST1 struct 
 A byte
 C byte
 B int64



func main() 
 fmt.Println("ST1.A 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.A)))
 fmt.Println("ST1.A 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.A)))
 fmt.Println("ST1.B 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.B)))
 fmt.Println("ST1.B 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.B)))
 fmt.Println("ST1.C 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1.C)))
 fmt.Println("ST1.C 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1.C)))
 fmt.Println("ST1结构体 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST1)))
 fmt.Println("ST1结构体 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST1)))


## 输出

ST1.A 占用的字节数是:1
ST1.A 对齐的字节数是:1
ST1.B 占用的字节数是:8
ST1.B 对齐的字节数是:8
ST1.C 占用的字节数是:1
ST1.C 对齐的字节数是:1
ST1结构体 占用的字节数是:16
ST1结构体 对齐的字节数是:8

重排字段后,ST1 结构体的内存布局变成了下图这样

仅仅只是调换了一下顺序,结构体 ST1 就减少了三分之一的内存占用空间。在实际编程应用时大部分时候我们不用太过于注意内存对齐对数据结构空间的影响,不过作为工程师了解内存对齐这个知识还是很重要的,它实际上是一种典型的以空间换时间的策略。

内存对齐

操作系统在读取数据的时候并非按照我们想象的那样一个字节一个字节的去读取,而是一个字一个字的去读取。

字是用于表示其自然的数据单位,也叫machine word。字是系统用来一次性处理事务的一个固定长度。

字长 / 步长 就是一个字可容纳的字节数,一般 N 位系统的字长是 (N / 8) 个字节。

因此,当 CPU 从存储器读数据到寄存器,或者从寄存器写数据到存储器,每次 IO 的数据长度是字长。如 32 位系统访问粒度是 4 字节(bytes),64 位系统的就是 8 字节。当被访问的数据长度为 n 字节且该数据的内存地址为 n 字节对齐,那么操作系统就可以高效地一次定位到数据,无需多次读取、处理对齐运算等额外操作。

内存对齐的原则是:将数据尽量的存储在一个字长内,避免跨字长的存储

Go 官方文档中对数据类型的内存对齐也有如下保证:

  1. 对于任何类型的变量 x,unsafe.Alignof(x) 的结果最小为1 (类型最小是一字节对齐的)

  2. 对于一个结构体类型的变量 x,unsafe.Alignof(x) 的结果为 x 的所有字段的对齐字节数中的最大值

  3. 对于一个数组类型的变量 x , unsafe.Alignof(x) 的结果和此数组的元素类型的一个变量的对齐字节数相等,也就是 unsafe.Alignof(x) == unsafe.Alignof(x[i])

下面这个表格列出了每种数据类型对齐的字节数

数据类型对齐字节数
bool, byte, unit8 int81
uint16, int162
uint32, int32, float32, complex644
uint64, int64, float64, complex648
array由其元素类型决定
struct由其字段类型决定, 最小为1
其他类型8

零字节类型的对齐

我们都知道 struct 类型占用的字节数是 0,但其实它的内存对齐数是 1,这么设定的原因为了保证当它作为结构体的末尾字段时,不会访问到其他数据结构的地址。比如像下面这个结构体 ST2

type ST2 struct 
 A uint32
 B uint64
 C struct

虽然字段 C 占用的字节数为0,但是编译器会为它补 8 个字节,这样就能保证访问字段 C 的时候不会访问到其他数据结构的内存地址。

type ST2 struct 
 A uint32
 B uint64
 C struct


func main() 
 fmt.Println("ST2.C 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST2.C)))
 fmt.Println("ST2.C 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST2.C)))
 fmt.Println("ST2 结构体占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST2)))


## 输出

ST2.C 占用的字节数是:0
ST2.C 对齐的字节数是:1
ST2 结构体占用的字节数是:24

当然因为 C 前一个字段 B 占据了整个字长,如果把 A 和 B 的顺序调换一下,因为 A 只占 4 个字节,C 的对齐字节数是 1, 足够排在这个字剩余的字节里。这样一来 ST2 结构体的占用空间就能减少到 16 个字节。

type ST2 struct 
 B uint64
 A uint32
 C struct


func main() 
 fmt.Println("ST2.C 占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST2.C)))
 fmt.Println("ST2.C 对齐的字节数是:" + fmt.Sprint(unsafe.Alignof(ST2.C)))
 fmt.Println("ST2 结构体占用的字节数是:" + fmt.Sprint(unsafe.Sizeof(ST2)))


## 输出
ST2.C 占用的字节数是:0
ST2.C 对齐的字节数是:1
ST2 结构体占用的字节数是:16

总结

内存对齐在我理解就是为了计算机访问数据的效率,对于像结构体、数组等这样的占用连续内存空间的复合数据结构来说:

  • 数据结构占用的字节数是对齐字节数的整数倍。

  • 数据结构的边界地址能够整除整个数据结构的对齐字节数。

这样 CPU 既减少了对内存的读取次数,也不需要再对读取到的数据进行筛选和拼接,是一种典型的以空间换时间的方法。

希望通过这篇文章能让你更了解 Go 语言也更了解内存对齐这个计算机操作系统减少内存访问频率的机制。

开发那些事儿:go与c类型转换出现内存泄漏该如何解决?

上次和大家分享了关于Go加C.free释放内存,编译出现报错情况的解决办法,今天来和大家讨论下Go与C类型转换出现内存泄漏该如何解决。我们在开发过程中发现,将Go字符串、byte切片转换为C对应的字符串、数组时࿰... 查看详情

c的那些事儿(代码片段)

...器设计者有意义。它的意思是“在进入程序块时自动进行内存分配“。其他程序员不必操心auto这个关键字,它是缺省的变量内存分配方式。表达式中的数组名可以看作是指针把数组当作指针,简化了很多东西。我们不在需要一... 查看详情

文件处理那些事儿~(代码片段)

...下来。  电脑也是一样,如果你把一个程序的变量写入内存中,当你关闭程序的时候,你的变量的信息就会丢失。如果所有的程序都是这样的话,当你在银行的ATM存款后,ATM机器重启,你的信息将全部丢失;如果你把变量的信... 查看详情

go函数的map型参数,会发生扩容后指向不同底层内存的事儿吗?(代码片段)

最近跟同事做项目,由于要在函数里向一个Map中写入不少数据,这个Map是作为参数传到函数里的。他问了我一个问题: “如果把Map作为函数参数传递,会不会像用Slice做参数时一样诡异,是不是一定要把Map当成返回值返回才能... 查看详情

python笔记——类与对象的那些事儿(代码片段)

python一切皆对象python中除了少数几个语句,其余全都是对象!创建一个类,这个类便称为类对象,也占内存空间,也有它的值。classStudent: passprint(id(Student))#2926222013040print(type(Student)) #<class'type'>pr 查看详情

python笔记——类与对象的那些事儿(代码片段)

python一切皆对象python中除了少数几个语句,其余全都是对象!创建一个类,这个类便称为类对象,也占内存空间,也有它的值。classStudent: passprint(id(Student))#2926222013040print(type(Student)) #<class'type'>pr 查看详情

go内存对齐(代码片段)

go内存对齐前言学过操作系统的人知道,OS为了CPU读取方便会一次性读取一块的单位,这个块的开大小又称为内存访问粒度。在64位系统中,这个粒度为8,也就是一次性读取8个字节。unsafe.Sizeof()//返回传入参数的大小unsafe.Alignof()/... 查看详情

glibc内存管理那些事儿(代码片段)

Linux内存空间简介32位Linux平台下进程虚拟地址空间分布如下图: 进程虚拟地址空间分布图中,0xC0000000开始的最高1G空间是内核地址空间,剩下3G空间是用户态空间。用户态空间从上到下依次为stack栈(向下增长)、mmap(匿名文... 查看详情

聊聊netty那些事儿之从内核角度看io模型(代码片段)

...有高吞吐,低延时,更少的资源消耗,高性能(非必要的内存拷贝最小化)等特征的高并发网络应用程序。本文我们来探讨下支持Netty具有高吞吐,低 查看详情

聊聊netty那些事儿之从内核角度看io模型(代码片段)

...有高吞吐,低延时,更少的资源消耗,高性能(非必要的内存拷贝最小化)等特征的高并发网络应用程序。本文我们来探讨下支持Netty具有高吞吐,低 查看详情

开发那些事儿:如何利用go单例模式保障流媒体高并发的安全性?(代码片段)

作为开发者,熟知不同语言的特性、灵活运用各种语言的结合都是开发者需要考虑的内容。TSINGSEE青犀视频的研发人员在平台开发过程中,智能分析方面用Python编译会比较多,在部分基层调用能力上则采用Golang比较多&... 查看详情

注解的那些事儿|注解的使用(代码片段)

...章首发于【博客园-陈树义】,点击跳转到原文《注解的那些事儿(三)|注解的使用》学会了如何定义自定义注解,那还要会用起来才行。其实自定义注解使用也非常简单,像我们上篇文章定义的一个Sweet注解。public@interfaceSweetSt... 查看详情

fastdfs运维友好那些事儿(代码片段)

FastDFS运维友好那些事儿本篇文章转载于FastDFS作者余庆大佬的FastDFS分享与交流公众号。FastDFS运维友好那些事儿(一)FastDFS运维友好那些事儿(二)最近有人在FastDFSQQ技术交流群里爆料,说网上有人吐槽FastDFS... 查看详情

诊断协议那些事儿(代码片段)

诊断协议那些事儿本专栏将以ISO14229、15765-2为基础深入介绍诊断那些事儿,从故障定位到软件刷写……重点掌握各个服务的功能、报文格式,为后续功能开发打下基础!提示:可参考目录索引进行学习一、UDS是什... 查看详情

ios单元测试的那些事儿(代码片段)

iOS单元测试的那些事儿作为客户端开发,很多时候我们过多的关注于功能的测试,而忽略标准的单元测试。其实,单元测试是保障项目稳定性的最有效且成本最低的测试方式。越偏向底层服务的代码,越需要使用单元测试来对可... 查看详情

linux字节对齐的那些事(代码片段)

...队列的方式传递消息(最终实现原理是中断+共享内存的方式),在实际操作过程中发现threadx总是crash,于是经过排查,是因为传递消息的结构体没有考虑字节对齐的问题。随手整理一下C语言中字节对齐的... 查看详情

字符串的那些事儿(代码片段)

一、字符串替换二、字符串排序三、删除字符串中的子串一、字符串替换描述:实现字符串"Goodmorning!"到"Goodevening!"的替换。代码实现:#include<stdio.h>char*MyReplace(char*s1,char*s2,intpos)//自定义的替换函数 inti,j; ... 查看详情

并发编程的那些事儿(代码片段)

1、生产者和消费者模型  作用:用于解耦。  原理:生产者将生产的数据放到缓存区,消费者从缓存区直接取。借助队列来实现该模型(队列就是缓存区)  队列是安全的,自带锁机制。  q=Queue(num)  num为队列的最... 查看详情