Skip to content

feat(ipc): Reduce redundant zero-initialization in IPC reader#9778

Draft
pchintar wants to merge 4 commits intoapache:mainfrom
pchintar:reader-zero-init
Draft

feat(ipc): Reduce redundant zero-initialization in IPC reader#9778
pchintar wants to merge 4 commits intoapache:mainfrom
pchintar:reader-zero-init

Conversation

@pchintar
Copy link
Copy Markdown
Contributor

@pchintar pchintar commented Apr 21, 2026

Which issue does this PR close?

Rationale for this change

In arrow-ipc/src/reader.rs, buffers are currently allocated with MutableBuffer::from_len_zeroed(len) and then passed to read_exact, which fully overwrites the entire buffer contents.

This results in an unnecessary full memory pass for all data bytes:

  • first zero-initialization
  • then complete overwrite by the incoming data

Since read_exact guarantees that the provided slice is fully written on success, the initial zeroing step is redundant and can be safely avoided by allocating with capacity and setting the length before the read.

What changes are included in this PR?

Affected locations

  • read_block (file reader path)
  • MessageReader::maybe_next (stream reader path, message body buffer)

Before/Current Approach

let mut buf = MutableBuffer::from_len_zeroed(len);
reader.read_exact(&mut buf)?;

After Approach

let mut buf = MutableBuffer::with_capacity(len);
unsafe { buf.set_len(len) };
reader.read_exact(buf.as_slice_mut())?;

Rationale

read_exact fully overwrites the provided slice on success, so the initial zero-fill is redundant.
This change removes one full memory write pass per read without altering behavior.

Safety

The unsafe set_len usage is sound because:

  • the buffer is not accessed between set_len and read_exact
  • read_exact either fully initializes the buffer or returns an error
  • on error, the buffer is not used and is dropped immediately
  • no partially initialized data is exposed to safe Rust

Crash or interruption scenarios (panic, early return, process termination) do not introduce unsoundness, as the buffer is never observed unless read_exact completes successfully by returning Ok(()).

Are these changes tested?

Yes the changes were tested successfully by running:

cargo test -p arrow-ipc
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings

Are there any user-facing changes?

No, there are no changes made to any public api code.

@github-actions github-actions Bot added the arrow Changes to the arrow crate label Apr 21, 2026
Comment thread arrow-ipc/src/reader.rs Outdated
let mut buf = MutableBuffer::from_len_zeroed(total_len);
reader.read_exact(&mut buf)?;
let mut buf = MutableBuffer::with_capacity(total_len);
// Buffer is immediately fully initialized by `read_exact` before any read occurs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, this is not sound for arbitrary Read implementations. Unfortunately, nothing would prevent a Read impl from reading the uninitialized bytes in buf, causing undefined behavior. For a known implementation like File this pattern could be fine though.

An alternative could be to combine take and read_to_end to read into a Vec, but note that this changes the alignment of the resulting Buffer:

    let mut buf = Vec::with_capacity(total_len);
    reader.take(total_len as u64).read_to_end(&mut buf)?;
    if buf.len() != total_len {
        return Err(...)
    }
    Ok(buf.into())

Copy link
Copy Markdown
Contributor Author

@pchintar pchintar Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reply @jhorstmann , so I dug into this a bit more and agree the set_len + read_exact approach isn’t suitable for generic Read.

While looking into alternatives, I came across the newer read_buf API using BorrowedBuf, which is designed specifically for reading into uninitialized memory safely. It seems like it would let us avoid the zero-fill while still preserving alignment via MutableBuffer.

Something like:

use std::io::{Read, BorrowedBuf};

let mut buf = MutableBuffer::with_capacity(total_len);

{
    let spare = buf.spare_capacity_mut();
    let mut borrowed = BorrowedBuf::from(spare);
    reader.read_buf_exact(borrowed)?;
}

unsafe { buf.set_len(total_len) };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it seems to be that the read_buf is a nightly api and so may not be compatible with stable rust, so I'm unsure about it.

Copy link
Copy Markdown
Contributor Author

@pchintar pchintar Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jhorstmann and @alamb — I revisited this and reworked my approach to avoid unsafe entirely.

The earlier set_len + read_exact pattern is not sound for generic Read. A fully safe alternative must also account for alignment: Arrow buffers require 64-byte alignment (arrow_buffer::buffer::ALIGNMENT), while Vec<u8> does not guarantee this.

To handle both safely:

  • read into Vec<u8> via take(...).read_to_end(...) (works for all Read)
  • reuse the allocation only if it is 64-byte aligned
  • otherwise copy into MutableBuffer::from_len_zeroed(...) to ensure proper alignment

To avoid overhead on small reads, my implementation retains the existing path for smaller buffer sizes and applies the optimized path only to larger buffers (8192 bytes or higher), which constitute the majority of cases in the benchmark.

Benchmark (official ipc_reader)

cargo bench -p arrow-ipc --bench ipc_reader --features zstd
StreamReader/read_10              ~910 µs → ~772 µs   (~15% faster)
StreamReader/no_validation        ~441 µs → ~300 µs   (~32% faster)

FileReader/read_10                ~900 µs → ~773 µs   (~14% faster)
FileReader/no_validation          ~576 µs → ~300 µs   (~48% faster)

zstd cases: small but consistent improvements
mmap: unchanged or slightly improved

@alamb alamb marked this pull request as draft April 22, 2026 13:31
@pchintar pchintar changed the title feat(ipc): Remove zero-initialization in IPC reader feat(ipc): Reduce redundant zero-initialization in IPC reader Apr 23, 2026
@alamb
Copy link
Copy Markdown
Contributor

alamb commented Apr 23, 2026

run benchmark ipc_reader

Copy link
Copy Markdown
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pchintar -- using Vec and Vec::read_exact sounds like a great optimization to me

I don't really understand the reason to keep any of the other MutableBuffer based paths though -- i think we can simplify this PR significantly (and probably make it faster) if it always uses Vec

Comment thread arrow-ipc/src/reader.rs Outdated
let mut buf = MutableBuffer::from_len_zeroed(total_len);
reader.read_exact(&mut buf)?;
Ok(buf.into())
if total_len < 8 * 1024 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you benchmark always using Vec? Vec is pretty fast so it might be better to always use the Vec path 🤔

Comment thread arrow-ipc/src/reader.rs Outdated
if ((vec.as_ptr() as usize) & 63) == 0 {
Ok(Buffer::from_vec(vec))
} else {
let mut buf = MutableBuffer::from_len_zeroed(total_len);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we would ever want to do a second copy? (why not always go directly to a Buffer from. a vec?)

@adriangbot
Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4307614360-1786-zqcd4 6.12.55+ #1 SMP Sun Feb 1 08:59:41 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing reader-zero-init (8de1dd4) to b93240a (merge-base) diff
BENCH_NAME=ipc_reader
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench ipc_reader
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot
Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                       main                                   reader-zero-init
-----                                                       ----                                   ----------------
arrow_ipc_reader/FileReader/no_validation/read_10           1.00    119.8±2.06µs        ? ?/sec    1.45    173.3±1.08µs        ? ?/sec
arrow_ipc_reader/FileReader/no_validation/read_10/mmap      1.00     56.9±0.28µs        ? ?/sec    1.02     58.3±0.30µs        ? ?/sec
arrow_ipc_reader/FileReader/read_10                         1.00    386.2±8.20µs        ? ?/sec    1.12    434.2±4.05µs        ? ?/sec
arrow_ipc_reader/FileReader/read_10/mmap                    1.01    456.0±2.71µs        ? ?/sec    1.00    452.9±3.58µs        ? ?/sec
arrow_ipc_reader/StreamReader/no_validation/read_10         1.00    118.9±2.25µs        ? ?/sec    1.45    172.3±0.54µs        ? ?/sec
arrow_ipc_reader/StreamReader/no_validation/read_10/zstd    1.00      2.4±0.02ms        ? ?/sec    1.01      2.5±0.02ms        ? ?/sec
arrow_ipc_reader/StreamReader/read_10                       1.00    390.8±5.56µs        ? ?/sec    1.11    434.3±4.63µs        ? ?/sec
arrow_ipc_reader/StreamReader/read_10/zstd                  1.00      2.7±0.02ms        ? ?/sec    1.00      2.7±0.02ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 85.0s
Peak memory 2.7 GiB
Avg memory 2.7 GiB
CPU user 70.5s
CPU sys 11.2s
Peak spill 0 B

branch

Metric Value
Wall time 90.0s
Peak memory 2.7 GiB
Avg memory 2.7 GiB
CPU user 76.1s
CPU sys 10.2s
Peak spill 0 B

File an issue against this benchmark runner

@pchintar
Copy link
Copy Markdown
Contributor Author

Thanks @alamb — this is very helpful.

My local results were positive, but the CI benchmark clearly shows the current hybrid approach regresses on this machine, so I simplified this and used the always-`Vec' approach now. i got these results below:

Image 4-23-26 at 5 06 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove redundant zero-initialization in Arrow IPC reader hot paths

4 participants