GH-26685: [Python][C++] Trim buffers when pickling a sliced array - #50159
kirilklein wants to merge 1 commit into
Conversation
|
@jorisvandenbossche Would you like to take a look? |
Pickling a sliced array previously serialized the array's entire parent buffers instead of just the referenced slice. Add arrow::internal::TrimArrayDataBuffers, which compacts a sliced array (offset != 0) to its referenced range via Concatenate, and call it from Array.__reduce__ so pickling only serializes referenced bytes. Unsliced arrays are returned untouched, preserving zero-copy / protocol-5 out-of-band pickling. ChunkedArray/RecordBatch/Table inherit the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Rebased on current main to resolve a test-file conflict |
AlenkaF
left a comment
There was a problem hiding this comment.
Concatenating does produce the result we need when it comes to pickling but I am afraid it comes at some cost. First, there will always be a copy made which I think can be avoided, for example with fixed-width arrays as in the ipc serialization.
Also the parent can have offset 0 but its children can be sliced and have offset that are not 0. This case is ignored in the current change if I am not mistaken.
The problem the issue #26685 is raising seems very complex to me. I would propose to reuse the existing IPC truncation logic (a branch has already been shared for continuing the work) but expose it in a way that was already asked for in the thread: with a plain function over ArrayData, no visitor interface.
Rationale
Pickling a sliced array currently serializes the array's entire parent
buffers rather than just the sliced range, because
Array.__reduce__wraps theraw
ArrayDatabuffers as-is. For a one-element slice of a large array thisserializes the whole parent (megabytes for a few bytes of data), which is a
long-standing pain point for multiprocessing / Dask / Ray (issue open since
2020). The IPC writer already truncates sliced buffers; pickling did not.
What this does
Adds
arrow::internal::TrimArrayDataBuffers(ArrayData, MemoryPool*)(
cpp/src/arrow/array/util.{h,cc}): when the array has a non-zerooffset(i.e. it is a slice sharing a parent's buffers) it compacts the array to the
referenced range via
Concatenate({MakeArray(data)}), which already handlesevery nested / variable-length / dictionary type correctly; otherwise it
returns the input unchanged.
Array.__reduce__calls it before reducing, so asliced array pickles only its referenced bytes. ChunkedArray / RecordBatch /
Table inherit the fix since they reduce through their arrays.
Pickle protocol-5 out-of-band buffers keep working (this is why the prior
IPC-based attempt, #37683, was rejected): we still reduce real
Bufferobjects, just trimmed ones. Crucially, unsliced arrays are returned
untouched, so their protocol-5 pickling stays zero-copy
(
test_array_pickle_protocol5keeps passing). The guard isoffset != 0rather than a buffer-size comparison precisely because allocator padding makes
an unsliced array's referenced size differ from its total buffer size — a
size-based guard would needlessly copy (and break zero-copy for) unsliced
arrays.
Known limitation: a zero-offset head slice (
arr.slice(0, k)of a largearray) is not trimmed, since it cannot be distinguished from an unsliced array
by
offsetalone without per-type buffer-size logic. This is no worse than thestatus quo (such slices already pickle the full buffers); the common case of
slices with a non-zero offset is fixed. A follow-up could trim these too via a
proper per-type buffer-truncation utility.
Design note for reviewers
Earlier discussion favored refactoring the IPC writer's per-type truncation
into a shared zero-copy visitor (Option 1). That refactor stalled for years on
nested / dictionary handling. This PR instead reuses
Concatenate, whichcopies only the (small) compacted slice — the same bytes pickle would serialize
anyway — for a much smaller, lower-risk change. Happy to evolve this toward a
zero-copy
SliceBuffer-based utility if preferred.Benchmarks
Local source build, arrays of 2,000,000 elements, pickling a 10-element slice:
Containers inherit the fix: a sliced ChunkedArray / RecordBatch / Table built on
a 2M-row array pickles to ~250–340 bytes (was tens of MB).
Tests
Adds regression tests in
python/pyarrow/tests/test_array.pycovering slicepickle size + round-trip across primitive / bool / string / list types,
protocol-5 out-of-band buffers, and the unsliced-array regression case. The
existing
test_array_pickle_protocol5(zero-copy guarantee) continues to pass.🤖 Generated with Claude Code