Go语言内存对齐详解

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

前面有篇文章我们学习了 Go语言空结构体,最近又在看 unsafe包 的知识,在查阅相关资料时不免会看到内存对齐相关的内容,虽然感觉这类知识比较底层,但是看到了却不深究和渣男有什么区别?虽然我不会,但我可以学🐶,那么这篇文章,我们就一起来看下什么是内存对齐吧!

说明:本文中的测试示例,均是基于Go1.17 64位机器

基础知识

在Go语言中,我们可以通过 unsafe.Sizeof(x) 来确定一个变量占用的内存字节数(不包含 x 所指向的内容的大小)。

例如对于字符串数组,在64位机器上,unsafe.Sizeof() 返回的任意字符串数组大小为 24 字节,和其底层数据无关:

func main() {
    s := []string{"1", "2", "3"}
    s2 := []string{"1"}
    fmt.Println(unsafe.Sizeof(s))  // 24
    fmt.Println(unsafe.Sizeof(s2)) // 24
}

对于Go语言的内置类型,占用内存大小如下:

类型 字节数
bool 1个字节
intN, uintN, floatN, complexN N/8 个字节 (int32 是 4 个字节)
int, uint, uintptr 计算机字长/8 (64位 是 8 个字节)
*T, map, func, chan 计算机字长/8 (64位 是 8 个字节)
string (data、len) 2 * 计算机字长/8 (64位 是 16 个字节)
interface (tab、data 或 _type、data) 2 * 计算机字长/8 (64位 是 16 个字节)
[]T (array、len、cap) 3 * 计算机字长/8 (64位 是 24 个字节)
func main() {
    fmt.Println(unsafe.Sizeof(int(1)))                  // 8
    fmt.Println(unsafe.Sizeof(uintptr(1)))                    // 8
    fmt.Println(unsafe.Sizeof(map[string]string{}))       // 8
    fmt.Println(unsafe.Sizeof(string("")))                    // 16
    fmt.Println(unsafe.Sizeof([]string{}))                    // 24

    var a interface{}
    fmt.Println(unsafe.Sizeof(a))                                   // 16
}

看个问题

基于上面的理解,那么对于一个结构体来说,占用内存大小就应该等于多个基础类型占用内存大小的和,我们就结合几个示例来看下:

type Example struct {
    a bool // 1个字节
    b int    // 8个字节
    c string // 16个字节
}

func main() {
    fmt.Println(unsafe.Sizeof(Example{})) // 32
}

Example 结构体的三个基础类型,加起来一个 25字节,但是最终输出的却是 32字节

我们再看两个结构体,即使这两个结构体包含的字段类型一致,但是顺序不一致,最终输出的大小也不一样

type A struct {
    a int32
    b int64
    c int32
}

type B struct {
    a int32
    b int32
    c int64
}

func main() {
    fmt.Println(unsafe.Sizeof(A{})) // 24
    fmt.Println(unsafe.Sizeof(B{})) // 16
}

是什么导致了上述问题的呢,这就引出了我们要看的知识点:内存对齐

什么是内存对齐

我们知道,在计算机中访问一个变量,需要访问它的内存地址,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是:在访问特定类型变量的时候通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置有限制,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

为什么需要内存对齐

  1. 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了。

  2. CPU 访问内存时并不是逐个字节访问,而是以字长(word size)为单位访问,例如 32位的CPU 字长是4字节,64位的是8字节。如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量

假设每次访问的步长为4个字节,如果未经过内存对齐,获取b的数据需要进行两次内存访问,最后再进行数据整理得到b的完整数据:

image-20220313230839425

如果经过内存对齐,一次内存访问就能得到b的完整数据,减少了一次内存访问:

image-20220313231143302

unsafe.AlignOf()

unsafe.AlignOf(x) 方法的返回值是 m,当变量进行内存对齐时,需要保证分配到 x 的内存地址能够整除 m。因此可以通过这个方法,确定变量x 在内存对齐时的地址:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
  • 对于 struct 结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f)unsafe.Alignof(x) 等于其中的最大值。
  • 对于 array 数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。

对于系统内置基础类型变量 x ,unsafe.Alignof(x) 的返回值就是 min(字长/8,unsafe.Sizeof(x)),即计算机字长与类型占用内存的较小值:

func main() {
  fmt.Println(unsafe.Alignof(int(1))) // 1 -- min(8,1)
  fmt.Println(unsafe.Alignof(int32(1))) // 4 -- min (8,4)
    fmt.Println(unsafe.Alignof(int64(1))) // 8 -- min (8,8)
  fmt.Println(unsafe.Alignof(complex128(1))) // 8 -- min(8,16)
}  

内存对齐规则

我们讲内存对齐,就是把变量放在特定的地址,那么如何计算特定地址呢,这就涉及到内存对齐规则:

  • 成员对齐规则

针对一个基础类型变量,如果 unsafe.AlignOf() 返回的值是 m,那么该变量的地址需要 被m整除 (如果当前地址不能整除,填充空白字节,直至可以整除)。

  • 整体对齐规则

针对一个结构体,如果 unsafe.AlignOf() 返回值是 m,需要保证该结构体整体内存占用是 m的整数倍,如果当前不是整数倍,需要在后面填充空白字节。

通过内存对齐后,就可以在保证在访问一个变量地址时:

  1. 如果该变量占用内存小于字长:保证一次访问就能得到数据;
  2. 如果该变量占用内存大于字长:保证第一次内存访问的首地址,是该变量的首地址。

举个例子

例1:

type A struct {
    a int32
    b int64
    c int32
}

func main() {
    fmt.Println(unsafe.Sizeof(A{1, 1, 1}))  // 24
}
  1. 第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:
成员变量1内存对齐
  1. 第二个字段是 int64 类型,unsafe.Sizeof(int64(1)) = 8,内存占用为 8 个字节,同时unsafe.Alignof(int64(1)) = 8,需保证变量放置首地址可以被8整除,当前地址为4,距离4最近的且可以被8整除的地址为8,因此需要添加四个空白字节,从8开始放置:
成员变量2内存对齐
  1. 第三个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为16,16可以被4整除:
成员变量3内存对齐
  1. 所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(A{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 20个字节,因此需要再补充4个字节:
整体对齐
  1. 最终该结构体的内存占用为 24字节。

例二:

type B struct {
    a int32
    b int32
    c int64
}

func main() {
    fmt.Println(unsafe.Sizeof(B{1, 1, 1}))  // 16
}
  1. 第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:
成员变量1内存对齐
  1. 第二个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为4,4可以被4整除:
成员变量2内存对齐
  1. 第三个字段是 int64 类型,unsafe.Sizeof(int64(1))=8,内存占用为8个字节,同时unsafe.Alignof(int64(1)) = 8,内存对齐需保证变量首地址可以被8整除,当前地址为8,8可以被8整除:
成员变量3内存对齐
  1. 所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(B{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 16个字节,已经符合规则,最终该结构体的内存占用为 16个字节。

空结构体的对齐规则

如果空结构体作为结构体的内置字段:当变量位于结构体的前面和中间时,不会占用内存;当该变量位于结构体的末尾位置时,需要进行内存对齐,内存占用大小和前一个变量的大小保持一致。

type C struct {
    a struct{}
    b int64
    c int64
}

type D struct {
    a int64
    b struct{}
    c int64
}

type E struct {
    a int64
    b int64
    c struct{}
}

type F struct {
    a int32
    b int32
    c struct{}
}

func main() {
    fmt.Println(unsafe.Sizeof(C{})) // 16
    fmt.Println(unsafe.Sizeof(D{})) // 16
    fmt.Println(unsafe.Sizeof(E{})) // 24
  fmt.Println(unsafe.Sizeof(F{})) // 12
}

总结

本篇文章我们一起学习了Go 语言中的内存对齐,主要内容如下:

  • unsafe.Sizeof(x) 返回了变量x的内存占用大小
  • 两个结构体,即使包含变量类型的数量相同,但是位置不同,占用的内存大小也不同,由此引出了内存对齐
  • 内存对齐包含成员对齐和整体对齐,与 unsafe.AlignOf(x) 息息相关
  • 空结构体作为成员变量时,是否占用内存和所处位置有关
  • 在实际开发中,我们可以通过调整变量位置,优化内存占用(一般按照变量内存大小顺序排列,整体占用内存更小)

更多

个人博客: https://lifelmy.github.io/

微信公众号:漫漫Coding路

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • 问题 在开始之前,希望你计算一下 Part1 共占用的大小是多少呢? 输出结果: 这么一算,Part1 这一个结构...
    羋学僧阅读 517评论 3 2
  • 原文地址:在 Go 中恰到好处的内存对齐 问题 在开始之前,希望你计算一下 Part1 共占用的大小是多少呢? 输...
    EDDYCJY阅读 1,114评论 1 11
  • Go语言中的关键字 Go 语言语法简明,所有关键字如下: 包:import package 程序实体声明和定义:...
    wayyyy阅读 129评论 0 0
  • 在组内学习go语言规范时,学习到了一个很有意思并且能减少内存分配提高性能的东西,先通过一个简单的问题向大家展示一下...
    左小星阅读 581评论 0 0
  • 无论什么语言,类型都涉及到了编程语法的方方面面。加强对于类型和指针的理解,对于提高编程水平十分关键。本文会主要讲解...
    没我找不到电子书阅读 1,553评论 0 1