Monibuca v5 实现优雅内存分配器
背景
v4
中使用了链表存储了不同大小的内存块的方式进行内存池的实现(参考这篇v4内存复用机制),实际测试中发现内存浪费比较严重,因此如何设计出使用效率高,操作简洁的内存池就成了 v5
的一个任务。
使用 make
使用 go 原生的内存分配,意味着交给 GC
来回收,在m7s
中测试发现gc
占据非常大的开销。
自定义内存分配
C 风格的内存分配
void * mem = malloc(100) free(mem)
这种分配方式最广为人知,也是最简洁易懂的,因此如果能实现这种方式,是最佳的。
设想一下
func (ma *MemoryAllocator) Malloc(size int) (memory []byte) { } func (ma *MemoryAllocator) Free(memory []byte) { }
问题:如何在 Free
的时候知道是哪块内存?如果把这个字节数组直接存储就会回到 v4
的版本,显然不是我们想要的。 我们想要的是在一块大的数组中切割分配,这样才能有效利用内存。
切片分配
假设有一个大数组,用来缓存内存,防止 GC
var mem = make([]byte,65535)
分配内存,就是切片
s1 := mem[0:1024]
分配第二块内存s2
s2 := mem[1024:2048]
最简单的方式就是记录一个已经分配的索引,第一次为 0
,第二次为 1024
type MemoryAllocator struct { start int64 memory []byte } func (ma *MemoryAllocator) Malloc(size int) (memory []byte) { memory = ma.memory[ma.start:size] ma.start+= size return }
回收
内存切出去容易,如何回收呢?
ma.Free(s2)
如何知道 s2
属于哪一部分呢?即使知道,如何修改原来的结构体使得下次分配可以利用回收过的内存呢?
使用附加信息
这种方式,就和 v4
一样,将额外的信息随同分配的内存给出去,回收的时候再一起带回来,但是不够简洁,我们希望回收的时候就是传[]byte
判断指针
我们知道同一块内存的底层的指针值肯定是相同的,即使切片被浅复制也一样。
ptr := uintptr(unsafe.Pointer(&mem[0]))
当然,有可能传入的切片长度为 0,可以用下面的方法规避
ptr := uintptr(unsafe.Pointer(&mem[:1][0]))
有了这个指针值,我只需要和内存池的起始指针进行比较,就可以得到在内存池中的偏移。从而为回收奠定基础。
标记
为了回收再利用,需要对可以分配的内存信息进行存储,这个信息就是标记偏移地址段,即 start:end。 可以用链表存储,[start,end],[start,end],[start,end],[start,end]
每一段代表可用的内存。当回收内存时,只需按照大小顺序插入这个链表即可
用数组也可以,但是由于数组对随机插入性能较差,因此用链表更合适
当然如果前一个 end 等于下一个 start,就可以合并: 例如[1:1024],[1024,2048]
就可以合并成 [1:2048]
,相当于碎片整理。
实现
type MemoryAllocator struct { start int64 memory []byte Size int blocks *List[Block] } func NewMemoryAllocator(size int) (ret *MemoryAllocator) { ret = &MemoryAllocator{ Size: size, memory: make([]byte, size), blocks: NewList[Block](), } ret.start = int64(uintptr(unsafe.Pointer(&ret.memory[0]))) ret.blocks.PushBack(Block{0, size}) return }
具体代码可以到仓库 github.com/langhuihui/monibuca
的 v5
分支里面找到
进阶
单个内存分配器可分配的内存有限,那么一个可以不断增长的需求如何满足呢? 可以实现动态创建内存分配器的高阶内存分配器就可以解决了
type ScalableMemoryAllocator []*MemoryAllocator
原理也很简单,不够就创建,Free 的时候就挨个查找。 优化后内存不再呈现锯齿状了

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Kafka 线程模型痛点攻克: 提升分区写入 2 倍性能
01 引言 单分区写入在一些需要全局顺序消息的场景中具备重要应用价值。在一些严格保序场景下,需要将分区数设置为 1,并且只用单个生产者来发送数据,从而确保消费者可以按照原始顺序读取所有数据。此时,Kafka 的单分区写入性能将会决定整个系统的吞吐上限。在我们的实践中发现,Kafka 由于其本身线程模型实现上的制约,并没有将单分区写入性能的极限发挥出来。本文今天将具体解读 Kafka 线程模型的不足以及 AutoMQ 如何对其进行改进优化,从而实现更好的单分区写入性能。 02 Apache Kafka 串行处理模型解析 Apache Kafka 的串行处理模型网络框架主要由 5 个类组成: 1. SocketServer:网络框架的核心类,包含 Acceptor 和 Processor 部分 Acceptor:监听端口,处理新建连接请求,并将连接分发给 Processor; Processor:网络线程,通过num.network.threads 配置数量。单个 TCP 连接有且只有一个 Processor 负责,Processor#run 方法驱动连接后续的生命周期管理,从网络解析请求...
- 下一篇
用three.js做一个3D汉诺塔游戏(下)
接上期:《用three.js做一个3D汉诺塔游戏(上)》 在上一期,我们成功地搭建了基础的 3D 场景。在本期中,我们将对场景进行优化,使其在视觉上更加真实,并为场景中的物体添加交互,同时编写游戏流程控制逻辑,最终完成这款3D汉诺塔游戏。 为桌台添加材质纹理 为物体添加适当的材质纹理,可以使其视觉效果产生质的飞跃。接下来,我们将为桌台添加一种木质纹理,用到的纹理贴图来自Pixabay.com。 我们使用 TextureLoader 来加载纹理贴图,其 load 方法第1个参数为贴图的 URL 字符串,该方法返回一个纹理对象,可直接赋值给材质对象的颜色贴图属性 map。代码实现如下: class Table { constructor({ width, height, depth }) { const geometry = new THREE.BoxGeometry(width, height, depth); // 纹理贴图 const url = 'https://cdn.pixabay.com/photo/2016/12/26/13/47/fresno-1932211_1280.j...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群