2012-11-09

关于Go内存的一点事

下午花了点时间研究Golang如何手工释放内存.

起因是玩Go的时候习惯性地会看下生存的plan9代码是怎样的,借此了解一些实现代码上不太一目了然的东西,但时不时地会看到若干memory allocation的动作.
作为一个写Java出身,长期纠结于内存不可触摸的人,到了这个时候,总归会有些心痒.
而且,确实,Golang本身的GC机制也不算特别好.

Golang的GC也是经典的mark-and-sweep,跟Java实现不太一样的是,Golang还是有stack的分别的.
所以多数时候的GC其实是爬栈上数据,然后一路延伸下去.

剩下的"suspended" Goroutine里的数据了也大致是这个思路.

毕竟,goroutine本身就算是一个带着context的stack.
跟传统意义不同点在于,它的内存区域是go自己管理的而已.

而Go的GC更微妙的地方在于,它是一个基本上是定时的GC.
在main起来之前会有一个所谓的MHeap_Scavenger的goroutine.
原则上来说,是2分钟出发一次GC.
而且这个值在目前来说并非可配置的.

更重要的是,它本身是一个goroutine.
也即是说,要有机会被调度到.
而目前貌似默认是只有一个M,也即内部go线程在跑的.
所以,假如是有比较频繁的内存申请操作的话,大概内存占用会涨地比较快.

当然,如果内存使用频率基本不变的话,内存也够大话,理论上到一定程度就会停止增长了.
但同样地,占用也会保持现状.

于是考虑下Go会申请内存的场景.

最简单的slice操作,这个就是必然的.
而对于一般对象,如果escape来的话,也必然会触发.

这么考虑的话,其实还算挺频繁的.

于是,很自然地会想如何去人工介入.

在这一点上来说,Go应该说是相当优秀的.
如果只是单纯的想使用脱离go管理的内存,那么直接cgo,然后malloc/free即可.

这里不得不说的就是Golang和C交互的便利性.

从go1开始引入的go commands基本上把go变得很便利了.
之前的诸如cgo之类的,可能还有一些繁琐的步骤要做.
而现在,直接在go代码里嵌C代码即可了.
更为主要的是,没有makefile和异与go toolchain的东西.

换句话说,就是和写纯go是没什么区别了的.

所以,手工管理内存或者跟现有生态链融合的话,基本上不算什么难事.
只要对方有C接口即可.
而这个世界上,没有C接口的东西,还真不太多.

但对于go自己的数据结构,能否也可以手动释放呢,比如逃逸了的slice?

这个其实也是可以的.
而且也很简单,不需要hack什么东西.
诸如算偏移量和改相应对象的header mask之类的.

能够想办法调用runtime·malloc系和runtime·free即可了.

普通的cgo可能无法调用这一类函数,原因在于"·"这个符号.

但是参考下go自己的bootstrap和build过程,会发觉,"·"本身是个比较有趣的东西.

在目前Go的实现里,有些Go func虽然有定义,但是却没有对于的Go实现.
因为本体是C实现的.
而go就是用前缀"·"来关联Go定义和C实现的.

而更重要的是,这些C实现的编译是走的比较独立的流程,而不是cgo生成的.
也就是说,只要在Go里定义一个wrap,然后C,再一起简单地go build就完成了.

这里需要注意的另外一点就是Go的C conversion.

因为原则上来说,Go是支持多返回值的,而这个在C里是没有的.

Go是如何实现的呢?
稍微犯下代码就明显了.
返回值不过是额外的传入参数而已.

这样原则上来说就不存在多返回值这个伪命题了.

而要如何把结果返回呢?
当然就是指针操作了.

这些,参考相应的makeslice之类的实现就明白了.

简单说就是,众所周知地,在调用前,参数会push到stack里.
换句话说,在函数内部,只要对对应区域操作即可返回值了.
类似于C#的out关键字的意思吧.

所以,实际上也就是4,5行代码的事情.

爽文

去看了好东西. 坦白说,多少是带着点挑刺的味道去的. 毕竟打着爱情神话和女性题材的气质,多多少少是热度为先了. 看完之后倒是有些新的想法. 某种程度上来说,现在的年轻人或者说声音就像小叶. 只要说点贴心的话就能哄好. 也是那种可以不用很努力了. 留在自己的舒适区避难所小圈子抱团就...