从CPU角度理解Go中的结构体内存对齐

大家好,我是「Go学堂」的渔夫子。今天跟大家聊聊结构体字段内存对齐相关的知识点。

原文链接:https://mp.weixin.qq.com/s/H3399AYE1MjaDRSllhaPrw

大家在写Go时有没有注意过,一个struct所占的空间不见得等于各个字段加起来的空间之和,甚至有时候把字段的顺序调整一下,struct的所占空间又有不同的结果。

本文就从cpu读取内存的角度来谈谈内存对齐的原理。

01 结构体字段对齐示例

我们先从一个示例开始。T1结构体,共有3个字段,类型分别为int8,int64,int32。所以变量t1所属的类型占用的空间应该是1+8+4=13字节。但运行程序后,实际上是24字节。和我们计算的13字节不一样啊。如果我们把该结构体的字段调整成T2那样,结果是16字节。但和13字节还是不一样。这是为什么呢?

<pre data-language="go" id="gT3No" class="ne-codeblock language-go" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">type T1 struct {
f1 int8 // 1 byte
f2 int64 // 8 bytes
f3 int32 // 4 bytes
}

type T2 struct {
f1 int8 // 1 byte
f3 int32 // 4 bytes
f2 int64 // 8 bytes
}

func main() {
fmt.Println(runtime.GOARCH) // amd64

t1 := T1{}
fmt.Println(unsafe.Sizeof(t1)) // 24 bytes

t2 := T2{}
fmt.Println(unsafe.Sizeof(t2)) // 16 bytes

}
</pre>

02 CPU按字的方式从内存读取数据

在买电脑时或看自己电脑的属性时,都会发现CPU的规格是64位或32位的,近些年的都是64位的。而这64位指的就是CPU一次可以从内存中读取64位的数据,即8个字节。这个长度也称为CPU的字长(注意这里和字节的区别,字节是固定的8位,而字长随着CPU的规格变化,32位的字长是4字节,64位的字长是8字节)。

虽然CPU一次可以抓取8字节,但也是想从哪里抓就从哪里抓取的。因为内存也会以8字节为单位分成一个一个的字(如下图),而CPU一次只能拿某一个字。所以,如果所需要读取的数据正好跨了两个字,那就得花两个CPU周期的时间去读取了。

image

03 struct字段内存对齐

了解了CPU从内存读取数据是按块读取的之后,我们再来看看开头的T1结构体各字段在内存中如果紧密排列的话会是怎么样的。在T1结构体中各字段的顺序是按int8、int64、int32定义的,所以把各字段在内存中的布局应该形如下面这样:因为第2个字段需要8字节,所以会有一个字节的数据排列到第2个字中。

image

那这样排列会有什么问题呢?如果我们的程序想要读取t1.f2字段的数据,那CPU就得花两个时钟周期把f2字段从内存中读取出来,因为f2字段分散在两个字中。

所以,为了能让CPU可以更快的存取到各个字段,Go编译器会帮你把struct结构体做数据的对齐。所谓的数据对齐,是指内存地址是所存储数据大小(按字节为单位)的整数倍,以便CPU可以一次将该数据从内存中读取出来。编译器通过在T1结构体的各个字段之间填充一些空白已达到对齐的目的。

重新排列后,内存的布局会长如下这样,有13个字节的空间是真正存储数据的,而深色的11个字节的空间则是为了对齐而填充上的,不存储任何数据,以确保每个字段的数据都会落到同一个字长里面,所以才会有了开头的13个字节的数据类型实际上变成了24字节。

image

04 如何减少struct的填充

虽然通过填充的方式可以提高CPU读写数据的效率,但这些填充的内存实际上是不存数任何数据的,也就相当于浪费掉了。以T1结构体为例,实际存储数据的只有13字节,但实际用了24字节,浪费了将近一半,那有没有什么办法既可以做到内存对齐提高CPU读取效率又能减少内存浪费的吗?

答案就是调整struct字段的顺序。我们再观察下T1结构体的字段分布,就会发现下面的f3字段的4个字节可以挪到f1字段所在的那一排的填充位置,毕竟第一排的填充空间超大,不用也是浪费,而且挪上去之后,每个字段还是都在同一个字里面。

image

一旦把 f3 移上去,就可以省掉最下面一整個 word(8 bytes) 的空间,所以 T2 整个 struct 就只需要 16 bytes,是原本 T1 24 bytes 的三分之二。

image

在Go程序中,Go会按照结构体中字段的顺序在内存中进行布局,所以需要将字段f2和f3的位置交换,定义的顺序变成int8、int32、int64,这样Go编译器才会顺利的按上图那样排列。

05 在同一个字(8字节)中的内存分布

上面都是看到的跨字长(64位系统下是8字节)的存储示例来说明CPU需要从内存读取两次才能将一个完整的数据完整的读取出来。那如果是有n个小于一个字长的类型在同一个字长中是否可以连续分配呢?我们通过示例来讲解一下:

<pre data-language="go" id="Q6rDB" class="ne-codeblock language-go" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">var x struct {
a bool
b int16
}</pre>

我们计算一下各个字段占用的内存空间:1字节(a)+2字节(b)= 3字节。没超过1个字长(8字节),但在内存中的分布是如下图这样:

image

我们发现b并没有直接在a的后面,而是在a中填充了一个空白后,放到了偏移量为2的位置上。为什么呢?

答案还是从内存对齐的定义中推导出来。我们上面说过,内存对齐是指数据存放的地址是数据大小的整数倍。也就是说会有数据存放的起始地址%数据的大小=0

我们来验证下上面的结构体的排列。假设结构体的起始地址为0,那么a从0开始占用1个字节。b字段如果放在地址1处,套用上面的公式 1 % 2 = 1,就不满足对齐的要求。所以在地址为2处开始存放b字段。 这也就解释了很多文章中列出的原则:构体变量中成员的偏移量必须是成员大小的整数倍

06 什么时候该关注结构体字段顺序

由此可知,对结构体字段的重新排列会让结构体更节省内。但我们需要这么做吗?以Student为例,我们看下Student的定义:

<pre data-language="go" id="cAVbq" class="ne-codeblock language-go" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">type Student struct {
id int8 //学号
name string //姓名
classID int8 //班级
phone [10]byte //联系电话
address string // 地址
grade int32 //成绩
}</pre>

我们看下该结构体在内存中的分布如下,可以看到有很多深色的填充空间,总计浪费了16字节,好像还可以优化。

image

我们通过调整Student结构体的字段顺序来进行下优化,可以看到从开始的64字节,可以优化到48字节,共剩下了25%的空间。

<pre data-language="go" id="sC6Ol" class="ne-codeblock language-go" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">type Student struct {

name string //姓名
address string // 地址
grade int32 //成绩
phone [10]byte //联系电话
id int8 //学号
classID int8 //班级  

}</pre>

调整后的结构体在内存中的分布如下:

image

我们看到,通过调整结构体中的字段顺序确实节省了内存空间,那我们真的有必要这样节省空间吗?

以student结构体为例,经过重新排列后,节省了16字节的空间,假设我们在程序中需要排列全校同学的成绩,需要定义一个长度为10万的Student类型的数组,那剩下的内存也不过16MB的空间,跟现在个人电脑的8G到16G的内存比起来微不足道。而且在字段重新排列后,可读性也变的很差了。像Student原本是以学号,姓名,班级...这样依次排列的,而重新调整后变成了姓名,地址,成绩...,一直到最后才是学号跟班级,不符合人们的思维习惯。

所以,我的建议是对于结构体的字段排列不需要过早的进行优化,除非一开始就知道你的程序瓶颈就卡在这里。否则,就按照正常的习惯编写Go程序即可。

07 总结

本文从CPU读取内存的角度分析了为什么需要进行数据对齐。该文目的是为了让你更好的了解底层的运行机制,而非时刻关注结构体的字段顺序。在编写代码时顺其自然就好。到了这里成为瓶颈的时候再记着调整下字段顺序就好。

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

推荐阅读更多精彩内容

  • 为什么要进行内存对齐 1.为了cpu读取更快使CPU的访问内存数据的速度加快,如果一个内存的数据是经过对齐的,那么...
    黑夜no烟丝阅读 876评论 0 2
  • 结构体内存对齐 一、结构体对齐的三大原则 1、数据成员对齐规则:结构(struct)(或联合体(union))的数...
    胖小夜曲阅读 1,079评论 0 2
  • 结构体内存对齐 alloc方法调用前做了什么 开篇前先看看alloc方法调用前系统做了什么,解决上篇文章遗留的一个...
    KG丿夏沫阅读 328评论 0 0
  • 前言 结构体是c/c++比较常见的数据结构,研究它对于深入学习Object-C也是比较重要的一环。一般的结构体的内...
    Johnny_Z阅读 435评论 1 4
  • 数据对象 本节谈及的内存对齐,而在进行这个话题之前,我先引入一个叫数据对象的概念。 大部份C语言教程的文章很少会提...
    铁甲万能狗阅读 4,005评论 4 2