Go-23-arena

最近 Go1.20 更新,中间讲到了一个特性 arena ,这里看看加入 arena 的作用

在Go的内存管理中,arena 其实就是所谓的堆区,然后将这个区域分割成 8KB 大小的页,组合起来称为 mspanmspan就是Go中内存管理的基本单元,是由一片连续的 8KB 的页组成的大块内存。其实 mspan 是一个包含起始地址、规格、页的数量等内容的双端链表

虽然 Go 的垃圾回收机制能够正常的进行内存管理,但是存在以下问题

  1. 垃圾回收机制需要花费大量CPU进行垃圾回收操作
  2. 垃圾回收机制有一定的延迟性,导致花费的内存比实际的内存要大

arena 的优势 允许从连续的内存空间中分配对象并一次性进行释放

注意:在 github arena 的话题中,有一个最新的笔记提醒, 即处在测试阶段的 arena 功能随时可能被去除掉

1
Note, 2023-01-17. This proposal is on hold indefinitely due to serious API concerns. The GOEXPERIMENT=arena code may be changed incompatibly or removed at any time, and we do not recommend its use in production.

带着问题看世界:

  1. 它跟内存池的区别
  2. 由于需要连续的内存空间,那么当需要分配的内存比较多,没有这么大的连续内存空间怎么办?
  3. 如果它分配的对象有一部分存在内存逃逸,那么该如何处理?
  4. 能支持并发吗

基础功能

由于是实验功能,所以需要配置环境变量 GOEXPERIMENT=arenas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "arena"

type T struct{
Foo string
Bar [16]byte
}

func processRequest(req *http.Request) {
// Create an arena in the beginning of the function.
mem := arena.NewArena()
// Free the arena in the end.
defer mem.Free()

// Allocate a bunch of objects from the arena.
for i := 0; i < 10; i++ {
obj := arena.New[T](mem)
}

// Or a slice with length and capacity.
slice := arena.MakeSlice[T](mem, 100, 200)
}

如果想在arena释放时候继续使用它分配的对象,则可以通过 Clone 从堆中浅拷贝一个对象

1
2
3
4
5
obj1 := arena.New[T](mem) // arena-allocated
obj2 := arena.Clone(obj1) // heap-allocated
fmt.Println(obj2 == obj1) // false

mem.Free()

其他接口包括

  • NewArena:创建一个新的 arena 内存空间。
  • Free:释放 arena 及其关联对象。
  • New:基于 arena,创建新对象。
  • MakeSlice:基于 arena,创建新切片。
  • Clone:克隆一个 arena 的对象,并移动到内存堆上。只能是指针、slice或者字符串

实现原理

代码路径:

  1. src/runtime/arena.go
  2. src/arena/arena.go
1
2
3
4
5
6
7
8
9
//arena 表示多个Go一起分配与释放的内存集合,当其中的对象不在被引用那么将会自动释放
type Arena struct {
a unsafe.Pointer
}

// NewArena allocates a new arena.
func NewArena() *Arena {
return &Arena{a: runtime_arena_newArena()}
}

根据描述这里注意点有两个:

  1. arena 分配的对象需要及时释放
  2. 既然是自动释放,然后在使用中 defer arena.Free() 可以任务是,不用等到二次垃圾回收,直接将资源释放,并将可重复使用的mspan放入reused

查看Arena 内部结构

1
2
3
4
5
6
7
8
9
10
11
12
13
type userArena struct {
// 指向一个链表,表示一系列没有足够空闲内存的 mspan(内存段)。当该内存管理区域被释放时,这些 mspan 也会被释放
fullList *mspan //内存组件 mspan

// 指向一个 mspan,表示未满的内存段。这个 mspan 中还有可用的内存可以分配
active *mspan

//一个指向 unsafe.Pointer 类型的切片,用于引用当前内存管理区域的对象。这可以防止在仍然有引用对象时释放该内存管理区域
refs []unsafe.Pointer

//一个原子布尔类型的变量,用于标记内存管理区域是否已经被释放。如果为 true,表示该内存管理区域已经被释放,以避免重复释放
defunct atomic.Bool
}
  • Arena 如果重复释放也没有关系,判断释放过则直接结束

**第一步:**分配一个 Arena

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// newUserArena creates a new userArena ready to be used.
func newUserArena() *userArena {
a := new(userArena)
SetFinalizer(a, func(a *userArena) { //g
// If arena handle is dropped without being freed, then call
// free on the arena, so the arena chunks are never reclaimed
// by the garbage collector.
a.free()
})
a.refill()
return a
}

func (a *userArena) refill() *mspan {
// If there's an active chunk, assume it's full.
s := a.active
//...
var x unsafe.Pointer

// Check the partially-used list.
lock(&userArenaState.lock)
if len(userArenaState.reuse) > 0 {
//当前存在可重用的就使用重用的
}
unlock(&userArenaState.lock)

if s == nil {
// Allocate a new one. 否则分配一个新的mspan
x, s = newUserArenaChunk()
if s == nil {
throw("out of memory")
}
}
a.refs = append(a.refs, x) //记录mspan.base(),报活mspan
a.active = s //记录当前使用的mspan
return s
}
  • SetFinalizer 函数可参考文章,当gc检测到unreachable对象有关联的SetFinalizer函数时,会执行关联的SetFinalizer函数, 同时取消关联。 这样当下一次gc的时候,对象重新处于unreachable状态并且没有SetFinalizer关联, 就会被回收。

**第二步:**从 arena 中分配具体类型的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func (a *userArena) refill() *mspan {
// If there's an active chunk, assume it's full.
s := a.active //上次分配了mspan
if s != nil {
if s.userArenaChunkFree.size() > userArenaChunkMaxAllocBytes {
// It's difficult to tell when we're actually out of memory
// in a chunk because the allocation that failed may still leave
// some free space available. However, that amount of free space
// should never exceed the maximum allocation size.
throw("wasted too much memory in an arena chunk")
}
s.next = a.fullList //将这个mspan放到fullList的链表头部
a.fullList = s
a.active = nil //active置为空
s = nil
}
var x unsafe.Pointer

// Check the partially-used list.
lock(&userArenaState.lock)
if len(userArenaState.reuse) > 0 { //
//如果有可以重用的mspan则放到s中
}
unlock(&userArenaState.lock)
if s == nil {
// Allocate a new one.
x, s = newUserArenaChunk() //否则新分配一个新的
if s == nil {
throw("out of memory")
}
}
a.refs = append(a.refs, x)
a.active = s
return s
}

**第三步:**释放的核心是这块代码

1
2
3
4
5
6
7
8
9
s := a.fullList	//获取这个mspan
i := len(a.refs) - 2
for s != nil { //不为空
a.fullList = s.next //指向下一个节点
s.next = nil
freeUserArenaChunk(s, a.refs[i]) //释放这个mspan
s = a.fullList //指向下一个节点
i--
}
  • 释放的时候仅仅是将fullList中所有的都释放掉了,而active中的则会进去到全局reuse对象中用于下次使用

这个全局变量就是 userArenaState 用于存放可重复使用的mspan以及回收的mspan

1
2
3
4
5
6
7
8
9
var userArenaState struct {
lock mutex

//可重复使用
reuse []liveUserArenaChunk

//回收释放的
fault []liveUserArenaChunk
}

对比Sync.Pool

arena 与 Sync.Pool 同样都是为了解决频繁分配对象和大量对象GC带来的开销

Sync.Pool相同类型的对象,使用完后暂时缓存不GC,下次再有相同的对象分配时直接用之前的缓存的对象,避免频繁创建大量对象。不承诺这些缓存对象的生命周期,GC时会释放之前的缓存,适合解决频繁创建相同对象带来的压力,短时间(两次GC之间)大量创建可能还是会有较大冲击,使用相对简单,但只能用于相同结构创建,不能创建slice等复杂结构

arena手动管理连续内容并统一释放,对象的生命周期完全自己控制,使用相对复杂,支持slice等复杂结构,也不是一个真正意义的连续超大空间,而是通过管理不同的mspan实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"arena"
"sync"
"testing"
)

type MyObj struct {
Index int
}

func BenchmarkCreateObj(b *testing.B) {
b.ReportAllocs()

var p *MyObj

for i := 0; i < b.N; i++ {
for j := 0; j < 1000; j++ {
p = new(MyObj)
p.Index = j
}
}
}

var (
objPool = sync.Pool{
New: func() interface{} {
return &MyObj{}
},
}
)

func BenchmarkCreateObj_SyncPool(b *testing.B) {
b.ReportAllocs()

var p *MyObj

for i := 0; i < b.N; i++ {
for j := 0; j < 1000; j++ {
p = objPool.Get().(*MyObj)
p.Index = 23
objPool.Put(p)
}
}
}

func BenchmarkCreateObj_Arena(b *testing.B) {
b.ReportAllocs()

var p *MyObj

a := arena.NewArena()
defer a.Free()

for i := 0; i < b.N; i++ {

for j := 0; j < 1000; j++ {
p = arena.New[MyObj](a)
p.Index = 23
}
}
}

性能对比的结果

1
2
3
4
cpu: Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
BenchmarkCreateObj-8 100518 11370 ns/op 8000 B/op 1000 allocs/op
BenchmarkCreateObj_SyncPool-8 110017 11523 ns/op 0 B/op 0 allocs/op
BenchmarkCreateObj_Arena-8 80409 15340 ns/op 8032 B/op 0 allocs/op

Sync.Pool 不需要重复分配且每次操作时间短,而Arena执行时间会长一点且每次还是需要分配内存的,因为需要引入新的 mspan

总结

  1. Arena 不支持并发,可以看出操作同一个 arena的时候并不存在锁操作
  2. Arena 强制 Free()之后的对象无法继续使用
  3. 优点:
    • 一旦被释放但仍然被访问则会显示的导致程序错误
    • arena 地址空间除非没有指针指向,否则将不能被重用
    • arena 永远不会被垃圾回收机制回收(如果GC不可达它会执行 SetFinalizer 自己释放掉,那我们手动free的意义在哪里 --> 也就是构建arena的目的,提前释放内存,降低GC扫描频率)

参考文档

  1. https://uptrace.dev/blog/golang-memory-arena.html
  2. https://colobu.com/2022/10/17/a-first-look-at-arena/
  3. https://zhuanlan.zhihu.com/p/604686258