IPC streams, Feather and memory mapping

PyArrow · Advanced · 12 min

Arrow IPC is the fastest way to move tables between processes or save them for quick reload, because no parsing or conversion happens.

Arrow IPC writes the in-memory columnar layout to bytes almost unchanged, so reading it back needs no parsing: just pointers into a buffer.

There are two flavours:

  • The stream format (pa.ipc.new_stream(sink, schema) and pa.ipc.open_stream(source)) is a sequence of record batches of unknown length, ideal for sockets, pipes and HTTP bodies.
  • The file format (pa.ipc.new_file and pa.ipc.open_file) adds a footer, so readers can jump to any batch. Its files usually use the .arrow extension.

Feather v2 is the IPC file format with optional LZ4 or Zstd compression, via pyarrow.feather.write_feather(table, path) and read_feather(path) or read_table(path).

Memory mapping makes reads nearly free. pa.memory_map(path) maps the file into virtual memory, and pa.ipc.open_file(mm).read_all() builds a Table whose buffers point straight into the mapped file: nothing is copied, and the OS loads only pages you touch. This only works for uncompressed IPC files; compressed data must be decompressed into new memory.

Compared with Parquet: IPC and Feather are faster to read and write but larger, and aimed at short-term storage and interchange. Parquet is smaller and better for long-term storage and analytics engines.

Example

import pyarrow as pa

t = pa.table({"id": list(range(100_000)), "x": [1.5] * 100_000})

with pa.OSFile("data.arrow", "wb") as sink:
    with pa.ipc.new_file(sink, t.schema) as writer:
        for batch in t.to_batches(max_chunksize=25_000):
            writer.write_batch(batch)

before = pa.total_allocated_bytes()
with pa.memory_map("data.arrow", "r") as source:
    reader = pa.ipc.open_file(source)
    print(reader.num_record_batches)            # 4
    loaded = reader.read_all()
    print(loaded.num_rows)                      # 100000
    print(pa.total_allocated_bytes() - before)  # 0  (buffers point into the map)

sink = pa.BufferOutputStream()
with pa.ipc.new_stream(sink, t.schema) as w:
    w.write_table(t.slice(0, 3))
print(pa.ipc.open_stream(sink.getvalue()).read_all()["id"].to_pylist())   # [0, 1, 2]

Remember

  • IPC = Arrow's memory layout on the wire or disk, no parsing needed
  • Stream format for pipes and sockets; file format for random access
  • Memory-mapped uncompressed IPC files load with zero copies

Watch out for

  • Expecting zero-copy reads from a compressed Feather file; decompression allocates memory.

Check yourself

  1. Why does reading a memory-mapped, uncompressed Arrow IPC file allocate almost no memory?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: arrow.apache.org/docs/python/ipc.html