GoMem - 高性能内存分配器库
GoMem 是一个为 Go 语言设计的高性能内存分配器库,从 Monibuca 项目中提取而来。
特性
- 多种分配策略: 支持单树和双树(AVL)分配算法
- 伙伴分配器: 可选的伙伴系统,用于高效的内存池管理
- 可回收内存: 支持内存回收,具有自动清理功能
- 可扩展分配器: 动态增长的内存分配器
- 内存读取器: 高效的多缓冲区读取器,支持零拷贝操作
构建标签
该库支持多个构建标签来自定义行为:
twotree: 使用双树(AVL)实现替代单树 treapenable_buddy: 启用伙伴分配器进行内存池管理disable_rm: 禁用可回收内存功能以减少开销
安装
go get github.com/langhuihui/gomem
使用方法
基本内存分配
package main
import "github.com/langhuihui/gomem"
func main() {
// 创建一个可扩展的内存分配器
allocator := gomem.NewScalableMemoryAllocator(1024)
// 分配内存
buf := allocator.Malloc(256)
// 使用缓冲区...
copy(buf, []byte("Hello, World!"))
// 释放内存
allocator.Free(buf)
}
分段内存释放
package main
import "github.com/langhuihui/gomem"
func main() {
// 创建一个可扩展的内存分配器
allocator := gomem.NewScalableMemoryAllocator(1024)
// 分配一大块内存
buf := allocator.Malloc(1024)
// 使用内存的不同部分
part1 := buf[0:256] // 前256字节
part2 := buf[256:512] // 中间256字节
part3 := buf[512:1024] // 后512字节
// 填充数据
copy(part1, []byte("Part 1 data"))
copy(part2, []byte("Part 2 data"))
copy(part3, []byte("Part 3 data"))
// 分段释放内存 - 可以释放部分内存
allocator.Free(part1) // 释放前256字节
allocator.Free(part2) // 释放中间256字节
// 继续使用剩余内存
copy(part3, []byte("Updated part 3"))
// 最后释放剩余内存
allocator.Free(part3)
}
可回收内存
// 为批量操作创建可回收内存
allocator := gomem.NewScalableMemoryAllocator(1024)
rm := gomem.NewRecyclableMemory(allocator)
// 分配多个缓冲区
buf1 := rm.NextN(128)
buf2 := rm.NextN(256)
// 使用缓冲区...
copy(buf1, []byte("Buffer 1"))
copy(buf2, []byte("Buffer 2"))
// 一次性回收所有内存
rm.Recycle()
内存缓冲区操作
// 创建一个内存缓冲区
mem := gomem.NewMemory([]byte{1, 2, 3, 4, 5})
// 添加更多数据
mem.PushOne([]byte{6, 7, 8})
// 获取总大小和缓冲区数量
fmt.Printf("Size: %d, Buffers: %d\n", mem.Size, mem.Count())
// 转换为字节数组
data := mem.ToBytes()
内存读取器
// 创建一个内存读取器
reader := gomem.NewReadableBuffersFromBytes([]byte{1, 2, 3}, []byte{4, 5, 6})
// 读取数据
buf := make([]byte, 6)
n, err := reader.Read(buf)
// buf 现在包含 [1, 2, 3, 4, 5, 6]
并发安全
⚠️ 重要: Malloc 和 Free 操作必须在同一个协程中调用,以避免竞态问题。为了更优雅的使用,建议使用 gotask,可以在 Start 方法中申请内存,在 Dispose 方法中释放内存。
// ❌ 错误:不同的协程
go func() {
buf := allocator.Malloc(256)
// ... 使用缓冲区
}()
go func() {
allocator.Free(buf) // 竞态条件!
}()
// ✅ 正确:同一个协程
buf := allocator.Malloc(256)
// ... 使用缓冲区
allocator.Free(buf)
// ✅ 优雅:使用 gotask
type MyTask struct {
allocator *gomem.ScalableMemoryAllocator
buffer []byte
}
func (t *MyTask) Start() {
t.allocator = gomem.NewScalableMemoryAllocator(1024)
t.buffer = t.allocator.Malloc(256)
}
func (t *MyTask) Dispose() {
t.allocator.Free(t.buffer)
}
性能考虑
- 在高吞吐量场景中使用
enable_buddy构建标签以获得更好的内存池性能 - 启用 RecyclableMemory 比禁用版本快53%,且内存使用更少
- 仅在不需要内存管理功能时使用
disable_rm构建标签(减少复杂度但牺牲性能) - 单树分配器比双树分配器显著更快(分配操作快77-86%)
- 仅在需要更快查找操作时使用
twotree构建标签(比单树快100%)
基准测试结果
以下基准测试结果在 Apple M2 Pro (ARM64) 和 Go 1.23.0 环境下获得:
单树 vs 双树分配器性能比较
| 操作类型 | 单树 (ns/op) | 双树 (ns/op) | 性能差异 | 胜出者 |
|---|---|---|---|---|
| 基础分配 | 12.33 | 22.71 | 快84% | 单树 |
| 小内存分配 (64B) | 12.32 | 22.60 | 快84% | 单树 |
| 大内存分配 (8KB) | 12.14 | 22.61 | 快86% | 单树 |
| 顺序分配 | 1961 | 3467 | 快77% | 单树 |
| 随机分配 | 12.47 | 23.02 | 快85% | 单树 |
| 查找操作 | 3.03 | 1.51 | 快100% | 双树 |
| 获取空闲大小 | 3.94 | 4.27 | 快8% | 单树 |
关键发现:
- 单树分配器在内存分配操作上快77-86%
- 双树分配器仅在查找操作上快100%
- 由于分配性能更优,推荐在大多数用例中使用单树分配器
RecyclableMemory 性能比较(启用 vs 禁用)
| 操作类型 | 启用 RM (ns/op) | 禁用 RM (ns/op) | 性能差异 | 内存使用 |
|---|---|---|---|---|
| 基础操作 | 335.2 | 511.9 | 快53% | 启用: 1536B/2 allocs, 禁用: 1788B/2 allocs |
| 多个分配 | - | 1035.1 | - | 禁用: 3875B/10 allocs |
| Clone操作 | - | 53.7 | - | 禁用: 240B/1 alloc |
关键发现:
- 启用 RecyclableMemory 在基础操作上快53%
- 启用 RM 内存使用更少(1536B vs 1788B 基础操作)
- 启用 RM 提供真正的内存管理和回收功能
- 禁用 RM 使用简单的
make([]byte, size)无内存池
内存分配器性能(单树)
| 基准测试 | 操作次数/秒 | 每次操作时间 | 内存/操作 | 分配次数/操作 |
|---|---|---|---|---|
| Allocate | 96,758,520 | 15.08 ns | 0 B | 0 |
| AllocateSmall | 98,864,434 | 12.49 ns | 0 B | 0 |
| AllocateLarge | 100,000,000 | 12.65 ns | 0 B | 0 |
| SequentialAlloc | 1,321,965 | 942.2 ns | 0 B | 0 |
| RandomAlloc | 96,241,566 | 12.79 ns | 0 B | 0 |
| GetFreeSize | 303,367,089 | 3.934 ns | 0 B | 0 |
内存操作性能
| 基准测试 | 操作次数/秒 | 每次操作时间 | 内存/操作 | 分配次数/操作 |
|---|---|---|---|---|
| PushOne | 31,982,593 | 35.05 ns | 143 B | 0 |
| Push | 17,666,751 | 70.40 ns | 259 B | 0 |
| ToBytes | 119,496 | 11,806 ns | 106,496 B | 1 |
| CopyTo | 417,379 | 2,905 ns | 0 B | 0 |
| Append | 979,598 | 1,859 ns | 7,319 B | 0 |
| Count | 1,000,000,000 | 0.3209 ns | 0 B | 0 |
| Range | 32,809,593 | 36.08 ns | 0 B | 0 |
内存读取器性能
| 基准测试 | 操作次数/秒 | 每次操作时间 | 内存/操作 | 分配次数/操作 |
|---|---|---|---|---|
| Read | 10,355,643 | 112.4 ns | 112 B | 2 |
| ReadByte | 536,228 | 2,235 ns | 56 B | 2 |
| ReadBytes | 2,556,602 | 608.7 ns | 1,080 B | 18 |
| ReadBE | 408,663 | 3,587 ns | 56 B | 2 |
| Skip | 8,762,934 | 125.8 ns | 56 B | 2 |
| Range | 15,608,808 | 70.99 ns | 80 B | 2 |
| RangeN | 20,101,638 | 79.09 ns | 80 B | 2 |
| LEB128Unmarshal | 356,560 | 3,052 ns | 56 B | 2 |
伙伴分配器性能
| 基准测试 | 操作次数/秒 | 每次操作时间 | 内存/操作 | 分配次数/操作 |
|---|---|---|---|---|
| Alloc | 4,017,826 | 388.2 ns | 0 B | 0 |
| AllocSmall | 3,092,535 | 410.7 ns | 0 B | 0 |
| AllocLarge | 3,723,950 | 276.4 ns | 0 B | 0 |
| SequentialAlloc | 62,786 | 17,997 ns | 0 B | 0 |
| RandomAlloc | 3,249,220 | 357.8 ns | 0 B | 0 |
| Pool | 27,800 | 56,846 ns | 196,139 B | 0 |
| NonPowerOf2 | 3,167,425 | 317.8 ns | 0 B | 0 |
性能总结
- 单树分配器: 极快的分配/释放操作,每次操作约12ns,零内存分配
- 双树分配器: 分配较慢(约23ns每次操作),但查找操作更快(约1.5ns vs 3ns)
- 启用 RecyclableMemory: 比禁用版本快53%,内存效率更高
- 禁用 RecyclableMemory: 实现更简单但性能较慢,内存使用更高
- 内存操作: 高效的缓冲区管理,开销最小
- 内存读取器: 高性能读取,支持零拷贝操作
- 伙伴分配器: 快速的2的幂次分配,支持池化以减少GC压力
推荐:
- 由于分配性能更优,推荐在大多数应用中使用单树分配器(默认)
- 保持 RecyclableMemory 启用(默认)以获得更好的性能和内存效率
- 仅在查找操作关键且频繁时才使用双树分配器
- 仅在不需要内存管理功能时才使用
disable_rm标签