[Golang]从sync.waitGroup看内存对齐

风过留情李寻欢 · · 657 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

阅读该文章需要了解sync.waitGroup的基本用法。

GO版本1.14

最近在阅读Go标准库的sync.waitGroup源码,其中下面的这段代码引起了我的兴趣:

image.png

先简单说明一下这段代码的含义,在WaitGroup这个结构体中的state1声明一个长度为3的uint32类型的数组, 而state这个方法会返回waiter数量sema信号量这两个参数。但根据if语句中的条件,返回的元素不同,但它们却表示相同的含义。

首先,我们分析一下if语句中的条件:uintptr(unsafe.Pointer(&wg.state1))%8 == 0。从代码层面来看,它把wg.state1的内存地址对8的取模。根据结构是否等于0,分别返回以下结果:

  • 等于0返回: wg.state1(wg.state1[0])和wg.state1[2]的内存地址
  • 不等于0返回:wg.state1[1]wg.state1[0]的内存地址

为什么是对8取模?

从代码来看,它用wg.state1的内存地址对8的取模。那么为什么这里是对8取模而不是其他的数值?查阅资料得知,在64位机器上,8字节是单个机器字的长度,内存地址对8取模,可以判断该数据对象的内存地址是否为64位对齐。参考以下描述:

在不同的硬件平台上,内存以字节为单位,不一定支持任何内存地址的存取,一般可能以双字节、4字节等为单位存取内存,为了保证处理器正确存取数据,需要进行内存对齐。 这样做的好处是能够提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。

了解这个内存对齐的概念之后,我们再来分析if语句中含义,因为1字节=8位,所以在32位的平台上,机器字长就是4(32/8),而在64位的平台上,则是8(64/8),而任意可访问变量的内存地址一定是内存存取粒度的整数倍,所以上述if判断语句中的对8取模就是在判断,当前wg.state1是否对8对齐,即是运行在64位平台上还是32位平台?(目前Go只兼容这两种平台)。

现在,我们再来分析wg.state1在不同平台下,waitGroup.state1的元素分别表示的含义:

image.png

image.png

从图中,我们可以清晰的看出,在不同平台下,数据元素表示的含义发生了变化,现在我们可以观察下面的源码(删除了冗余的部分):

image.png

我们可以看到第2行代码,wg.state()返回的返回的第一个参数statep(waiter数量),由上述可知,在不同平台上statep对应wg.state1中不同的元素。

为什么平台不同,元素的含义也不同?

再看到第3行红色加粗显示的代码,这里表示的是对wg.state返回的第一个参数statep(waiter数量)进行64位的原子操作。而要执行64位的原子操作需要数据的内存地址是64位对齐的,即数据的内存地址能够是8的整数倍。

在32位平台上,即数据的内存地址是32位对齐的。所以如果wg.state1的元素依然按照64位平台的顺序返回(waiter, counter, sema),那么wg.state1[0](数组第一个元素的地址和数组相同)的内存地址是32位对齐的,不能保证一定是64位对齐的,这样就无法进行64位原子操作。我们可以假设在32位平台上,wg.state1的地址为0xc420016244 ,此时wg.state1的内存地址就如下图所示:

image.png

这里我们可以知道,为了保证对waiter数量的操作是64位对齐的,程序在32位机器上,将wg.state1[0]用于表示信号量,而由于wg.state1[0]是32位对齐的且它的大小位4字节,那么wg.state1[1]的内存地址就是64位对齐的。这样保证了atomic.AddUnit64一定是原子操作。

Go如何处理内存对齐?

从上面的源码分析中可以得知,CPU按照固定字长读取数据,保证定位数据的高效性。同时Go中为了保证数据结构对齐,存在内存对齐保证的概念。

现在让我们通过一个demo,再来看看Go的内存对齐保证:

image.png 这里声明了两个几乎一摸一样的struct:T1 和 T2,唯一的区别是它们的字段声明顺序不同,我们通过打印它们的内存大小以及内存对齐保证来观察一下:

=== RUN   TestPointerAlignment
T1 sizeof = 24; alignof = 8
T2 sizeof = 16; alignof = 8
--- PASS: TestPointerAlignment (0.00s)
PASS
复制代码

这个示例运行在64位的机器上,我们通过Go中提供的APIunsafe.Alignof得到内存对齐保证都是8,struct大小分别为2416。 已知[2]int8、int64、int16在64位平台上分别占2字节、8字节、2字节,struct占用0字节,理论上来说它们的大小应该是12字节,但它们的大小却高于理论值且不相同,这是为什么?

因为为了保证内存对齐,在64位机器上,struct会被N个8字节机器(内存块)读取。如果单个机器字剩下的空间无法存储某个字段,那么内存块剩下的空间会被填充,然后用一个新的内存块去读取,这也是为什么实际数据大小比我们预估的要大。这个填充的过程叫做结构填充(C语言常见的概念)

结构如何填充?

我们以64位机器的填充过程为例,如下图所示:

image.png

我们看到,实际上在编译器眼里,strutct T1和T2变成了这样:

image.png

综上,我们可以知道编译器,按照字段声明的顺便读取字段,并按块(跟机器相关:64bit-8B;32bit-4B)读取数据,在遇到不满块大小的字段时,会自动进行结构填充,使用充满块大小,这样是为了提高CPU读区内存的效率,避免过多的数据拼接和过滤。

下面我们再来看一种特殊情况。

struct如何填充?

我们还是用一个demo来说明:

image.png

=== RUN   TestZeroSizedFieldAlignment
T3.E Offsetof = 32, T3.E Sizeof = 0, T3 Sizeof = 40
--- PASS: TestZeroSizedFieldAlignment (0.00s)
PASS
复制代码

这里我们T3.E的大小为0,其他字段加起来的大小为32,但整个struct的大小却为40。这说明空sturct被填充了8个字节。

那么,如果我们把T3.E放到第一个字段的位置,还会填充吗?

image.png

=== RUN   TestEmptyFieldPointer
T3 Sizeof = 32; T3.E Sizeof = 0
--- PASS: TestEmptyFieldPointer (0.00s)
PASS
复制代码

我们可以看到,此时T3的大小为32,没有发生填充。再来看一个例子:

如果T3.E是空数组呢? image.png

=== RUN   TestZeroSizedFieldAlignment
T4.E Offsetof = 32, T4.E Sizeof = 0, T4 Sizeof = 40
--- PASS: TestZeroSizedFieldAlignment (0.00s)
PASS
复制代码

这里我们可以知道,空数组也填充了8个字节。Go中只会对最后一个空字段进行填充。原因是struct字段的地址是不能超过该struct的分配地址空间,这种情况会影响GO的GC,导致内存泄漏

零大小字段(zero sized field)是指struct{},大小为 0,按理作为字段时不需要对齐,但当在作为结构体最后一个字段(final field)时需要对齐的。即开篇我们讲到的面试题的情况,假设有指针指向这个final zero field, 返回的地址将在结构体之外(即指向了别的内存),如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放),go 会对这种final zero field也做填充,使对齐。当然,有一种情况不需要对这个final zero field做额外填充,也就是这个末尾的上一个字段未对齐,需要对这个字段进行填充时,final zero field就不需要再次填充,而是直接利用了上一个字段的填充。

现在我们可以知道,在Go中,数据对象的结构填充依赖于内存对齐保证。我们在看一个示例:

image.png

=== RUN   TestTypeAlignmentGuarantee
T8.B Sizeof = 12, T8.B Alignment = 4, T8 Sizeof = 12, T8 Alignment = 4
--- PASS: TestTypeAlignmentGuarantee (0.00s)
PASS
复制代码

这里,我们可以发现对于不同的数据类型,内存对齐保证是不同的,而对于struct、数组这种复合类型,它们的内存对齐保证则依赖它的字段或元素的内存对齐保证的大小
翻阅官方文档,具体规则如下所示:

type                      alignment guarantee
------                    ------
bool, uint8, int8         1
uint16, int16             2
uint32, int32             4
float32, complex64        4
arrays                    元素的内存对齐保证
structs                   字段中最大的内存对齐保证
other types               机器字长
复制代码

现在我们再回头来看sync.waitGroup的源码,在对内存地址进行操作的时候,不但需要考虑不同类型的对齐保证,同时也要考虑不同平台上机器字长的影响。

最后,感谢阅读。

参考


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:风过留情李寻欢

查看原文:[Golang]从sync.waitGroup看内存对齐

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

657 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传