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

news2024/9/29 9:26:47

如果你看了上一篇《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模式中各个类型之间的关系以下面这幅示意图的形式呈现一下:

db5564800f50a9aed5d569d3336cac8f.png

当然这幅图也大概可以作为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.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
}
  • 通过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.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)
}

输出上面示例运行结果:

$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"]

想必到这里,大家对Arrow的Go实现原理有了一个大概的认知了。接下来,我们再来看Go arrow实现的内存引用计数管理。

2. Go Arrow实现的内存引用计数管理

在上面图中,我们看到Go Arrow实现的几个主要接口Builder、Array、ArrayData都包含了Release和Retain方法,也就是说实现了这些接口的类型都支持采用引用计数方法(Reference Counting)进行内存的跟踪和管理。Retain方法的语义是引用计数加1,而Release方法则是引用计数减1。由于采用了原子操作对引用计数进行加减,因此这两个方法是并发安全的。当引用计数减到0时,该引用计数对应的内存块就可以被释放掉了。

Go Arrow实现的主页[3]上对引用计数的使用场景和规则做了如下说明:

  • 如果你被传递了一个对象并希望获得它的所有权(ownership),你必须调用Retain方法。当你不再需要该对象时,你必须调用对应的Release方法。"获得所有权"意味着你希望在当前函数调用的范围之外访问该对象。

  • 你通过名称以New或Copy开头的函数创建的任何对象,或者在通过channel接收对象时,你都将拥有所有权。因此,一旦你不再需要这个对象,你必须调用Release。

  • 如果你通过一个channel发送一个对象,你必须在发送之前调用Retain,因为接收者将拥有该对象。接收者有义务在以后不再需要该对象时调用Release。

有了这个说明后,我们对于Retain和Release的使用场景基本做到心里有谱了。但还有一个问题亟待解决,那就是:Go是GC语言,为何还要在GC之上加上一套引用计数呢

这个问题我在这个issue[4]中找到了答案。一个Go arrow实现的commiter在回答issue时提到:“理论上,如果你知道你使用的是默认的Go分配器,你实际上不必在你的消费者(指的是Arrow Go包 API的使用者)代码中调用Retain/Release,可以直接让Go垃圾回收器管理一切。我们只需要确保我们在库内调用Retain/Release,这样如果消费者使用非Go GC分配器,我们就可以确保他们不会出现内存泄漏”。

下面是默认的Go分配器的实现代码:

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) {}

我们看到默认的Allocator只是分配一个原生切片,并且切片的底层内存块要保证64-byte对齐。

但为什么Retain和Release依然存在且需要调用呢?这位commiter给出了他理解的几点原因:

  • 允许用户控制buffer和内部数据何时被设置为nil,以便在可能的情况下提前标记为可被垃圾收集;

  • 如果用户愿意,允许正确使用不依赖Go垃圾收集器的分配器(比如mallocator实现,它使用malloc/free来管理C内存而不是使用Go垃圾收集来管理);

  • 虽然用户可以通过SetFinalizer来使用Finalizer进行内存释放,但一般来说,我们建议最好有一个显式的释放动作,而不是依赖finalizer,因为没有实际保证finalizer会运行。此外,finalizer只在GC期间运行,这意味着如果你的分配器正在分配C内存或其他东西,而Go内存一直很低,那么你有可能在任何finalizer运行以实际调用Free之前,就被分配了大量的C内存,从而耗尽了你的内存。

基于这些原因,Go Arrow实现保留了Retain和Release,虽然有上门的一些场景使用方法,但这两个方法的加入一定程度上增加了Go Arrow API使用的门槛。并且在重度使用Go Arrow实现的程序中,大家务必对程序做稳定性长测试验证,以确保memory没有leak。

3. 如何实现ZeroCopy的内存数据共享

《In-Memory Analytics with Apache Arrow》[5]一书在第二章中提到了采用Arrow实现zerocopy的内存数据共享的原理,这里将其称为“切片(slice)原理”,用书中的例子简单描述就是这样的:假设你想对一个有数十亿行的非常大的数据集进行一些分析操作。提高这种操作性能的一个常见方法是对行的子集进行并行操作,即仅通过对数组和数据缓冲区进行切分,而不需要复制底层数据。这样你操作的每个批次都不是一个副本--它只是数据的一个视图。书中还给出了如下示意图:

f01dc403fdea09aadb4e7047fc434245.png

右侧切片列中的每个切片的虚线表示它们只是各自列中的数据子集的视图,每个切片都可以安全地进行并行操作。

array type是逻辑上immutable的,底层data buffer一旦建立后,便可以通过切片的方式来以zerocopy方式做内存数据共享,极大提高了数据操作的性能。

4. 小结

本文介绍了Go arrow实现的主要结构以及实现模式:builder模式,并结合Go arrow官方资料说明了采用引用计数进行内存管理的原因与使用方法,最后介绍了Arrow实现ZeroCopy的内存数据共享的原理。这些将为后续继续深入学习Arrow高级数据类型/结构奠定良好的基础。

注:本文涉及的源代码在这里[6]可以下载。


“Gopher部落”知识星球[7]旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

a53187582e5e325f5c813162ee76e70b.jpeg

94b09a1a08c869c2f402cb6b8bf6d1e0.png

7f9e8e5c9a42551fbfb318c1bf7c0cc2.png

808258b0cd2a9df0d82ee3246a2bdc25.jpeg

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[8]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx

  • 微博2:https://weibo.com/u/6484441286

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

3147d45759273d4690d51943fc70bfb1.png

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1] 

《Go语言开发者的Apache Arrow使用指南:数据类型》: https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1

[2] 

Go是GC语言: https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience

[3] 

Go Arrow实现的主页: https://github.com/apache/arrow/tree/main/go

[4] 

这个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部落”知识星球: https://wx.zsxq.com/dweb2/index/group/51284458844544

[8] 

链接地址: https://m.do.co/c/bff6eed92687

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/711706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MWCS 2023,到底有些啥?(下篇)

█ 亚信科技 5G行业专网一体机: 反光太厉害了,看不太清: 这几张都是小枣妹拍的,^_^: █ 浩鲸科技 浩鲸,就是以前的中兴软创: █ 紫光展锐 6G这块,干货很多: 这次重点展示…

docker容器日志占满硬盘空间的解决方案

目录 原因分析解决方案方案一 定时清空日志文件方案二 全局容器日志大小方案三 修改日志驱动 docker常用清理空间命令 原因分析 由于默认情况下,docker使用json-file类型的日志驱动,该日志驱动默认情况下,每个容器的日志会一直追加在文件名为…

chatgpt赋能python:用Python模拟用户登录,实现多个网站的SEO优化

用Python模拟用户登录,实现多个网站的SEO优化 介绍 在互联网时代,SEO已成为许多网站提高曝光率和流量的重要手段之一。而SEO优化的一个重要方面就是网站的用户登录。然而,手动登录多个网站进行SEO操作是非常耗时耗力的。那么,有…

卷积神经网络实现猫狗分类

目录 一、环境配置二、神经网络CNN1、简介2、CNN结构3、层次说明 三、数据集准备1、下载数据集2、数据集分类 四、 猫狗分类的实例——基准模型1、构建网络模型2、配置训练方法3、转换格式4、模型训练并保存生成的模型5、结果可视化 五、调整基准模型1、图像增强2、增强后的图像…

【C语言初阶(8)】函数1

文章目录 1. 函数的介绍2. 函数的分类2.1 库函数2.2 自定义函数 3. 函数的参数4. 函数的调用4.1 传值调用4.2 传址调用 1. 函数的介绍 1. 什么是函数? 函数是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。 一些函数执行某些动作&#xff…

Guava 之 EventBus

​​EvenBus​​​ 是 Guava 中 Pub/Sub 模式的轻量级实现。平时开发如果我们要实现自己的 Pub/Sub 模型,要写不少类,设计也挺复杂,对业务代码也有一定的侵入,但是在使用了 ​​EventBus​​ 之后就很方便了。 在 Pub/Sub 模式中…

Java——《面试题——tomcat篇》

全文章节 Java——《面试题——基础篇》 Java——《面试题——JVM篇》 Java——《面试题——多线程&并发篇》 Java——《面试题——Spring篇》 Java——《面试题——SpringBoot篇》 Java——《面试题——MySQL篇》​​​​​​ Java——《面试题——SpringCloud》 Java——…

干货 | 智慧教育平台生成式人工智能应用的安全要求

以下内容整理自清华大学《数智安全与标准化》课程大作业期末报告同学的汇报内容。 第一部分:编制说明 标准制定的基本原则主要包括以下四个方面: 综合性:本标准全面漫盖了智慧教育平台ChatGPT安全保护的要求,以便用户参考&#xf…

Spring:Bean

Bean 概述配置方式自动装配继承与依赖作用域外部属性文件的使用 概述 Spring 容器负责管理依赖注入&#xff0c;它将被管理的对象都称为 bean 。我们通过 xml 文件配置方式进行对 bean 的声明和管理。 写法如下&#xff1a; <beans><bean id"bean的唯一标识符…

Scrapy框架--CrawlSpider (详解+例子)

目录 CrawlSpider 简介 基本运行 特性和概念 基本使用 创建CrawlSpider 运行 使用CrawlSpider中核心的2个类对象 Rule对象 LinkExtractors 作用 使用 查看效果-shell中验证 示例 注意 CrawlSpider 简介 CrawlSpider 是 Scrapy 框架提供的一个特殊的 Spider 类…

Jvm内存模型剖析优化-JVM(四)

上篇文章代码实例详解如何自定义双亲委派&#xff0c;主要实现ClassLoader&#xff0c;有两个方法&#xff0c;一个直接loadClass用父类的&#xff0c;如果想在破坏&#xff0c;则需要重写loadClass&#xff0c;一个findClass必须要重新&#xff0c;因为父类是空的&#xff0c;…

SpringBoot3之GraalVM之Linux详细安装及使用教程

Linux安装底层工具相关依赖 yum install -y gcc glibc-devel zlib-devel安装GraalVM JDK 《GraalVM官网下载》 找到最近的GraalVM Community Edition X.X.X点击Assets&#xff08;因为我的是SpringBoot3项目&#xff0c;起始JDK就要求17&#xff0c;所以我下载17&#xff09;下…

青少年机器人技术一级核心知识点:机械结构及模型(一)

随着科技的不断进步&#xff0c;机器人技术已经成为了一个重要的领域。在这个领域中&#xff0c;机械结构是机器人设计中至关重要的一部分&#xff0c;它决定了机器人的形态、运动方式和工作效率。对于青少年机器人爱好者来说&#xff0c;了解机械结构的基础知识&#xff0c;掌…

vim背景颜色设置

cd ~进入个人家目录下&#xff0c;vim .vimrc进入vimrc文件&#xff1a; 在主题设置部分对颜色背景进行设置&#xff0c;onedark表示黑色背景&#xff0c;default表示白色背景&#xff0c;按需设置即可&#xff01;

网络知识点-链路聚合

链路聚合&#xff08;英语&#xff1a;Link Aggregation&#xff09;是一个计算机网络术语&#xff0c;指将多个物理端口汇聚在一起&#xff0c;形成一个逻辑端口&#xff0c;以实现出/入流量吞吐量在各成员端口的负荷分担&#xff0c;交换机根据用户配置的端口负荷分担策略决定…

【数据结构】算法的时间和空间复杂度

目录 1.什么是算法&#xff1f; 1.1算法的复杂度 2.算法的时间复杂度 2.1 时间复杂度的概念 计算Func1中count语句总共执行了多少次 2.2 大O的渐进表示法 2.3常见时间复杂度计算举例 实例1:执行2N10次 实例2:执行MN次 实例3:执行了100000000次 实例4:计算strchr的时…

java jwt生成token并在网关设置全局过滤器进行token的校验并在给请求头设置参数及在微服务中解析参数

1、首先引入jjwt的依赖 <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>2、编写生成token的工具类 package com.jjw.result.util;import com.jjw.res…

软考高级系统架构设计师(九) 作文模板-论设计模式及其应用(未完待续)

目录 掌握的知识点 创建型 结构型 行为型 掌握的知识点 设计模式分为哪3类 每一类包含哪些具体的设计模式 创建型 创建型模式是对对象实例化过程的抽象&#xff0c;他通过抽象类所定义的接口&#xff0c;封装了系统中对象如何创建、组合等信息。 创建型模式主要用于创建对…

【物联网】微信小程序接入阿里云物联网平台

微信小程序接入阿里云物联网平台 一 阿里云平台端 1.登录阿里云 阿里云物联网平台 点击进入公共实例&#xff0c;之前没有的点进去申请 2.点击产品&#xff0c;创建产品 3.产品名称自定义&#xff0c;按项目选择类型&#xff0c;节点类型选择之恋设备&#xff0c;联网方式W…

Linux下安装Redis的详细安装步骤

一.Redis安装 1.下载linux压缩包 【redis-5.0.5.tar.gz】 2.通过FlashFXP把压缩包传送到服务器 3.解压缩 tar -zxvf redis-5.0.5.tar.gz4.进入redis-5.0.5可以看到redis的配置文件redis.conf 5.基本的环境安装 使用gcc -v 命令查看gcc版本已经是4.8.5了&#xff0c;于是就…