yongster opened a new issue, #10800:
URL: https://github.com/apache/arrow-rs/issues/10800

   ## Summary
   
   `StringViewArray` / `BinaryViewArray` `filter`, `take`, and `interleave` 
copy only the 16-byte views and then reattach the full `data_buffers()` list. 
That is intentional zero-copy, and those kernels are fast because of it.
   
   The cost shows up later: a 1% filter still holds 100% of the long-string 
payload. Arrow IPC (file or network) then writes those unused variadic buffers 
in full, so the message/file is billed at the unfiltered size.
   
   `StringViewArray::gc()` already compact the buffers, and its docs say to use 
it after filter/slice. `filter`/`take` do not call it. `BatchCoalescer` already 
has a density check: copy only if actual data-buffer capacity is more than 2× 
bytes actually referenced.
   
   This is not "StringView is slower than Utf8". The compute kernels are 
faster. The question is whether we should compact, and **where**, without 
giving up that compute path by default.
   
   ## Local measurements
   
   Local `--release` benchmark on current `arrow-rs` (Apple M4 Pro). Input: 
50_000 rows × 200-byte `StringViewArray` (all long, so all bytes live in data 
buffers). `filter`/`take` keep 1% (500 rows). Logical selected payload = 100 
KiB.
   
   | Result | Rows | Data buffer cap | IPC size |
   |---|---|---|---|
   | Filtered StringView (no gc) | 500 | 9.98 MiB (~105×) | 9.55 MiB |
   | Same after `gc()` | 500 | 97.7 KiB | 106 KiB |
   | Filtered Utf8 (control) | 500 | 97.7 KiB | 100 KiB |
   
   Kernel medians: StringView `filter` 1.4 µs, Utf8 `filter` 7.3 µs, `gc()` of 
the filtered view 3.9 µs.
   
   Related: `concat` of two slices of the **same** view remounts the same 
buffers again (400 rows, 80 KiB logical, ~19 MiB IPC). Parquet file size is 
fine (it encodes selected values), but RSS while writing still holds the fat 
buffers.
   
   Root cause in `filter` (same pattern in `take`):
   
   ```rust
   let buffers = array.data_buffers().to_vec();
   unsafe { GenericByteViewArray::new_unchecked(views, buffers, nulls) }
   ```
   
   ## Position
   
   **In-memory compute latency matters more than compacting every 
intermediate.** Unconditionally calling `gc()` in `filter`/`take` would copy 
even dense results (`gc()` always copies; the docs warn about that) and would 
erase the zero-copy advantage when almost all rows are kept.
   
   The default should stay zero-copy. Compaction should happen only when the 
array is clearly sparse, or when we are about to serialize.
   
   ## Option A — compact by density threshold
   
   After `filter`/`take` (maybe `concat`), call `gc()` only if:
   
   ```text
   sum(data_buffers.capacity) > total_buffer_bytes_used * N
   ```
   
   `N = 2` is already used as `need_gc` in 
`arrow-select/src/coalesce/byte_view.rs`.
   
   *Pros:* no caller changes; sparse pipelines get smaller RSS and IPC; 
consistent with coalescing.
   
   *Cons:* sparse `filter` pays an extra copy (~3.9 µs in the 1% case above, 
still often cheaper than Utf8); the threshold will be bikeshed; pipelines that 
filter and immediately drop the batch pay for a copy they do not need.
   
   ## Option B — compact at the IPC / serialization boundary
   
   Keep `filter`/`take` zero-copy. Compact unused view buffers in 
`IpcWriteOptions` when encoding a batch (file, stream, Flight). Compute code 
does not change; we only copy when turning an Array into bytes.
   
   *Pros:* compute stays fastest by default; IPC is the place that currently 
writes unused buffers verbatim, so that is a precise time to pay.
   
   *Cons:* in-memory RSS stays fat until something serializes; later `concat` 
can still remount the same buffers; shared-memory / FFI consumers that never go 
through IPC would not benefit.
   
   A per-`filter` flag is intentionally not proposed. Most callers would not 
know to set it, and it would push a serialization concern into the compute API.
   
   ## Questions
   
   1. Is this a compute-kernel concern, a serialization-boundary concern, or 
both?
   2. If a threshold: reuse the coalescer's 2×? Make it configurable?
   3. If Option B: should `IpcWriteOptions::compact_view_buffers` default on or 
off?
   4. Should remounting the same `Buffer` in `concat` (two slices of one view) 
be this issue or a follow-up?
   5. `ListView` `filter`/`take` leaving the full child is the same design. 
Same ticket or separate?
   
   ## Preference for discussion (not a decision)
   
   Keep `filter`/`take` zero-copy by default.
   
   Prefer either or both of:
   
   - compact view buffers when writing IPC (Option B);
   - auto-`gc()` only when clearly sparse (Option A, reuse the 2× coalescer 
rule).
   
   Do **not** unconditionally `gc()` on every `filter`/`take`.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to