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。

posted @ 2023-05-11 17:44  独揽风月  阅读(343)  评论(0编辑  收藏  举报