阅读该文章需要了解sync.waitGroup
的基本用法。
GO版本1.14
最近在阅读Go
标准库的sync.waitGroup
的源码,其中下面的这段代码引起了我的兴趣:
先简单说明一下这段代码的含义,在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
的元素分别表示的含义:
从图中,我们可以清晰的看出,在不同平台下,数据元素表示的含义发生了变化,现在我们可以观察下面的源码(删除了冗余的部分):
我们可以看到第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
的内存地址就如下图所示:
这里我们可以知道,为了保证对waiter数量
的操作是64位对齐的,程序在32位机器上,将wg.state1[0]
用于表示信号量,而由于wg.state1[0]
是32位对齐的且它的大小位4字节,那么wg.state1[1]
的内存地址就是64位对齐的。这样保证了atomic.AddUnit64
一定是原子操作。
Go如何处理内存对齐?
从上面的源码分析中可以得知,CPU按照固定字长读取数据,保证定位数据的高效性。同时Go
中为了保证数据结构对齐,存在内存对齐保证的概念。
现在让我们通过一个demo,再来看看Go
的内存对齐保证:
这里声明了两个几乎一摸一样的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
大小分别为24
和16
。
已知[2]int8、int64、int16
在64位平台上分别占2字节、8字节、2字节,struct占用0字节,理论上来说它们的大小应该是12字节,但它们的大小却高于理论值且不相同,这是为什么?
因为为了保证内存对齐,在64位机器上,struct
会被N个8字节机器(内存块)读取。如果单个机器字剩下的空间无法存储某个字段,那么内存块剩下的空间会被填充,然后用一个新的内存块去读取,这也是为什么实际数据大小比我们预估的要大。这个填充的过程叫做结构填充
(C语言常见的概念)
结构如何填充?
我们以64位机器的填充过程为例,如下图所示:
我们看到,实际上在编译器眼里,strutct T1和T2
变成了这样:
综上,我们可以知道编译器,按照字段声明的顺便读取字段,并按块(跟机器相关:64bit-8B;32bit-4B)读取数据,在遇到不满块大小的字段时,会自动进行结构填充,使用充满块大小,这样是为了提高CPU读区内存的效率,避免过多的数据拼接和过滤。
下面我们再来看一种特殊情况。
空struct
如何填充?
我们还是用一个demo来说明:
=== 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
放到第一个字段的位置,还会填充吗?
=== RUN TestEmptyFieldPointer
T3 Sizeof = 32; T3.E Sizeof = 0
--- PASS: TestEmptyFieldPointer (0.00s)
PASS
复制代码
我们可以看到,此时T3
的大小为32,没有发生填充。再来看一个例子:
如果T3.E
是空数组呢?
=== 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
中,数据对象的结构填充依赖于内存对齐保证。我们在看一个示例:
=== 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
的源码,在对内存地址进行操作的时候,不但需要考虑不同类型的对齐保证,同时也要考虑不同平台上机器字长的影响。
最后,感谢阅读。
参考
有疑问加站长微信联系(非本文作者)