内存布局
!--https://groups.google.com/forum/#!topic/golang-nuts/XDfQUn4U_g8https://groups.google.com/forum/#!topic/golang-nuts/YSxEBt9kzuchttps://groups.google.com/forum/#!topic/golang-nuts/HaxJMtSngOo--Go是一门属于C语言家族的编程语言,所以本文谈及的很多概念和C语言是相通的。
类型对齐保证也称为值地址对齐保证。如果一个类型的对齐保证为N
(一个正整数),则在运行时刻T
类型的每个(可寻址的)值的地址都是N
的倍数。我们也可以说类型T
的值的地址保证为N
字节对齐的。
事实上,每个类型有两个对齐保证。当它被用做结构体类型的字段类型时的对齐保证称为此类型的字段对齐保证,其它情形的对齐保证称为此类型的一般对齐保证。
对于一个类型T
,我们可以调用unsafe.Alignof(t)
来获得它的一般对齐保证,其中t
为一个T
类型的的非字段值,也可以调用unsafe.Alignof(x.t)
来获得T
的字段对齐保证,其中x
为一个结构体值并且t
为一个类型为T
的结构体字段值。
unsafe
标准库包中的函数的调用都是在编译时刻估值的。
在运行时刻,对于类型为T
的一个值t
,我们可以调用reflect.TypeOf(t).Align()
来获得类型的一般对齐保证,也可以调用reflect.TypeOf(t).FieldAlign()
来获得T
的字段对齐保证。
从这些要求可以看出,Go白皮书并未为任何类型指定了确定的对齐保证要求,它只是指定了一些最基本的要求。 即使对于同一个编译器,具体类型的对齐保证在不同的架构上也是不相同的。同一个编译器的不同版本做出的具体类型的对齐保证也有可能是不相同的。当前版本(1.13)的标准编译器做出的对齐保证列在了下面:
这里,一个自然字(native word)的尺寸在32位的架构上为4字节,在64位的架构上为8字节。
这意味着,对于当前版本的标准编译器,其它类型的对齐保证为4
或者8
,具体取决于程序编译时选择的目标架构。此结论对另一个流行Go编译器gccgo也成立。
一般情况下,在Go编程中,我们不必关心值地址的对齐保证。除非有时候我们打算优化一下内存消耗,或者编写跨平台移植性良好的Go代码。请阅读下两节以获得详情。
Go白皮书只对以下种类的类型的尺寸进行了。
Go白皮书没有对其它种类的类型的尺寸最初明确规定。请阅读值复制成本一文来获取标准编译器使用的各种其它类型的尺寸。
为了满足上一节中规定的地址对齐保证要求,Go编译器可能会在结构体的相邻字段之间填充一些字节。这使得一个结构体类型的尺寸并非等于它的各个字段类型尺寸的简单相加之和。 下面是一个展示了一些字节是如何填充到一个结构体中的例子。首先,从上面的描述中,我们已得知(对于标准编译器来说):
- 下例中的类型
T1
和的对齐保证均为它们的各个字段的最大对齐保证。所以它们的对齐保证和内置类型int64
相同,即在32位架构上为4个字节,在64位架构上为8个字节。
从这个例子可以看出,尽管类型T1
和T2
拥有相同的字段集,但是它们的尺寸并不相等。
一个有趣的事实是有时候一个结构体类型中零尺寸类型的字段可能会影响到此结构体类型的尺寸。请阅读获取详情。
在此文中,64位字是指类型为内置类型int64
或uint64
的值。
一文提到了一个事实:一个64位字的原子操作要求此64位字的地址必须是8字节对齐的。这对于标准编译器目前支持的64位架构来说并不是一个问题,因为标准编译器保证任何一个64位字的地址在64位架构上都是8字节对齐的。
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX.所以,情况并非无可挽救。 - 这些非常老旧的架构在今日已经相当得不主流了。如果一个程序需要在这些架构上对64位字进行原子操作,还有很多其它同步技术可用。 - 对其它不是很老旧的32位架构,有一些途径可以保证在这些架构上对一些64位字的原子操作是安全的。 这些途径被描述为开辟的结构体、数组和切片值中的第一个(64位)字可以被认为是8字节对齐的。这里的开辟的应该如何解读?我们可以认为一个开辟的值为一个声明的变量、内置函数
On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.
On both ARM and x86-32, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
make
的调用返回值,或者内置函数new
的调用返回值所引用的值。如果一个切片是从一个开辟的数组派生出来的并且此切片和此数组共享第一个元素,则我们也可以将此切片看作是一个开辟的值。
此对哪些64位字可以在32位架构上被安全地原子访问的描述是有些保守的。有很多此描述并未包括的64位字在32位架构上也是可以被安全地原子访问的。比如,如果一个元素类型为64位字的数组或者切片的第一个元素可以被安全地进行64位原子访问,则此数组或切片中的所有元素都可以被安全地进行64位原子访问。只是因为很难用三言两语将所有在32位架构上可以被安全地原子访问的64位字都罗列出来,所以官方文档采取了一种保守的描述。
下面是一个展示了哪些64位字在32位架构上可以和哪些不可以被安全地原子访问的例子。
如果一个结构体类型的某个64位字的字段(通常为第一个字段)在代码中需要被原子访问,为了保证此字段值在各种架构上都可以被原子访问,我们应该总是使用此结构体的开辟值。当此结构体类型被用做另一个结构体类型的一个字段的类型时,此字段应该(尽量)被安排为另一个结构体类型的第一个字段,并且总是使用另一个结构体类型的开辟值。
如果一个结构体含有需要一个被原子访问的字段,并且我们希望此结构体可以自由地用做其它结构体的任何字段(可能非第一个字段)的类型,则我们可以用一个[15]byte
值来模拟此64位值,并在运行时刻动态地决定此64位值的地址。比如:
通过时此方法,Counter
类型可以自由地用做其它结构体的任何字段的类型,而无需担心此类型中维护的64位字段值可能不是8字节对齐的。此方法的缺点是,对于每个Counter
类型的值,都有7个字节浪费了。而且此方法使用了非类型安全指针。sync
标准库包中的采用了此方法,但使用的是[3]uint32
类型而不是[15]byte
类型。这基于uint32
类型的对齐保证为4的倍数这个假设。此假设对于当前的官方Go编译器和gccgo编译器来说都是成立的。但此假设对于一个可能的第三方编译器未必成立。
为了原子操作的跨架构兼容编程变得更简单,Russ Cox,无论在64位架构上还是32位架构上。但截至目前(Go 1.13),此提议尚未得到通过。
Go语言101项目目前同时托管在Github和上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。
本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。