Repository navigation
Conversation
Contributor
Author
|
Benchmark results for Results
|
adriangb
force-pushed
the
minify-kernel
branch
from
November 11, 2026 22:03
9471a40 to
5b9e6cd
Compare
adriangb
force-pushed
the
minify-kernel
branch
from
November 11, 2026 23:14
5b9e6cd to
e3e488f
Compare
Sliced arrays and view arrays can keep memory alive that their rows do not reference. Callers such as spilling and IPC writers each work around this on their own. `minify` returns a logically equal array with offset zero whose buffers hold only referenced data. This commit adds the reuse policy and the null buffer handling, and supports the flat types: primitive, temporal, decimal, interval, boolean, null, fixed size binary, byte and byte view arrays. Null rows of byte arrays get empty ranges. `view_data_len` and `keeps_view_buffers` hold the view array decision, so that later code can share it. The dispatch matches every `DataType` variant without a wildcard, so a new variant in arrow-schema fails to compile until `minify` handles it. Nested, union, dictionary and run-end encoded types return `NotYetImplemented`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…oded types in `minify` Sliced lists and maps share the full child array. Dictionaries can hold values that no key references, and dense unions can hold child rows that no row references. `minify` needs to recurse into these types to drop that memory. This commit adds List, LargeList, Map, FixedSizeList, Struct, ListView, LargeListView, sparse and dense Union, Dictionary and RunEndEncoded. Null rows of lists and maps get empty ranges. These types share one gather path, `gather_ranges`. It gathers dense unions, list views and run-end encoded arrays by their parts, so child rows that several rows share stay shared. `list_view_child` and `dense_union_rows` hold the child row selection of list views and dense unions, so that later code can share it. `minify`, `gathers_parts` and `gather_ranges` match every `DataType` variant without a wildcard. The dispatch passes the list field and the union fields to the helpers, so they need no `unreachable!` arm. Invalid dictionary key, run end and time unit types return `InvalidArgumentError`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The `minify` kernel has no benchmarks, so performance changes cannot be measured. This adds `minify_kernels` with slices of 8192 rows from parents of 100k rows for each main type, an already minimal input, null-heavy strings with non-empty null ranges, and one large 4M-row case. Co-Authored-By: Claude Opus 5.5 <[email protected]>
adriangb
force-pushed
the
minify-kernel
branch
from
November 12, 2026 00:22
e3e488f to
940d911
Compare
adriangb
added a commit
to adriangb/datafusion
that referenced
this pull request
Nov 12, 2026
A spill batch has at most batch_size rows, whatever its size in bytes. A merge of spill files reserves memory for the largest batch of each file, so wide rows make that reservation larger than the pool. An operator needs a way to bound the batches it spills by bytes. This commit adds SpillManager::with_max_batch_bytes (crate-private) and SpillPieces. A bounded writer cuts each appended batch into row ranges by SpilledRowSizes, copies each range, and writes it as its own IPC message. The recorded size of a bounded writer counts the IPC padding of each buffer, so a batch read back never exceeds it. A slice of a list, run-end encoded array, union or list view keeps whole children or values, so its measured size is far above what the IPC reader allocates for it. compact_batch copies each range into fresh buffers that hold only its rows, so the measure is exact for every type and no type has to be written whole. It uses arrow take, plus fixes for what take keeps whole: view buffers, unused dictionary values and list view children. Rows of a list view can share a child range, so it copies the covered range once, and the halving of a piece stops when the halves do not get smaller. arrow-rs minify (apache/arrow-rs#12184) replaces the copy. A dictionary that the rows of a split batch share is written once per piece. A batch within the bound is written as before. No operator sets a bound yet, and an unbounded writer records the same size as before, so this commit changes no behavior. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…iased buffers Review found inputs where `minify` kept or created memory that the rows do not need. Repeated views counted shared bytes once per view, so `minify` kept a 1 MB buffer for 100 distinct bytes or made `gc` write the bytes once per view. Null view rows kept their bytes. The slack of 64 bytes per data buffer kept thousands of unreferenced small buffers. A buffer aliased twice inside one array was copied twice, so memory grew. The dense union lookup table was as long as the child. View arrays now merge the distinct ranges that valid views reference. `minify` keeps a data buffer only if the views reference all of its bytes and it is not oversized. It copies the other ranges once into new buffers and gives null rows an empty view. Equal struct columns share one minified column. If the result still uses more memory than the input, `minify` returns the input unchanged. The dense union table spans only the visible offsets. The module doc lists the parents that keep child data of null rows. The `allocated` test helper counts each allocation once. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…in `minify` `gather_run` copied a run value once per gathered range. A list whose null rows keep their child ranges, or a dense union over a run-end encoded child, then gathered many ranges of one run. A 183 KB input reached a 1 GB peak, and the fallback returned the input unchanged although nothing was aliased. A range that starts in the run where the previous range ended now extends that run. Dense union child rows are numbered in ascending order, so gathered ranges never go back and coalesce. The offsets keep the logical rows unchanged, and a child that is used in full stays shared. `view_data` adds view offsets and lengths as `usize`, because a data buffer can be longer than `u32::MAX`. Dictionary garbage collection costs O(keys + span of used keys) and not O(values). New tests pin the run fix, the view range math, the fallback threshold, the null comparisons of `same_data`, the paths for all-valid null buffers, `push_range`, `gathers_parts` and the exact byte capacity of a gather. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cases cover already compact arrays, small slices of large utf8, view, dictionary and dense union parents, reversed views and arrays with one data buffer per row. `gc`, `take` and `garbage_collect_dictionary` run as baselines that do comparable work. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Rationale for this change
Sliced arrays, view arrays, dictionaries and dense unions can keep memory alive that their rows do not reference. Downstream users (for example DataFusion spilling) each work around this on their own. The issue asks for one kernel in
arrow-select.What changes are included in this PR?
A new
arrow_select::minifymodule withminify(&dyn Array) -> Result<ArrayRef, ArrowError>. The result is logically equal to the input, has offset zero, and its buffers hold only referenced data.minifyrecurses into children, dictionary values and run-end encoded values.gather_ranges, nottake.gather_run, which keeps the runs of the input and copies each run value once, also when many gathered ranges hit one run.garbage_collect_dictionary, but gather the kept values withgather_ranges, then recurse into keys and values.Reuse policy:
minifycopies a buffer only when its allocation is larger than the visible data needs (rounded up to 64 bytes). Already-minimal arrays are returned without copies, sominify(minify(a))copies nothing. New allocations also round up to 64 bytes, so a tiny input whose buffers are already exact-size can grow slightly.Null rows:
minifyalways gives null rows of byte, list, map and list view arrays an empty value range (and null rows of byte view arrays an empty view), as #9970 proposed. There is no option for this. A fast path keeps it cheap. One O(rows) check over the offsets and the null bitmap finds out if any null row has a non-empty range. If none has (including arrays with no nulls),minifyslices or copies the referenced range in one piece. Otherwise it copies the child range of each run of valid rows, so contiguous runs stay single copies. The result has no reclaimable null ranges, so a second call copies nothing.Open questions for reviewers:
minifycopies it even if other arrays keep it alive. The docs call this out.minifyreturns the input unchanged.Are these changes tested?
Yes. Unit tests cover every type above, null rows with non-empty ranges (bytes, map, and a list of strings with nulls at both levels, also inside a dictionary and a struct), the no-null fast path, plus nested combinations: dictionary of
Utf8View, dictionary of list, dictionary inside struct and list children, run-end encoded dictionary, fixed-size list of struct, dense union of lists, and lists over sliced children. A sharedcheckhelper slices each array several ways and asserts that the result:validate_full,Benchmarks are in
arrow/benches/minify_kernels.rs.How to review
Read the PR commit by commit:
minifyfor flat types (reuse policy, null buffers, primitive, boolean, null, fixed size binary, byte and byte view arrays). The dispatch matches everyDataTypevariant without a wildcard. Nested types returnNotYetImplementedin this commit only.gather_rangespath. The row selection helpers (list_view_child,dense_union_rows) exist from the commit that first needs them, so feat(arrow-select): add compacted_size, row_sizes and split_by_size next to minify #12190 can reuse them.minify_kernelsbenchmark.view_data,keeps_view_buffer). Aliased buffers cannot make the result larger than the input. The dense union lookup table spans only the visible offsets.gather_run, number dense union child rows in ascending order, useusizefor view ranges, and make dictionary garbage collection O(keys + span of used keys).minify_kernelscases for small slices of large parents.Are there any significant user-facing changes?
New public API only:
arrow_select::minify::minify.AI usage disclosure
Claude Code wrote the implementation, tests and this description under my direction.
🤖 Generated with Claude Code