8

Go中实现手动内存分配的坑

 3 years ago
source link: https://www.zenlife.tk/go-allocator-trap.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go中实现手动内存分配的坑

2016-07-10

你一定想到过,分配一块大的内存,然后从里面切小的对象出来,手动管理对象分配。分配的开销非常小,就是offset加一下。尤其是有些场景,释放时直接把offset重置,就可以重用这块空间了。实现手动内存分配的好处是,减少小对象数目,从而减少垃圾回收时的扫描开销,降低延迟和提升整个性能。

想到不代表做过,做过会踩坑,这篇文章会把你可能要踩的坑都说一遍。不过先说结论:别这么干,不作死就不会死!

TL;DR

开始很容易想用make([]byte)分配空间,如果大小不够时,还可以进行扩容。这是第一个陷阱。

不要append,别让它扩容。一旦发生扩容,会分配一块新的空间,而旧的slice将不再有任何变量引用它,于是会被垃圾回收掉。等等!之前分配的对象还在里面呢,被回收掉岂不傻逼了?

所以建议直接用固定大小的数组,而不是slice。如果想做成可增长的,用一个链表串起来。

const blockSize = 32*1024*1024 - 16
type node struct {
    block [blockSize]byte
    off   int
    next  *node
}
type Allocator {
    head *node
    tail *node
}

初始化是很容易漏掉的地方。重用之前的内存空间,如果忘记了初始化,分配出来的对象不是干净的。

一种方式是C的malloc语义,分配的对象空间就是不初始化的,用户自己去处理。比如:

t := (*T)(ac.Alloc(sizeT))
*t = T{a:3, b:5}
另一种做法可以在Reset的时候把整块空间清除一遍,这样分配出去的都是初始化为零的。

对象内部存在引用

现在分配器的接口是这样子的:

func (ac *Allocator) Alloc(size int) unsafe.Pointer

你觉得没什么问题了,拿它来分配对象,结果使用时却遇到莫名奇妙的内存错误。为什么呢?

假设用它来分配对象T:

type T struct {
    s *S
}
t := (*T)(ac.Alloc(sizeT))
t.s = &S{}

T对象的空间是从一块数组里面划出来的,垃圾回收其实并不知道T这个对象。不过只要Allocator里面的大块内存不被回收,T对象还是安全的。但是,对于T里面的S,它是标准方式分配的,这就会有问题了。

假设发生垃圾回收了,GC会以为那块内存空间就是一个大的数组,而不会被扫描对象T,那么t.s的空间未被任何对象引用到,它会被清理掉。最后t.s就变成一个悬挂指针了!

这样实现的分配器只能处理两种情况,一种是用于分配对象里面不包含其它引用。另一种,对象里包含引用,但引用的对象空间也是在这个分配器里面。

string的处理

我们的分配器不能分配包含引用的对象,这条限制是很严格的。假设T是:

type T struct {
    name string
}

这样子都是不行的!string其实就是典型引用类型,它是一个指针加一个长度,指针指向实现的数据。你明白了吧,这样的约束之后分配器几乎就不可用了。

为了能处理引用,需要改造一下。我们加一个Prevent接口:

func (ac *Allocator) Prevent(v interface{}) {
    ac.ref = append(ac.ref, v)
}
在Allocator里面加一个ref []interface,把引用的对象都加进去,这样子垃圾回收就不会把引用到的数据清掉了。

slice的处理

slice也是引用类型,处理起来更复杂一些。坑也更深,留点空间给大家去想了。

最后,当你把这些都考虑足够充分后,就发现跟初衷相违了。

本希望是一个简单的分配器来手动管理内存,可以减少对象分配,可以减少垃圾回收的扫描—-但是不扫描就可能把还在使用的对象回收掉。为了处理,我们必须把对象的引用再加回去,减少对象扫描的努力成了无用功。再注意到Prevent的接口是interface类型,传参时其实会生成一个临时对象的,于是减少对象分配也没做到。


Recommend

  • 12

    C 语言编程经常需要用到数组,若实现能确定数组的长度,非常好实现数组的定义。但是,如果数组的长度需要经过程序运行后才能确定,动态内存分配就比较合适了。开发时,常用的动态内存分配都是一维的,今天需要用到二维动态内存分配,将过程记录在此...

  • 4
    • www.zenlife.tk 3 years ago
    • Cache

    Go手动内存分配

    2013-10-27Go手动内存分配用Go的时候,有时候又想自己管理内存。所以决定写个手动内存管理的包吧。就当无聊练练手...两级分配。较大内存以页为单位分配,每页4k。分配出去的大块内存只能是1页,2页,3页...较小内存使用buddy算法结合分...

  • 8

    写给前端的手动内存管理基础入门(一)返璞归真:从引用类型到裸指针doodlewind雪碧 | github.com/doodlewind

  • 1
    • www.luozhiyun.com 3 years ago
    • Cache

    详解Go中内存分配源码实现

    详解Go中内存分配源码实现 Posted on 2021年1月30日2021年3月7日 by luozhiyun 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:

  • 10

    C++ malloc出来的内存,可以手动调用构造函数吗?登录后你可以不限量看优质回答私信答主深度交流精彩内容一键收藏查看全部 5 个回答

  • 7

    手动为我的4G内存 lede软路由设置swap(虚拟内存) 2019-04-07 技术 暂无评论

  • 5

    PHP内存管理ZMM(四)-GDB调试php源码并手动调用ZMM相关函数 2018-04-11 本章讲介绍gdb调试php,并手动调用ZMM中申请内存和查找大内存块的函数 _zend_mm_alloc_int ...

  • 2
    • jasonkayzk.github.io 1 year ago
    • Cache

    简单实现C++内存分配跟踪

    有的时候我们想要跟踪我们的代码到底分配了多少的内存,一个常用的方法是使用 Valgrind 工具进行内存分析; 但是对于一些场景,我们不想这么麻烦,那么此时我们可以通过简单的覆盖 malloc、free 等函数实现!

  • 2

    Go 要违背初心吗?新提案:手动管理内存 作者:陈煎鱼 2022-11-15 09:16:59 本提案所提到的 Arena,指的是一种从一个连续的内存区域分配一组内存对象的方式。优点是 arena 中的对象分配通常比一般的内存分配更有效...

  • 2
    • studygolang.com 1 year ago
    • Cache

    Go 语言中手动内存管理

    Go 语言中手动内存管理 1071954237 · 2017-04-19 00:00:19 · 3624 次点击 · 预计阅读时间 2 分钟 · 大约8小时之前 开始浏览    

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK