共计 6288 个字符,预计需要花费 16 分钟才能阅读完成。
原文链接:详解内存对齐
前言
哈喽,大家好,我是
asong
。好久不见,上周停更了一周,因为工作有点忙,好在这周末闲了下来,就连忙来肝文喽。明天咱们来聊一聊一道常见的面试八股文——内存对齐,咱们平时在业务开发中基本不care
内存对齐,然而在面试中,这就是一个高频考点,明天咱们就一起来看一看到底什么是内存对齐。
前情概要
在理解内存对齐之前,先来明确几个对于操作系统的概念,更加方面咱们对内存对齐的了解。
-
内存治理:咱们都晓得内存是计算中重要的组成之一,内存是与
CPU
进行沟通的桥梁,用于暂存CPU
中的运算数据、以及与硬盘等内部存储器替换的数据。晚期,程序是间接运行在物理内存上的,间接操作物理内存,然而会存在一些问题,比方应用效率低、地址空间不隔离等问题,所以就呈现了虚拟内存,虚拟内存就是在程序和物理内存之间引入了一个中间层,这个中间层就是虚拟内存,这样就达到了对过程地址和物理地址的隔离。在linux
零碎中,将虚拟内存划分为用户空间
和内核空间
,用户过程只能拜访用户空间的虚拟地址,只有通过零碎调用、外设中断或异样能力拜访内核空间,咱们次要来看一下用户空间,用户空间被分为5
个不同内存区域:- 代码段:寄存可执行文件的操作指令,只读
- 数据段:用来寄存可执行文件中已初始化全局变量,寄存动态变量和全局变量
- BSS 段:用来存未初始化的全局变量
- 栈区:用来存长期创立的局部变量
- 堆区:用来存动态分配的内存段
内存的常识先介绍个大略,对于本文的了解应该够了,咱们接着介绍操作系统几个其余概念。
CPU
:地方处理单元(Cntral Pocessing Unit)的缩写,也叫处理器;CPU
是计算机的运算外围和管制外围,咱们人类靠着大脑思考,电脑就是靠着CPU
来运算、管制,起到协调和管制作用,从性能来看,CPU 的外部由寄存器、控制器、运算器和时钟四局部组成,各局部之间通过电信号连通。CPU
和内存的工作关系:当咱们执行一个程序时,首先由输出设施向CPU
收回操作指令,CPU
接管到操作指令后,硬盘中对应的程序就会被间接加载到内存中,尔后,CPU 再对内存进行寻址操作,将加载到内存中的指令翻译进去,而后发送操作信号给操作控制器,实现程序的运行或数据的解决。存在于内存中的目标就是为了CPU
可能过总线进行寻址,取指令、译码、执行取数据,内存与寄存器交互,而后CPU
运算,再输入数据至内存。
os
:os
全称为Operating System
,也就是操作操作系统,是一组主管并管制计算机操作、使用和运行硬件、软件资源和提供公共服务组织用户交互的互相关联的系统软件,同时也是计算机系统的内核与基石。- 编译器:编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个古代编译器的次要工作流程:源代码 (source code) → 预处理器(preprocessor) → 编译器 (compiler) → 指标代码 (object code) → 链接器 (Linker) → 可执行程序(executables)。
写在最初的一个知识点:
计算机中,最小的存储单元为字节,实践上任意地址都能够通过总线进行拜访,每次寻址能传输的数据大小就跟
CPU
位数无关。常见的CPU
位数有 8 位,16 位,32 位,64 位。位数越高,单次操作执行的数据量越大,性能也就越强。os
的位数个别与CPU
的位数相匹配,32
位CPU
能够寻址4
GB 内存空间,也能够运行32
位的os
,同样情理,64
位的CPU
能够运行32
位的os
,也能够运行 64 位的os
。
何为内存对齐
以下内容来源于网络总结:
古代计算机中内存空间都是依照字节 (byte) 进行划分的,所以从实践上讲对于任何类型的变量拜访都能够从任意地址开始,然而在理论状况中,在拜访特定类型变量的时候常常在特定的内存地址拜访,所以这就须要把各种类型数据依照肯定的规定在空间上排列,而不是依照程序一个接一个的排放,这种就称为内存对齐,内存对齐是指首地址对齐,而不是说每个变量大小对齐。
为何要有内存对齐
次要起因能够归结为两点:
- 有些
CPU
能够拜访任意地址上的任意数据,而有些CPU
只能在特定地址拜访数据,因而不同硬件平台具备差异性,这样的代码就不具备移植性,如果在编译时,将调配的内存进行对齐,这就具备平台能够移植性了 CPU
每次寻址都是要生产工夫的,并且CPU
拜访内存时,并不是一一字节拜访,而是以字长(word size)为单位拜访,所以数据结构应该尽可能地在天然边界上对齐,如果拜访未对齐的内存,处理器须要做两次内存拜访,而对齐的内存拜访仅须要一次拜访,内存对齐后能够晋升性能。举个例子:
假如以后
CPU
是32
位的,并且没有内存对齐机制,数据能够任意寄存,当初有一个int32
变量占4byte
,寄存地址在0x00000002 - 0x00000005
(纯假如地址,莫当真),这种状况下,每次取4
字节的CPU
第一次取到[0x00000000 - 0x00000003]
,只失去变量1/2
的数据,所以还须要取第二次,为了失去一个int32
类型的变量,须要拜访两次内存并做拼接解决,影响性能。如果有内存对齐了,int32
类型数据就会依照对齐规定在内存中,下面这个例子就会存在地址0x00000000
处开始,那么处理器在取数据时一次性就能将数据读出来了,而且不须要做额定的操作,应用空间换工夫,进步了效率。
没有内存对齐机制:
内存对齐后:
对齐系数
每个特定平台上的编译器都有本人的默认 ” 对齐系数 ”,罕用平台默认对齐系数如下:
- 32 位零碎对齐系数是 4
- 64 位零碎对齐系数是 8
这只是默认对齐系数,实际上对齐系数咱们是能够批改的,之前写 C
语言的敌人晓得,能够通过预编译指令 #pragma pack(n)
来批改对齐系数,因为 C
语言是预处理器的,然而在 Go
语言中没有预处理器,只能通过 tags
和命名约定
来让 Go
的包能够治理不同平台的代码,然而怎么批改对齐系数,感觉 Go
并没有凋谢这个参数,找了良久没有找到,等前面再认真看看,找到了再来更新!
既然对齐系数无奈更改,然而咱们能够查看对齐系数,应用 Go
语言中的 unsafe.Alignof
能够返回相应类型的对齐系数,应用我的 mac(64 位)测试后发现,对齐系数都合乎 2^n
这个法则,最大也不会超过8
。
func main() {fmt.Printf("string alignof is %d\n", unsafe.Alignof(string("a")))
fmt.Printf("complex128 alignof is %d\n", unsafe.Alignof(complex128(0)))
fmt.Printf("int alignof is %d\n", unsafe.Alignof(int(0)))
}
运行后果
string alignof is 8
complex128 alignof is 8
int alignof is 8
留神:不同硬件平台占用的大小和对齐值都可能是不一样的。
构造体的内存对齐规定
一提到内存对齐,大家都喜爱拿构造体的内存对齐来举例子,这里要揭示大家一下,不要混同了一个概念,其余类型也都是要内存对齐的,只不过拿构造体来举例子能更好的了解内存对齐,并且构造体中的成员变量对齐有本人的规定,咱们须要搞清这个对齐规定。
C 语言
的对齐规定与 Go
语言一样,所以 C 语言
的对齐规定对 Go
同样实用:
- 对于构造体的各个成员,第一个成员位于偏移为
0
的地位,构造体第一个成员的偏移量 (offset) 为0
,当前每个成员绝对于构造体首地址的offset
都是该成员大小与无效对齐值中较小那个的整数倍,如有须要编译器会在成员之间加上填充字节。 - 除了构造成员须要对齐,构造自身也须要对齐,构造的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。
举个例子
依据下面的对齐规定,咱们来剖析一个例子,加深了解:
// 64 位平台,对齐参数是 8
type User struct {
A int32 // 4
B []int32 // 24
C string // 16
D bool // 1
}
func main() {
var u User
fmt.Println("u1 size is",unsafe.Sizeof(u))
}
// 运行后果
u size is 56
这里我的 mac
是64
位的,对齐参数是 8
,int32
、[]int32
、string
、bool
对齐值别离是4
、8
、8
、1
,占用内存大小别离是4
、24
、16
、1
,咱们先依据第一条对齐规定剖析User
:
- 第一个字段类型是
int32
,对齐值是 4,大小为 4,所以放在内存布局中的第一位. - 第二个字段类型是
[]int32
,对齐值是 8,大小为24
,依照第一条规定,偏移量应该是成员大小24
与对齐值8
中较小那个的整数倍,那么偏移量就是8
,所以4-7
位会由编译进行填充,个别为0
值,也称为空洞,第9
到32
位为第二个字段B
. - 第三个字段类型是
string
,对齐值是8
,大小为16
,所以他的内存偏移值必须是 8 的倍数,因为user
前两个字段就曾经排到了第32
位,所以offset
为32
正好是8
的倍数,不要填充,从32
位到48
位是第三个字段C
. - 第四个字段类型是
bool
,对齐值是1
,大小为1
,所以他的内存偏移值必须是1
的倍数,因为user
前两个字段就曾经排到了第48
位,所以下一位的偏移量正好是48
,正好是字段D
的对齐值的倍数,不必填充,能够间接排列到第四个字段,也就是从48
到第49
位是第三个字段D
.
依据第一条规定剖析后,当初构造所占大小为 49
字节,咱们再来依据第二条规定剖析:
- 依据第二条规定,默认对齐值是
8
,字段中最大类型水平是24
,所以求出构造体的对齐值是8
,咱们目前的内存长度是49
,不是8
的倍数,所以须要补齐,所以最终的后果就是56
,补了7
位。
成员变量程序对内存对齐带来的影响
依据下面的规定咱们能够看出,成员变量的程序也会影响内存对齐的后果,咱们先来看一个例子:
type test1 struct {
a bool // 1
b int32 // 4
c string // 16
}
type test2 struct {
a int32 // 4
b string // 16
c bool // 1
}
func main() {
var t1 test1
var t2 test2
fmt.Println("t1 size is",unsafe.Sizeof(t1))
fmt.Println("t2 size is",unsafe.Sizeof(t2))
}
运行后果:
t1 size is 24
t2 size is 32
test1
的内存布局:
test2
的内存布局:
)
通过以上剖析,咱们能够看出,构造体中成员变量的程序会影响构造体的内存布局,所以在日常开发中大家要留神这个问题,能够节俭内存空间。
空构造体字段对齐
Go
语言中空构造体的大小为0
,如果一个构造体中蕴含空构造体类型的字段时,通常是不须要进行内存对齐的,举个例子:
type demo1 struct {a struct{}
b int32
}
func main() {fmt.Println(unsafe.Sizeof(demo1{}))
}
运行后果:4
从运行后果可知构造体 demo1
占用的内存与字段 b
占用内存大小雷同,所以字段 a
是没有占用内存的,然而空构造体有一个特例,那就是当 struct{}
作为构造体最初一个字段时,须要内存对齐。因为如果有指针指向该字段, 返回的地址将在构造体之外,如果此指针始终存活不开释对应的内存,就会有内存泄露的问题(该内存不因构造体开释而开释),所以当 struct{}
作为构造体成员中最初一个字段时,要填充额定的内存保障平安。
type demo2 struct {
a int32
b struct{}}
func main() {fmt.Println(unsafe.Sizeof(demo2{}))
}
运行后果:8
思考内存对齐的设计
在之前的文章 源码分析 sync.WaitGroup剖析 sync.waitgroup
的源码时,应用 state1
来存储状态:
// A WaitGroup must not be copied after first use.
type WaitGroup struct {
noCopy noCopy
// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
// 64-bit atomic operations require 64-bit alignment, but 32-bit
// compilers do not ensure it. So we allocate 12 bytes and then use
// the aligned 8 bytes in them as state, and the other 4 as storage
// for the sema.
state1 [3]uint32
}
state1
这里总共被调配了 12
个字节,这里被设计了三种状态:
- 其中对齐的
8
个字节作为状态,高32
位为计数的数量,低32
位为期待的goroutine
数量 - 其中的
4
个字节作为信号量存储
提供了 (wg *WaitGroup) state() (statep *uint64, semap *uint32)
帮忙咱们从 state1
字段中取出他的状态和信号量,为什么要这样设计呢?
因为 64
位原子操作须要 64
位对齐,然而 32
位编译器不能保障这一点,所以为了保障 waitGroup
在32
位平台上应用的话,就必须保障在任何时候,64 位
操作不会报错。所以也就不能分成两个字段来写,思考到字段程序不同、平台不同,内存对齐也就不同。因而这里采纳动静辨认以后咱们操作的 64
位数到底是不是在 8
字节对齐的地位下面,咱们来剖析一下 state
办法:
// state returns pointers to the state and sema fields stored within wg.state1.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
当数组的首地址是处于一个 8
字节对齐的地位上时,那么就将这个数组的前 8
个字节作为 64
位值应用示意状态,后 4
个字节作为 32
位值示意信号量 (semaphore
)。同理如果首地址没有处于8
字节对齐的地位上时,那么就将前 4
个字节作为 semaphore
,后8
个字节作为 64
位数值。画个图示意一下:
)
总结
终于靠近序幕了,内存对齐始终面试中的高频考点,通过内存对齐能够理解面试者对操作系统常识的理解水平,所以这块常识还是比拟重要的,心愿这篇文章能帮忙大家答疑解惑,更好的忽悠面试官~。
文中代码已上传 github:https://github.com/asong2020/… 欢送 star;
文中有任何问题欢送留言区探讨~;
欢送关注公众号:Golang 梦工厂
举荐往期文章:
- 学习 channel 设计:从入门到放弃
- 面试官:小松子来聊一聊内存逃逸
- Go 语言中 new 和 make 你应用哪个来分配内存?
- 源码分析 panic 与 recover,看不懂你打我好了!
- 空构造体引发的大型打脸现场
- [面试官:你能聊聊 string 和[]byte 的转换吗?](https://mp.weixin.qq.com/s/jz…
- 面试官:两个 nil 比拟后果是什么?
- 面试官:你能用 Go 写段代码判断以后零碎的存储形式吗?