Go语言开发者的Apache Arrow使用指南:内存管理

如果你看了上一篇《Go语言开发者的Apache Arrow使用指南:数据类型》[1]中的诸多Go操作arrow的代码示例,你很可能会被代码中大量使用的RetAIn和Release方法搞晕 。不光大家有这样的感觉,我也有同样的feeling:**Go是GC语言[2],为什么还要借助另外一套Retain和Release来进行内存管理呢**?
在这一篇文章中,我们就来探索一下这个问题的答案,并看看如何使用Retain和Release,顺便再了解一下Apache Arrow的Go实现原理 。

注:本文的内容基于Apache Arrow Go v13版本(go.mod中go version为v13)的代码 。
1. Go Arrow实现中的builder模式看过第一篇文章中的代码的童鞋可能发现了,无论是Primitive array type还是嵌套类型的诸如List array type,其array的创建套路都是这样的:
  • 首先创建对应类型的Builder,比如array.Int32Builder;
  • 然后,向Builder实例中Append值;
  • 最后,通过Builder的NewArray方法获得目标Array的实例,比如array.Int32 。
据说这个builder模式是参考了Arrow的C++实现 。这里将Go的builder模式中各个类型之间的关系以下面这幅示意图的形式呈现一下:
Go语言开发者的Apache Arrow使用指南:内存管理

文章插图
图片
当然这幅图也大概可以作为Go Arrow实现的原理图 。
从图中,我们可以看到:
  • Arrow go提供了Builder、Array、ArrayData接口作为抽象,在这些接口中都包含了用作内存引用计数管理的Retain和Release方法;
  • array包提供了Builder接口的一个默认实现builder类型,所有的XXXBuilder都组(内)合(嵌)了这个类型,这个类型实现了Retain方法,Release方法需要XXXBuilder自行实现 。
  • array包提供了Array接口的一个默认实现array类型,所有的array type(比如array.Int32)都组(内)合(嵌)了这个array类型 。该类型实现了Retain和Release方法 。
// Github.com/apache/arrow/go/arrow/array/array.gotype array struct {refCountint64data*DatanullBitmapBytes []byte}// Retain increases the reference count by 1.// Retain may be called simultaneously from multiple goroutines.func (a *array) Retain() {atomic.AddInt64(&a.refCount, 1)}// Release decreases the reference count by 1.// Release may be called simultaneously from multiple goroutines.// When the reference count goes to zero, the memory is freed.func (a *array) Release() {debug.Assert(atomic.LoadInt64(&a.refCount) > 0, "too many releases")if atomic.AddInt64(&a.refCount, -1) == 0 {a.data.Release()a.data, a.nullBitmapBytes = nil, nil}}下面以Int64 array type为例:
// github.com/apache/arrow/go/arrow/array/numeric.gen.go // A type which represents an immutable sequence of int64 values.type Int64 struct {array // “继承”了array的Retain和Release方法 。values []int64}
  • 通过XXXBuilder类型的NewArray方法可以获得该Builder对应的Array type实例,比如:调用Int32Builder的NewArray可获得一个Int32 array type的实例 。一个array type实例对应的数据是逻辑上immutable的,一旦创建便不能改变 。
  • 通过Array接口的Data方法可以得到该array type的底层数据layout实现(arrow.ArrayData接口的实现),包括child data 。
  • arrow包定义了所有的数据类型对应的ID值和string串,这个与arrow.DataType接口放在了一个源文件中 。
  • 另外要注意,XXXBuilder的实例是“一次性”的,一旦调用NewArray方法返回一个array type实例,该XXXBuilder就会被reset 。如果再次调用其NewArray方法,只能得到一个空的array type实例 。你可以重用该Builder,只需向该Builder实例重新append值即可(见下面示例):
// reuse_string_builder.gofunc main() {bldr := array.NewStringBuilder(memory.DefaultAllocator)defer bldr.Release()bldr.AppendValues([]string{"hello", "apache arrow"}, nil)arr := bldr.NewArray()defer arr.Release()bitmaps := arr.NullBitmapBytes()fmt.Println(hex.Dump(bitmaps))bufs := arr.Data().Buffers()for _, buf := range bufs {fmt.Println(hex.Dump(buf.Buf()))}fmt.Println(arr)// reuse the builderbldr.AppendValues([]string{"happy birthday", "leo messi"}, nil)arr1 := bldr.NewArray()defer arr1.Release()bitmaps1 := arr1.NullBitmapBytes()fmt.Println(hex.Dump(bitmaps1))bufs1 := arr1.Data().Buffers()for _, buf := range bufs1 {if buf != nil {fmt.Println(hex.Dump(buf.Buf()))}}fmt.Println(arr1)}输出上面示例运行结果:


推荐阅读