golang - 内存对齐

1. 介绍

CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)
为保证程序顺利高效的运行,编译器会把各种类型的数据安排到合适的地址,并占用合适的长度,这就是内存对齐

示例:
假设cpu的memory access granularity =8 byte, 内存不对齐的情况下
,cpu 要从抵制1开始读8字节的数据,如下图所示:


示例

此时cpu 需要读取两次,

  • 第一次读取0-7 地址段
  • 第二次读取8-15 地址段
  • 两个地址段拼接,读取1-8地址的数据

相反的,如果在内存对齐的情况下, 1- 8 地址段的数据,将会直接存储在8-15 地址段,cpu 就可以一次获取到对应的字段。 但是之前的1-7地址段的内存就会被浪费了

总结来说,内存对齐的原因有如下两点:

  1. 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
  2. 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作

2. 对齐边界/ 对齐系数

在不同平台上的编译器都有自己默认的 对齐系数(对齐边界),一般来讲,我们常用的平台的系数如下:

32 位系统:4byte(可以理解为cpu的memory access granularity=4)
64 位系统:8byte (可以理解为cpu的memory access granularity=8)

在go 语言中,每个数据类型,也有自己对应的对齐系数,我们可以通过官方的unsafe 包中的unsafe.Alignof 函数获取不同数据类型的对齐边界

func main() {
   fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
   fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
   fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
   fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
   fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
   fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
}
  • 对于数组类型 []elementType,对齐系数 = unsafe.Alignof(elementType)
  • 对于slice , map,指针类型, 对齐系数 = 8(64位操作系统)
  • 对于结构体, 首先要确定每个成员的对齐边界,然后取其中最大的,这就是这个结构体的对齐边界


    结构体内存系数

注意:
所有类型的对齐系数不会超过编译器设定的对齐系数, 例如在64 位操作系统中的默认对齐系数是8 ,所有的类型对齐系数不会超过8

3. 对齐规则

  1. 存储这个结构体的起始地址,是对齐边界的倍数。假设从0开始存,结构体的每个成员在存储时,都要把这个起始地址当作地址0,然后再用相对地址来决定自己该放在哪里。
  1. 结构体整体占用字节数需要是类型对齐边界的倍数,不够的话要往后扩张一下
  • 示例:

结构体T1 的两个成员的bool 和i6的大小分别占1byte 和2byte, 对应的对齐系数分别是1byte 和2byte, 此时结构体T1的对齐系数是Max{1,2}=2byte

type T1 struct {
  bool bool  
  i16  int16 
}

func main() {
    fmt.Printf("bool size:%v, bool align:%v\n", unsafe.Sizeof(bool(true)), unsafe.Alignof(bool(true)))
    fmt.Printf("int16 size:%v, int16 align:%v\n", unsafe.Sizeof(int16(0)), unsafe.Alignof(int16(0)))
    fmt.Printf("T1 size:%v, T1 align:%v\n", unsafe.Sizeof(T1{}), unsafe.Alignof(T1{}))
    return
}

在内存未对齐的状态下 T1 的大小为3字节, 但是通过内存对齐后,内存占4 字节,如图1 所示:

i16并没有直接放在bool的后面,而是在bool中填充了一个空白后,放到了偏移量为2的位置上。如果i16从偏移量为1的位置开始占用2个字节,根据对齐原则1:构体变量中成员的偏移量必须是成员大小的整数倍,套用公式 1 % 2 = 1,就不满足对齐的要求,所以i16从偏移量为2的位置开始

图1

以 结构体T2,T3 为例

type T2 struct {
  i8  int8  
  i64 int64
  i32 int32 
}


type T3 struct {
  i8  int8  
  i32 int32 
  i64 int64 
}

i8,i32,i64 的大小分别是1字节,4字节,8字节,对齐系数1字节,4 字节,8字节,所以T2和T3 的对齐系数也都是8字节, 通过unsafe.Sizeof 函数计算得到T2 和T3类型的数据大小为24字节和16字节

func main() {
    t2 := T2{}
    fmt.Println(unsafe.Sizeof(t2)) // 24 bytes
    t3 := T3{}
    fmt.Println(unsafe.Sizeof(t3)) // 16 bytes
        // 通过offset 函数来确认成员的偏移量
    fmt.Printf("t2 i8 offset:%v,i64 offset:%v,i32 offset:%v\n", unsafe.Offsetof(t2.i8), unsafe.Offsetof(t2.i64), unsafe.Offsetof(t2.i32))// 0,8,16
    fmt.Printf("t3 i8 offset:%v,i32 offset:%v,i64 offset:%v\n", unsafe.Offsetof(t3.i8), unsafe.Offsetof(t3.i32), unsafe.Offsetof(t3.i64))//0,4,8


  }

具体解析如下图所示:
以T2结构体为例,实际存储数据的只有13字节,但实际用了24字节,浪费了11个字节


T2

以T3结构体为例,实际存储数据的只有13字节,但实际用了16字节,浪费了3个字节:


T3

可以看到合理的结构体内的成员的排布可以减少对内存的消耗, 提高内存的使用效率
  • 结构体嵌套
type T4 struct {
    i32 int32
    b   bool
    t   T1
}
type T1 struct {
    i16  int16
    bool bool
}

结构体T4 嵌套T1,前面已经计算过T1类型的对齐边界为2字节, 字节长度为4字节。 通过unsafe.Sizeof和Alignof函数可得T4 类型的字节长度为12字节和对齐系数为4字节

  1. 首先来分析下T4类型的对齐系数:
    int32 的对齐系数为4字节,bool的对齐系数为1字节,T1的对齐系数为2字节

对齐系数= max{4,1,2}=4

  1. 其次分析T4 类型是如何实现内存对齐的:
    T4 内存对齐后的分布如下图所示:


    结构体嵌套

从起始地址开始前四位地址为数据i32,接下来b的对齐系数为1,所以直接接在后面,即在5号位置,接下来就是t类型数据,因为对齐系数为2, 此时在6号位置6%2==0 成立,此时t的位置就在6号位放置,6-7 号为t.i32, 8号为t.bool, 9号为t类型的pad , 因为T4的对齐系数是4, 此时T4 的总长度为9, 9%4!=0, 需要在10-11 补充pad 。最终12%4==0, 所以结构4 所占的字节长度为12

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

推荐阅读更多精彩内容

  • 先看一个结构体 对于这两个结构体,都有a、b、c三个定义完全一样的字段,只是在定义结构体的时候字段顺序不一样而已,...
    cfanbo阅读 88评论 0 0
  • 1.golang内存对齐保证 typealign 保证bool,type,uint8,int81个字节uint16...
    shuaidong阅读 919评论 0 0
  • 如何得到一个对象所占内存大小? 内存对齐: 为何会有内存对齐?1.并不是所有硬件平台都能访问任意地址上的任意数据。...
    郭老汉阅读 1,914评论 2 4
  • 什么是内存对齐 此时的 打印值为:16,一个64长度int占8字节,两个就是16。 此时的 打印值为:16,4+8...
    Stevennnmmm阅读 489评论 0 0
  • 欢迎关注微信公众号:全栈工厂 1. 先看一个问题 请思考30秒想想以下代码输出的内容是多少? 执行后代码输出: 你...
    liqingbiubiu阅读 233评论 0 0