内存和 I/O 接口#

本节将向您介绍 PyArrow 内存管理和 I/O 系统中的主要概念

  • 缓冲区

  • 内存池

  • 类文件和类流对象

引用和分配内存#

pyarrow.Buffer#

Buffer 对象封装了 C++ arrow::Buffer 类型,它是 Apache Arrow 在 C++ 中进行内存管理的主要工具。它允许更高级别的数组类安全地与它们可能拥有或不拥有的内存进行交互。arrow::Buffer 可以进行零拷贝切片,以允许 Buffer 廉价地引用其他 Buffer,同时保留内存生命周期和清晰的父子关系。

有许多 arrow::Buffer 的实现,但它们都提供了一个标准接口:一个数据指针和长度。这类似于 Python 的内置 buffer protocolmemoryview 对象。

一个 Buffer 可以通过调用 py_buffer() 函数从任何实现缓冲协议的 Python 对象创建。我们来考虑一个 bytes 对象

In [1]: import pyarrow as pa

In [2]: data = b'abcdefghijklmnopqrstuvwxyz'

In [3]: buf = pa.py_buffer(data)

In [4]: buf
Out[4]: <pyarrow.Buffer address=0x7fdfff036750 size=26 is_cpu=True is_mutable=False>

In [5]: buf.size
Out[5]: 26

以这种方式创建 Buffer 不会分配任何内存;它是 data 字节对象导出的内存的零拷贝视图。

外部内存,以原始指针和大小的形式,也可以使用 foreign_buffer() 函数进行引用。

Buffer 可以在需要 Python buffer 或 memoryview 的情况下使用,并且这些转换是零拷贝的

In [6]: memoryview(buf)
Out[6]: <memory at 0x7fdfff078100>

Buffer 的 to_pybytes() 方法将 Buffer 的数据转换为 Python 字节字符串(因此会复制数据)

In [7]: buf.to_pybytes()
Out[7]: b'abcdefghijklmnopqrstuvwxyz'

内存池#

所有内存分配和解除分配(如 C 中的 mallocfree)都在 MemoryPool 实例中进行跟踪。这意味着我们可以精确地跟踪已分配的内存量

In [8]: pa.total_allocated_bytes()
Out[8]: 56640

让我们从默认池中分配一个可调整大小的 Buffer

In [9]: buf = pa.allocate_buffer(1024, resizable=True)

In [10]: pa.total_allocated_bytes()
Out[10]: 57664

In [11]: buf.resize(2048)

In [12]: pa.total_allocated_bytes()
Out[12]: 58688

默认分配器以最小增量 64 字节请求内存。如果 Buffer 被垃圾回收,所有内存都将被释放

In [13]: buf = None

In [14]: pa.total_allocated_bytes()
Out[14]: 56640

除了默认的内置内存池,根据 Arrow 的构建方式,可能还有其他内存池可供选择(例如 jemalloc)。可以获取内存池的后端名称

>>> pa.default_memory_pool().backend_name
'mimalloc'

另请参阅

内存池的 API 文档.

另请参阅

使用 Arrow 的可选 CUDA 集成在 GPU 上创建缓冲区。

输入和输出#

Arrow C++ 库有几种抽象接口用于不同类型的 I/O 对象

  • 只读流

  • 支持随机访问的只读文件

  • 只写流

  • 支持随机访问的只写文件

  • 支持读、写和随机访问的文件

为了使这些对象更像 Python 的内置 file 对象,我们定义了一个 NativeFile 基类,它实现了与常规 Python 文件对象相同的 API。

NativeFile 具有一些重要特性,使其在可能的情况下优于将 Python 文件与 PyArrow 一起使用

  • 其他 Arrow 类可以原生访问内部 C++ I/O 对象,并且不需要获取 Python GIL

  • 原生 C++ I/O 可能能够进行零拷贝 I/O,例如内存映射

有几种 NativeFile 选项可用

还有高级 API 可以使实例化常见类型的流变得更容易。

高级 API#

输入流#

input_stream() 函数允许从各种来源创建可读的 NativeFile

  • 如果传入 Buffermemoryview 对象,将返回 BufferReader

    In [15]: buf = memoryview(b"some data")
    
    In [16]: stream = pa.input_stream(buf)
    
    In [17]: stream.read(4)
    Out[17]: b'some'
    
  • 如果传入字符串或文件路径,它将打开磁盘上的给定文件进行读取,创建 OSFile。可选地,文件可以被压缩:如果其文件名以诸如 .gz 等可识别的扩展名结尾,其内容将在读取时自动解压缩。

    In [18]: import gzip
    
    In [19]: with gzip.open('example.gz', 'wb') as f:
       ....:     f.write(b'some data\n' * 3)
       ....: 
    
    In [20]: stream = pa.input_stream('example.gz')
    
    In [21]: stream.read()
    Out[21]: b'some data\nsome data\nsome data\n'
    
  • 如果传入 Python 文件对象,它将被封装在 PythonFile 中,以便 Arrow C++ 库可以从中读取数据(但会带来轻微的开销)。

输出流#

output_stream() 是输出流的等效函数,并允许创建可写的 NativeFile。它具有与上述 input_stream() 相同的功能,例如能够写入缓冲区或进行即时压缩。

In [22]: with pa.output_stream('example1.dat') as stream:
   ....:     stream.write(b'some data')
   ....: 

In [23]: f = open('example1.dat', 'rb')

In [24]: f.read()
Out[24]: b'some data'

磁盘文件和内存映射文件#

PyArrow 包含两种与磁盘数据交互的方式:标准操作系统级文件 API 和内存映射文件。在常规 Python 中,我们可以这样写

In [25]: with open('example2.dat', 'wb') as f:
   ....:     f.write(b'some example data')
   ....: 

使用 pyarrow 的 OSFile 类,你可以这样写

In [26]: with pa.OSFile('example3.dat', 'wb') as f:
   ....:     f.write(b'some example data')
   ....: 

对于读取文件,可以使用 OSFileMemoryMappedFile。它们之间的区别在于 OSFile 在每次读取时都会分配新内存,就像 Python 文件对象一样。在内存映射读取中,库会构建一个引用映射内存的缓冲区,而无需任何内存分配或复制

In [27]: file_obj = pa.OSFile('example2.dat')

In [28]: mmap = pa.memory_map('example3.dat')

In [29]: file_obj.read(4)
Out[29]: b'some'

In [30]: mmap.read(4)
Out[30]: b'some'

read 方法实现了标准的 Python 文件 read API。要读取到 Arrow Buffer 对象中,请使用 read_buffer

In [31]: mmap.seek(0)
Out[31]: 0

In [32]: buf = mmap.read_buffer(4)

In [33]: print(buf)
<pyarrow.Buffer address=0x7fe0dbe86000 size=4 is_cpu=True is_mutable=False>

In [34]: buf.to_pybytes()
Out[34]: b'some'

PyArrow 中的许多工具,特别是 Apache Parquet 接口以及文件和流消息传递工具,与这些 NativeFile 类型一起使用时比与普通 Python 文件对象一起使用时更高效。

内存中读写#

为了协助内存中数据的序列化和反序列化,我们提供了可以读写 Arrow 缓冲区的文件接口。

In [35]: writer = pa.BufferOutputStream()

In [36]: writer.write(b'hello, friends')
Out[36]: 14

In [37]: buf = writer.getvalue()

In [38]: buf
Out[38]: <pyarrow.Buffer address=0x7fe071a900c0 size=14 is_cpu=True is_mutable=True>

In [39]: buf.size
Out[39]: 14

In [40]: reader = pa.BufferReader(buf)

In [41]: reader.seek(7)
Out[41]: 7

In [42]: reader.read(7)
Out[42]: b'friends'

这些与 Python 内置的 io.BytesIO 具有相似的语义。