如果你看了上一篇《Go語(yǔ)言開(kāi)發(fā)者的Apache Arrow使用指南:數(shù)據(jù)類型》[1]中的諸多Go操作arrow的代碼示例,你很可能會(huì)被代碼中大量使用的Retain和Release方法搞暈。不光大家有這樣的感覺(jué),我也有同樣的feeling:**Go是GC語(yǔ)言[2],為什么還要借助另外一套R(shí)etain和Release來(lái)進(jìn)行內(nèi)存管理呢**?
在這一篇文章中,我們就來(lái)探索一下這個(gè)問(wèn)題的答案,并看看如何使用Retain和Release,順便再了解一下Apache Arrow的Go實(shí)現(xiàn)原理。
注:本文的內(nèi)容基于Apache Arrow Go v13版本(go.mod中g(shù)o version為v13)的代碼。
1. Go Arrow實(shí)現(xiàn)中的builder模式
看過(guò)第一篇文章中的代碼的童鞋可能發(fā)現(xiàn)了,無(wú)論是Primitive array type還是嵌套類型的諸如List array type,其array的創(chuàng)建套路都是這樣的:
首先創(chuàng)建對(duì)應(yīng)類型的Builder,比如array.Int32Builder;
然后,向Builder實(shí)例中append值;
最后,通過(guò)Builder的NewArray方法獲得目標(biāo)Array的實(shí)例,比如array.Int32。
據(jù)說(shuō)這個(gè)builder模式是參考了Arrow的C++實(shí)現(xiàn)。這里將Go的builder模式中各個(gè)類型之間的關(guān)系以下面這幅示意圖的形式呈現(xiàn)一下:
當(dāng)然這幅圖也大概可以作為Go Arrow實(shí)現(xiàn)的原理圖。
從圖中,我們可以看到:
Arrow go提供了Builder、Array、ArrayData接口作為抽象,在這些接口中都包含了用作內(nèi)存引用計(jì)數(shù)管理的Retain和Release方法;
array包提供了Builder接口的一個(gè)默認(rèn)實(shí)現(xiàn)builder類型,所有的XXXBuilder都組(內(nèi))合(嵌)了這個(gè)類型,這個(gè)類型實(shí)現(xiàn)了Retain方法,Release方法需要XXXBuilder自行實(shí)現(xiàn)。
array包提供了Array接口的一個(gè)默認(rèn)實(shí)現(xiàn)array類型,所有的array type(比如array.Int32)都組(內(nèi))合(嵌)了這個(gè)array類型。該類型實(shí)現(xiàn)了Retain和Release方法。
//?github.com/apache/arrow/go/arrow/array/array.go
type?array?struct?{
????refCount????????int64
????data????????????*Data
????nullBitmapBytes?[]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
}
通過(guò)XXXBuilder類型的NewArray方法可以獲得該Builder對(duì)應(yīng)的Array type實(shí)例,比如:調(diào)用Int32Builder的NewArray可獲得一個(gè)Int32 array type的實(shí)例。一個(gè)array type實(shí)例對(duì)應(yīng)的數(shù)據(jù)是邏輯上immutable的,一旦創(chuàng)建便不能改變。
通過(guò)Array接口的Data方法可以得到該array type的底層數(shù)據(jù)layout實(shí)現(xiàn)(arrow.ArrayData接口的實(shí)現(xiàn)),包括child data。
arrow包定義了所有的數(shù)據(jù)類型對(duì)應(yīng)的ID值和string串,這個(gè)與arrow.DataType接口放在了一個(gè)源文件中。
另外要注意,XXXBuilder的實(shí)例是“一次性”的,一旦調(diào)用NewArray方法返回一個(gè)array type實(shí)例,該XXXBuilder就會(huì)被reset。如果再次調(diào)用其NewArray方法,只能得到一個(gè)空的array type實(shí)例。你可以重用該Builder,只需向該Builder實(shí)例重新append值即可(見(jiàn)下面示例):
//?reuse_string_builder.go
func?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?builder
????bldr.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)
}
輸出上面示例運(yùn)行結(jié)果:
$go?run?reuse_string_builder.go
00000000??03????????????????????????????????????????????????|.|
00000000??03?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000010??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000000??00?00?00?00?05?00?00?00??11?00?00?00?00?00?00?00??|................|
00000010??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000000??68?65?6c?6c?6f?61?70?61??63?68?65?20?61?72?72?6f??|helloapache?arro|
00000010??77?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|w...............|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
["hello"?"apache?arrow"]
00000000??03????????????????????????????????????????????????|.|
00000000??03?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000010??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000000??00?00?00?00?0e?00?00?00??17?00?00?00?00?00?00?00??|................|
00000010??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000000??68?61?70?70?79?20?62?69??72?74?68?64?61?79?6c?65??|happy?birthdayle|
00000010??6f?20?6d?65?73?73?69?00??00?00?00?00?00?00?00?00??|o?messi.........|
00000020??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
00000030??00?00?00?00?00?00?00?00??00?00?00?00?00?00?00?00??|................|
["happy?birthday"?"leo?messi"]
想必到這里,大家對(duì)Arrow的Go實(shí)現(xiàn)原理有了一個(gè)大概的認(rèn)知了。接下來(lái),我們?cè)賮?lái)看Go arrow實(shí)現(xiàn)的內(nèi)存引用計(jì)數(shù)管理。
2. Go Arrow實(shí)現(xiàn)的內(nèi)存引用計(jì)數(shù)管理
在上面圖中,我們看到Go Arrow實(shí)現(xiàn)的幾個(gè)主要接口Builder、Array、ArrayData都包含了Release和Retain方法,也就是說(shuō)實(shí)現(xiàn)了這些接口的類型都支持采用引用計(jì)數(shù)方法(Reference Counting)進(jìn)行內(nèi)存的跟蹤和管理。Retain方法的語(yǔ)義是引用計(jì)數(shù)加1,而Release方法則是引用計(jì)數(shù)減1。由于采用了原子操作對(duì)引用計(jì)數(shù)進(jìn)行加減,因此這兩個(gè)方法是并發(fā)安全的。當(dāng)引用計(jì)數(shù)減到0時(shí),該引用計(jì)數(shù)對(duì)應(yīng)的內(nèi)存塊就可以被釋放掉了。
Go Arrow實(shí)現(xiàn)的主頁(yè)[3]上對(duì)引用計(jì)數(shù)的使用場(chǎng)景和規(guī)則做了如下說(shuō)明:
如果你被傳遞了一個(gè)對(duì)象并希望獲得它的所有權(quán)(ownership),你必須調(diào)用Retain方法。當(dāng)你不再需要該對(duì)象時(shí),你必須調(diào)用對(duì)應(yīng)的Release方法。"獲得所有權(quán)"意味著你希望在當(dāng)前函數(shù)調(diào)用的范圍之外訪問(wèn)該對(duì)象。
你通過(guò)名稱以New或Copy開(kāi)頭的函數(shù)創(chuàng)建的任何對(duì)象,或者在通過(guò)channel接收對(duì)象時(shí),你都將擁有所有權(quán)。因此,一旦你不再需要這個(gè)對(duì)象,你必須調(diào)用Release。
如果你通過(guò)一個(gè)channel發(fā)送一個(gè)對(duì)象,你必須在發(fā)送之前調(diào)用Retain,因?yàn)榻邮照邔碛性搶?duì)象。接收者有義務(wù)在以后不再需要該對(duì)象時(shí)調(diào)用Release。
有了這個(gè)說(shuō)明后,我們對(duì)于Retain和Release的使用場(chǎng)景基本做到心里有譜了。但還有一個(gè)問(wèn)題亟待解決,那就是:Go是GC語(yǔ)言,為何還要在GC之上加上一套引用計(jì)數(shù)呢?
這個(gè)問(wèn)題我在這個(gè)issue[4]中找到了答案。一個(gè)Go arrow實(shí)現(xiàn)的commiter在回答issue時(shí)提到:“理論上,如果你知道你使用的是默認(rèn)的Go分配器,你實(shí)際上不必在你的消費(fèi)者(指的是Arrow Go包 API的使用者)代碼中調(diào)用Retain/Release,可以直接讓Go垃圾回收器管理一切。我們只需要確保我們?cè)趲?kù)內(nèi)調(diào)用Retain/Release,這樣如果消費(fèi)者使用非Go GC分配器,我們就可以確保他們不會(huì)出現(xiàn)內(nèi)存泄漏”。
下面是默認(rèn)的Go分配器的實(shí)現(xiàn)代碼:
package?memory
//?DefaultAllocator?is?a?default?implementation?of?Allocator?and?can?be?used?anywhere
//?an?Allocator?is?required.
//
//?DefaultAllocator?is?safe?to?use?from?multiple?goroutines.
var?DefaultAllocator?Allocator?=?NewGoAllocator()
type?GoAllocator?struct{}
func?NewGoAllocator()?*GoAllocator?{?return?&GoAllocator{}?}
func?(a?*GoAllocator)?Allocate(size?int)?[]byte?{
????buf?:=?make([]byte,?size+alignment)?//?padding?for?64-byte?alignment
????addr?:=?int(addressOf(buf))
????next?:=?roundUpToMultipleOf64(addr)
????if?addr?!=?next?{
????????shift?:=?next?-?addr
????????return?buf[shift?:?size+shift?:?size+shift]
????}
????return?buf[:size:size]
}
func?(a?*GoAllocator)?Reallocate(size?int,?b?[]byte)?[]byte?{
????if?size?==?len(b)?{
????????return?b
????}
????newBuf?:=?a.Allocate(size)
????copy(newBuf,?b)
????return?newBuf
}
func?(a?*GoAllocator)?Free(b?[]byte)?{}
我們看到默認(rèn)的Allocator只是分配一個(gè)原生切片,并且切片的底層內(nèi)存塊要保證64-byte對(duì)齊。
但為什么Retain和Release依然存在且需要調(diào)用呢?這位commiter給出了他理解的幾點(diǎn)原因:
允許用戶控制buffer和內(nèi)部數(shù)據(jù)何時(shí)被設(shè)置為nil,以便在可能的情況下提前標(biāo)記為可被垃圾收集;
如果用戶愿意,允許正確使用不依賴Go垃圾收集器的分配器(比如mallocator實(shí)現(xiàn),它使用malloc/free來(lái)管理C內(nèi)存而不是使用Go垃圾收集來(lái)管理);
雖然用戶可以通過(guò)SetFinalizer來(lái)使用Finalizer進(jìn)行內(nèi)存釋放,但一般來(lái)說(shuō),我們建議最好有一個(gè)顯式的釋放動(dòng)作,而不是依賴finalizer,因?yàn)闆](méi)有實(shí)際保證finalizer會(huì)運(yùn)行。此外,finalizer只在GC期間運(yùn)行,這意味著如果你的分配器正在分配C內(nèi)存或其他東西,而Go內(nèi)存一直很低,那么你有可能在任何finalizer運(yùn)行以實(shí)際調(diào)用Free之前,就被分配了大量的C內(nèi)存,從而耗盡了你的內(nèi)存。
基于這些原因,Go Arrow實(shí)現(xiàn)保留了Retain和Release,雖然有上門(mén)的一些場(chǎng)景使用方法,但這兩個(gè)方法的加入一定程度上增加了Go Arrow API使用的門(mén)檻。并且在重度使用Go Arrow實(shí)現(xiàn)的程序中,大家務(wù)必對(duì)程序做穩(wěn)定性長(zhǎng)測(cè)試驗(yàn)證,以確保memory沒(méi)有l(wèi)eak。
3. 如何實(shí)現(xiàn)ZeroCopy的內(nèi)存數(shù)據(jù)共享
《In-Memory Analytics with Apache Arrow》[5]一書(shū)在第二章中提到了采用Arrow實(shí)現(xiàn)zerocopy的內(nèi)存數(shù)據(jù)共享的原理,這里將其稱為“切片(slice)原理”,用書(shū)中的例子簡(jiǎn)單描述就是這樣的:假設(shè)你想對(duì)一個(gè)有數(shù)十億行的非常大的數(shù)據(jù)集進(jìn)行一些分析操作。提高這種操作性能的一個(gè)常見(jiàn)方法是對(duì)行的子集進(jìn)行并行操作,即僅通過(guò)對(duì)數(shù)組和數(shù)據(jù)緩沖區(qū)進(jìn)行切分,而不需要復(fù)制底層數(shù)據(jù)。這樣你操作的每個(gè)批次都不是一個(gè)副本--它只是數(shù)據(jù)的一個(gè)視圖。書(shū)中還給出了如下示意圖:
右側(cè)切片列中的每個(gè)切片的虛線表示它們只是各自列中的數(shù)據(jù)子集的視圖,每個(gè)切片都可以安全地進(jìn)行并行操作。
array type是邏輯上immutable的,底層data buffer一旦建立后,便可以通過(guò)切片的方式來(lái)以zerocopy方式做內(nèi)存數(shù)據(jù)共享,極大提高了數(shù)據(jù)操作的性能。
4. 小結(jié)
本文介紹了Go arrow實(shí)現(xiàn)的主要結(jié)構(gòu)以及實(shí)現(xiàn)模式:builder模式,并結(jié)合Go arrow官方資料說(shuō)明了采用引用計(jì)數(shù)進(jìn)行內(nèi)存管理的原因與使用方法,最后介紹了Arrow實(shí)現(xiàn)ZeroCopy的內(nèi)存數(shù)據(jù)共享的原理。這些將為后續(xù)繼續(xù)深入學(xué)習(xí)Arrow高級(jí)數(shù)據(jù)類型/結(jié)構(gòu)奠定良好的基礎(chǔ)。
注:本文涉及的源代碼在這里[6]可以下載。
“Gopher部落”知識(shí)星球[7]旨在打造一個(gè)精品Go學(xué)習(xí)和進(jìn)階社群!高品質(zhì)首發(fā)Go技術(shù)文章,“三天”首發(fā)閱讀權(quán),每年兩期Go語(yǔ)言發(fā)展現(xiàn)狀分析,每天提前1小時(shí)閱讀到新鮮的Gopher日?qǐng)?bào),網(wǎng)課、技術(shù)專欄、圖書(shū)內(nèi)容前瞻,六小時(shí)內(nèi)必答保證等滿足你關(guān)于Go語(yǔ)言生態(tài)的所有需求!2023年,Gopher部落將進(jìn)一步聚焦于如何編寫(xiě)雅、地道、可讀、可測(cè)試的Go代碼,關(guān)注代碼質(zhì)量并深入理解Go核心技術(shù),并繼續(xù)加強(qiáng)與星友的互動(dòng)。歡迎大家加入!
著名云主機(jī)服務(wù)廠商DigitalOcean發(fā)布最新的主機(jī)計(jì)劃,入門(mén)級(jí)Droplet配置升級(jí)為:1 core CPU、1G內(nèi)存、25G高速SSD,價(jià)格5$/月。有使用DigitalOcean需求的朋友,可以打開(kāi)這個(gè)鏈接地址[8]:https://m.do.co/c/bff6eed92687 開(kāi)啟你的DO主機(jī)之路。
Gopher Daily(Gopher每日新聞)歸檔倉(cāng)庫(kù) - https://github.com/bigwhite/gopherdaily
我的聯(lián)系方式:
微博(暫不可用):https://weibo.com/bigwhite20xx
微博2:https://weibo.com/u/6484441286
博客:tonybai.com
github: https://github.com/bigwhite
商務(wù)合作方式:撰稿、出書(shū)、培訓(xùn)、在線課程、合伙創(chuàng)業(yè)、咨詢、廣告合作。
參考資料
[1]?
《Go語(yǔ)言開(kāi)發(fā)者的Apache Arrow使用指南:數(shù)據(jù)類型》:?https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1
[2]?Go是GC語(yǔ)言:?https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience
[3]?Go Arrow實(shí)現(xiàn)的主頁(yè):?https://github.com/apache/arrow/tree/main/go
[4]?這個(gè)issue:?https://github.com/apache/arrow/issues/35232
[5]?《In-Memory Analytics with Apache Arrow》:?https://book.douban.com/subject/35954154/
[6]?這里:?https://github.com/bigwhite/experiments/blob/master/arrow/memory-management
[7]?“Gopher部落”知識(shí)星球:?https://wx.zsxq.com/dweb2/index/group/51284458844544文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-516066.html
[8]?鏈接地址:?https://m.do.co/c/bff6eed92687文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-516066.html
到了這里,關(guān)于Go語(yǔ)言開(kāi)發(fā)者的Apache Arrow使用指南:內(nèi)存管理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!