Go的手动内存管理方案

Arena方案

Arena 方案是 Go 语言中一种用于内存管理的技术,在 Go 1.20 版本引入了 arena 包,需要开启goexperiment.arenas实验特性来启用。Arena 本质上是一个连续的内存区域,你可以在这个区域内分配对象,并且这些对象的生命周期会与 Arena 绑定。在Google内部的实验中,arena可以节省15%的CPU和内存开销。

arena 包提供了一种能力,能够为一组 Go 语言的值分配内存,并可以安全地一次性手动释放这些内存空间。这个功能的目的在于提高效率:在垃圾回收(GC)之前手动释放内存,可以延迟垃圾回收的周期。垃圾回收周期不那么频繁,意味着垃圾回收器占用 CPU 的开销也会不那么频繁。

这个包中的功能主要体现在 Arena 类型中。Arena 会一次性分配大块内存,所以如果只是分配少量的小对象,使用 Arena 也不是最优的方案。Arena 最适合批量使用的场景,每次使用时所分配的内存量通常在MB级别。

注意,由于支持这种手动的内存分配,对于常规的 Go 对象,可能会出现释放后使用(use-after-free,也就是野指针)的错误用法,这种用法,目前官方没有严格保证安全性。

github.com/golang/go/i...

如何使用Arena Api

Arena的Api直接位于src目录下,是go sdk的一级package。

Arena的数据结构非常简单,只有一个指针,指向arena所持有的内容块。

rust 复制代码
type Arena struct {
    a unsafe.Pointer
}

然而实际在runtime里,Arena的结构也比较简单,主要的两个字段:

  1. fullList *mspan,完全使用的chunk。
  2. active *mspan,当前正在使用的chunk。
rust 复制代码
type userArena struct {
    fullList *mspan
    active *mspan
    refs []unsafe.Pointer
    defunct atomic.Bool
}

开放的几个函数和方法包括:

NewArena

  • NewArena 新建Arena,用于管理一块连续的内存空间,注意Arena指针的管理依然遵循GC的原则,也就是如果Arena自身不被引用,那么就会被GC回收掉,所以可能得借助runtime.KeepAlive用于保活。
csharp 复制代码
// NewArena allocates a new arena.
func NewArena() *Arena {
    return &Arena{a: runtime_arena_newArena()}
}

Free

  • .Free(),释放整个Arena占有的空间,这里释放只清理了元信息,不会对Arena持有的完整内存空间做清理操作。
scss 复制代码
// Free frees the arena (and all objects allocated from the arena) so that
// memory backing the arena can be reused fairly quickly without garbage
// collection overhead. Applications must not call any method on this
// arena after it has been freed.
func (a *Arena) Free() {
    runtime_arena_arena_Free(a.a)
    a.a = nil
}

注意一个arena只能调用一次Free,如果调用2次,会发生panic。

New

  • New,泛型函数,从给定的Arena中申请T类型的空间,返回*T类型。
scss 复制代码
// New creates a new *T in the provided arena. The *T must not be used after
// the arena is freed. Accessing the value after free may result in a fault,
// but this fault is also not guaranteed.
func New[T any](a *Arena) *T {
    return runtime_arena_arena_New(a.a, reflectlite.TypeOf((*T)(nil))).(*T)
}

MakeSlice

  • MakeSlice,泛型函数,类似New,不过申请的是数组的空间。
go 复制代码
// MakeSlice creates a new []T with the provided capacity and length. The []T must
// not be used after the arena is freed. Accessing the underlying storage of the
// slice after free may result in a fault, but this fault is also not guaranteed.
func MakeSlice[T any](a *Arena, len, cap int) []T {
    var sl []T
    runtime_arena_arena_Slice(a.a, &sl, cap)
    return sl[:len]
}

Clone

  • Clone,将T从Arena的空间中,浅拷贝一份到堆中,适用于对象需要从Arena中逃逸的场景,避免直接引用原始对象导致野指针问题,注意T只能是指针、字符串或切片。
scss 复制代码
// Clone makes a shallow copy of the input value that is no longer bound to any
// arena it may have been allocated from, returning the copy. If it was not
// allocated from an arena, it is returned untouched. This function is useful
// to more easily let an arena-allocated value out-live its arena.
// T must be a pointer, a slice, or a string, otherwise this function will panic.
func Clone[T any](s T) T {
    return runtime_arena_heapify(s).(T)
}

Api的源码非常简单,但是很多核心逻辑,其实是用linkname实现在了runtime中,比如我们上边介绍的几个Api,以及反射包中的Api reflect.ArenaNew。

swift 复制代码
在入参的Arena中申请一个typ对象并返回
//go:linkname reflect_arena_New reflect.arena_New
func reflect_arena_New(a *Arena, typ any) any {
    return runtime_arena_arena_New(a.a, typ)
}

//go:linkname runtime_arena_newArena
func runtime_arena_newArena() unsafe.Pointer

//go:linkname runtime_arena_arena_New
func runtime_arena_arena_New(arena unsafe.Pointer, typ any) any

// Mark as noescape to avoid escaping the slice header.
//
//go:noescape
//go:linkname runtime_arena_arena_Slice
func runtime_arena_arena_Slice(arena unsafe.Pointer, slice any, cap int)

//go:linkname runtime_arena_arena_Free
func runtime_arena_arena_Free(arena unsafe.Pointer)

//go:linkname runtime_arena_heapify
func runtime_arena_heapify(any) any

但是Arena本质上还是堆上的一段空间,如何避免被GC回收掉,如何避免GC的扫描成本,也是非常复杂的,这部分的完整实现,在runtime中有部分独立的实现,并且用userArena集成在GC整体的实现中,在runtime中的独立实现在arena.go中,go1.21的版本是1000行代码。

适用场景

官方的介绍里适用在一段时间内申请大量 内存 空间 ,然后统一释放掉,所以其实几个适用场景也比较明确,例如:

  1. http/rpc handler等无状态的访问接口,io密集型的服务,在处理完请求并返回后,所有在请求期间的对象都可以统一释放掉
  2. json/protobuf/thrift等序列化框架,在处理完序列化/反序列化后,一些中间对象其实都可以统一释放掉

不适用的场景其实也就是不需要大量对象申请、统一释放的场景,例如

  1. 也就是cpu密集型的服务,不涉及大量对象的申请
  2. cache型的服务,对象的生命周期不固定

风险

Arena的风险其实也是手动内存管理语言(例如C/C++)可能会遇到的一些问题:

  1. 内存泄露:因为依赖手动内存管理,若使用不当,未及时释放Arena,会导致内存泄露。
  2. 野指针:在Arena释放后依然引用Arena分配的空间,会导致指针访问有问题的空间。

参考文档

  1. proposal: github.com/golang/go/i...
  2. source code:github.com/golang/go/r...
相关推荐
希陌ximo17 分钟前
GPU选型大对决:4090、A6000、L40谁才是AI推理的最佳拍档?
人工智能·算法·支持向量机·排序算法·推荐算法·迭代加深
编程轨迹18 分钟前
剖析 Java 23 特性:深入探究最新功能
后端
洛小豆24 分钟前
一个场景搞明白Reachability Fence,它就像一道“结账前别走”的红外感应门
java·后端·面试
IceTeapoy34 分钟前
【RL】强化学习入门(一):Q-Learning算法
人工智能·算法·强化学习
郝同学的测开笔记35 分钟前
云原生探索系列(十六):Go 语言锁机制
后端·云原生·go
艾醒37 分钟前
探索大语言模型(LLM):ReAct、Function Calling与MCP——执行流程、优劣对比及应用场景
算法
智者知已应修善业38 分钟前
2021-11-14 C++三七二十一数
c语言·c++·经验分享·笔记·算法·visual studio
艾醒1 小时前
探索大语言模型(LLM):Transformer 与 BERT从原理到实践
算法
七月丶1 小时前
🛠 用 Node.js 和 commander 快速搭建一个 CLI 工具骨架(gix 实战)
前端·后端·github
艾醒1 小时前
探索大语言模型(LLM):循环神经网络的深度解析与实战(RNN、LSTM 与 GRU)
算法