golang内存对齐
内存对齐的概念
为了减少cpu的访存次数,提高cpu的吞吐量,cpu并不会逐个字节的访问内存,而是以机器字/字长(word size)为单位访问。比如 64 位架构的 CPU ,字长为 8 字节(8B),那么 CPU 访问内存的单位也是 8 字节。但是,如果被访问的数据在内存中的起始地址不是字长的倍数的话,反而有可能增加cpu的访存次数
内存对齐说白了就是给数据分配一个合理的起始地址,比如在64位系统中给int64类型的值分配的内存块起始地址就必须是字长的整数倍,才能保证cpu一次访存就能取出该类型对应值。
比如这个值的起始地址为0(8的倍数),那么它恰好就能被cpu的一次访问就全部取出:
如果这个值所在的起始地址为1(不是8的倍数),那么cpu需要两次访问内存才能将这个值取出。第一次先取这个值的低7个byte,即第1byte到第7byte,第二次再取第8byte,这等于是白白的浪费掉了一倍的cpu性能:
内存对齐实现方式
对齐值/对齐边界
先看下golang里各数据类型的占用内存的大小,以64位系统为例:
类型 | 内存占用(字节) | 对齐值 |
---|---|---|
bool | 1 | 1 |
int/uinit | 8 | 8 |
int8 | 1 | 1 |
int16 | 2 | 4 |
int32 | 4 | 8 |
int64 | 8 | 8 |
string | 16 | 8 |
[]T | 24 | 8 |
map | 8 | 8 |
chan | 8 | 8 |
func | 8 | 8 |
interface | 16 | 8 |
- 通过
unsafe.Sizeof
可以查看变量占用的内存大小 - 通过
unsafe.Alignof
可以查看变量的对齐值,最大不超过一个机器字
- 通过
unsafe.Offsetof
可以查看变量在内存中其实的偏移量
示例
package main
import (
"fmt"
"unsafe"
)
type X struct {
a bool
b int16
c []int
}
func main() {
var x = X{
true,
1,
[]int{1, 2, 3},
}
fmt.Println(unsafe.Sizeof(x)) // 32
fmt.Println(unsafe.Sizeof(x.a)) // 1
fmt.Println(unsafe.Sizeof(x.b)) // 2
fmt.Println(unsafe.Sizeof(x.c)) // 24
fmt.Println("alignof")
fmt.Println(unsafe.Alignof(x.a)) // 1
fmt.Println(unsafe.Alignof(x.b)) // 2
fmt.Println(unsafe.Alignof(x.c)) // 8
fmt.Println("offsetof")
fmt.Println(unsafe.Offsetof(x.a)) // 0
fmt.Println(unsafe.Offsetof(x.b)) // 2
fmt.Println(unsafe.Offsetof(x.c)) // 8
}
问:unsafe.Sizeof(x)的返回值是多少?
如果根据基础数据类型的大小我们可以知道x的大小应该是sizeof(a) + sizeof(b) + sizeof(c) = 1 + 2 + 24 = 27,但是由于内存对齐的存在其实答案应该是8+ 24 = 32,如下图所示:
struct的内存对齐
如果改变struct中字段的顺序,那么内存分配肯能会不一样,比如改成这样:
type X struct {
a bool
b []int
c int16
}
内存中的分配如下:
可以看到内存占用变成40了。
伪共享False Sharing
cpu在处理内存中的数据时,为了匹配内存的速度差异,都会使用缓存技术,cpu现从缓存中读取数据,如果没有则从内存中读取。以cacheline
为例,它有以下特性:
-
在cpu的Cache中数据是以缓存行(CacheLine)为单位进行存储的
-
cpu取缓存都是按照一行为最小单位操作的
-
在64位cpu中Cacheline的大小为64byte
现在假设这样一个场景:
变量a和b都分配在一个cacheline中,此时有两个协程同时修改这两个变量,协程1对应的cpu1处理变量
a,协程2对应的cpu2处理变量b,这两个cpu将会同时去覆盖刷新 CacheLine,造成 Cacheline 的反复失效,那 CPU Cache 将失去了作用
解决方法
为了解决这个问题,可以使用填充技术,使变量尽量单独分配到一个cacheline中,而不用和其他变量共享,比如sync.pool
中的实现:
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
为了防止出现 false sharing 问题,主动使用 pad 的方式凑齐 128 个 byte,这样就不会和其他 P 的 poolLocal 共享一套 CacheLine。