Go语言的内存对齐

关于内存对齐

内存对齐,即字节对齐,指代码编译后在内存的布局与使用方式。现代计算机多为32位或64位地址对齐,若变量内存未对齐,可能触发总线错误。

为什么需要内存对齐

CPU访问内存是以字长为单位,而非逐个字节。例如32位CPU,字长为4字节,其访问内存单位也是4字节。这种设计旨在减少CPU访问内存次数,提升访问吞吐量。若不进行内存对齐,会增加CPU访问内存次数,降低性能。同时,内存对齐对实现变量的原子性操作也有益处,若变量大小不超过字长,内存对齐后,对该变量的访问就是原子的,这在并发场景下至关重要。

内存对齐带来的影响

内存对齐虽能提升性能,但需付出代价。因变量间增加填充,未存储真实有效数据,故占用内存会更大,属于“空间换时间”策略。

对齐规则

类型 大小
bool 1个字节
intN, uintN, floatN, complexN N/8个字节(例如float64是8个字节)
int, uint, uintptr 1个字
*T 1个字
string 2个字(数据、长度)
[]T 3个字(数据、长度、容量)
map 1个字
func 1个字
chan 1个字
interface 2个字(类型、值)

字长为4字节时,1个字是4字节;字长为8字节时,1个字是8字节。

内存未对齐示例

package performance

import (
	"testing"
)

// 占用32个字节
type person struct {
	hasMoney bool   // 1个字节
	name     string // 16个字节
	age      int16  // 2个字节
}

func Benchmark_Alignment(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = make([]person, b.N)
	}
}
  • hasMoney是第1个字段,对齐倍数1,从位置0开始占据1个字节。
  • name是第2个字段,对齐倍数8,空出7个字节,从位置8开始占据16个字节。
  • age是第3个字段,对齐倍数2,此时内存已对齐,从位置24开始占据2个字节。

运行测试命令:

$ go test -run='^$' -bench=. -count=1 -benchtime=10000x -benchmem > slow.txt

内存对齐示例

package performance

import (
	"testing"
)

// 占用24个字节
type person struct {
	name     string // 16个字节
	age      int16  // 2个字节
	hasMoney bool   // 1个字节
}

func Benchmark_Alignment(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = make([]person, b.N)
	}
}
  • name是第1个字段,对齐倍数8,从位置0开始占据16个字节。
  • age是第2个字段,对齐倍数2,此时内存已对齐,从位置16开始占据2个字节。
  • hasMoney是第3个字段,对齐倍数1,此时内存已对齐,从位置18开始占据1个字节。

运行测试命令:

$ go test -run='^$' -bench=. -count=1 -benchtime=10000x -benchmem > fast.txt

使用benchstat比较差异

$ benchstat -alpha=100 slow.txt fast.txt

输出结果如下:

name          old time/op    new time/op    delta
_Alignment-8    18.1µs ± 0%    15.2µs ± 0%  -15.80%  (p=1.000 n=1+1)

name          old alloc/op   new alloc/op   delta
_Alignment-8     328kB ± 0%     246kB ± 0%  -25.00%  (p=1.000 n=1+1)

name          old allocs/op  new allocs/op  delta
_Alignment-8      1.00 ± 0%      1.00 ± 0%     ~     (all equal)

采用内存对齐方案后,运行时间提升了15%,内存分配优化了25%。

空结构体

空结构体struct{}大小为0。当结构体中字段类型为struct{}时,一般无需内存对齐,但若最后一个字段类型为struct{},则需内存对齐。若内存未对齐且有指针指向结构体最后一个字段,指针对应地址将达结构体之外,虽Go保证无法对该指针进行操作,但若该指针一直存活不释放内存,会产生内存泄露问题。良好实践是:不要将struct{}类型的字段放在结构体最后,以避免内存对齐带来的占用损耗。

内存对齐造成的额外占用示例

package main

import (
	"fmt"
	"unsafe"
)

type t1 struct {
	x int32
	y struct{}
}

type t2 struct {
	y struct{}
	x int32
}

func main() {
	fmt.Printf("size = %d\n", unsafe.Sizeof(t1{}))
	fmt.Printf("size = %d\n", unsafe.Sizeof(t2{}))
}

运行结果:

size = 8
size = 4

struct{}类型的字段从最后一个换到第一个,避免了内存对齐,节省了一半内存使用量。

打 赏