Skip to content

BufferIterator can be Send + Sync#8873

Merged
robert3005 merged 1 commit into
developfrom
rk/bufferiter
Jul 23, 2026
Merged

BufferIterator can be Send + Sync#8873
robert3005 merged 1 commit into
developfrom
rk/bufferiter

Conversation

@robert3005

@robert3005 robert3005 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

BufferIterator is Send and Sync just like Vec IntoIter, fix #8869

Signed-off-by: Robert Kruszewski <github@robertk.io>
@robert3005 robert3005 added the changelog/chore A trivial change label Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Polar Signals Profiling Results

Latest Run

Status Commit Job Attempt Link
🟢 Done fdaf786 1 Explore Profiling Data

Powered by Polar Signals Cloud

@github-actions

Copy link
Copy Markdown
Contributor

Benchmarks: Vortex queries 📖

Verdict: No clear signal (low confidence)
Attributed Vortex impact: +5.2%
Engines: DataFusion No clear signal (+7.3%, low confidence) · DuckDB No clear signal (+3.2%, low confidence)
Vortex (geomean): 1.008x ➖
Parquet (geomean): 0.983x ➖
Shifts: Parquet (control) -1.7% · Median polish +1.1%

How to read Verdict and Engines
  • Verdict: Overall PR-level signal after subtracting baseline drift estimated from Parquet control rows. It can be Likely improvement, Likely regression, or No clear signal.
  • Engines: Per-engine attribution. DataFusion is compared against DataFusion/Parquet controls; DuckDB is compared against DuckDB/Parquet controls. This answers whether each engine improved or regressed independently.
  • Confidence: Based on directional consistency, share of rows above the noise floor, and control-run noise.

datafusion / vortex-file-compressed (1.041x ➖, 0↑ 0↓)
name PR fdaf786 (ns) base 39f4abe (ns) ratio (PR/base)
vortex_q00/datafusion:vortex-file-compressed 9786474 9523459 1.03
vortex_q01/datafusion:vortex-file-compressed 6324871 5998335 1.05
datafusion / parquet (0.970x ➖, 0↑ 0↓)
name PR fdaf786 (ns) base 39f4abe (ns) ratio (PR/base)
vortex_q00/datafusion:parquet 20218238 20757506 0.97
vortex_q01/datafusion:parquet 4532087 4691686 0.97
duckdb / vortex-file-compressed (1.027x ➖, 0↑ 0↓)
name PR fdaf786 (ns) base 39f4abe (ns) ratio (PR/base)
vortex_q00/duckdb:vortex-file-compressed 10103765 10315691 0.98
vortex_q01/duckdb:vortex-file-compressed 6742507 6256783 1.08
duckdb / parquet (0.996x ➖, 0↑ 0↓)
name PR fdaf786 (ns) base 39f4abe (ns) ratio (PR/base)
vortex_q00/duckdb:parquet 23436087 23377171 1.00
vortex_q01/duckdb:parquet 9421921 9525013 0.99

No file size changes detected.

@robert3005
robert3005 enabled auto-merge (squash) July 21, 2026 09:40
@robert3005
robert3005 requested review from 0ax1 and joseph-isaacs July 21, 2026 09:40
end: *const T,
}

// SAFETY: `BufferIterator` is a `Buffer<T>` plus two cursors into it, so it can safely be

@0ax1 0ax1 Jul 23, 2026

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.

Doesn't the compiler auto-derive Send + Sync if T is so? Right bc of the pointer


// SAFETY: `BufferIterator` is a `Buffer<T>` plus two cursors into it, so it can safely be
// `Send`/`Sync` exactly when `Buffer<T>` is. Same bounds as `std::vec::IntoIter`.
unsafe impl<T: Copy + Send> Send for BufferIterator<T> {}

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.

@claude Can't we copy the iterator and share across threads such that it aliases and races?

@github-actions github-actions Bot Jul 23, 2026

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.

Claude finished @0ax1's task in 57s —— View job


Re: "Can't we copy the iterator and share across threads such that it aliases and races?"

Short answer: no, this can't cause a data race. A few reasons stack up:

1. BufferIterator is neither Copy nor Clone.
There's no derive and no manual impl (buffer.rs:709-714), so you literally cannot duplicate it. To move it to another thread you have to move it (Send), which leaves nothing behind on the original thread.

2. Even if it were cloneable, there'd still be no race.
The iterator only ever reads from the buffer — next() does self.ptr.read() (a copy out) and advances its own cursor (buffer.rs:725-733). It never writes into the backing Buffer<T>. Two independent iterators over the same buffer are just two concurrent readers of shared immutable memory, which is exactly what Buffer<T>: Sync already guarantees is safe (same as cloning a slice::Iter or holding two Arc readers).

3. Sync only exposes &self methods, none of which mutate.
Sync means &BufferIterator can be shared across threads. The only thing you can do through a shared & is call size_hint() (buffer.rs:736-740), which reads ptr/end immutably. Advancing the cursor requires next(&mut self), i.e. exclusive access — the borrow checker forbids two threads holding &mut to the same iterator simultaneously. So the aliasing-mutation-race scenario can't be constructed safely.

4. The bounds match the standard library.
T: Send/T: Sync are exactly the bounds std::vec::IntoIter<T> and slice::Iter<'_, T> use. The manual impls are needed only because the two raw pointers (*const T) opt the struct out of the auto-derive (which 0ax1 already noted above); the Send/Sync semantics of the underlying data are unchanged.

So the impls are sound: the cursors are owned per-iterator, the buffer is read-only during iteration, and mutation is gated behind &mut. This is the same reasoning that lets Vec::into_iter() be Send + Sync.

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.

@claude this is sound then?

@github-actions github-actions Bot Jul 23, 2026

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.

Claude finished @0ax1's task in 30s —— View job


Yes — this is sound. ✅

I re-read the current code at vortex-buffer/src/buffer.rs:709-740 and the reasoning from my earlier reply holds:

1. The bounds match std exactly.

unsafe impl<T: Copy + Send> Send for BufferIterator<T> {}
unsafe impl<T: Copy + Sync> Sync for BufferIterator<T> {}

These are the same bounds std::vec::IntoIter<T> (Send/Sync iff T) and slice::Iter<'_, T> use. The manual unsafe impls are only needed because the two *const T cursors (lines 712-713) opt the struct out of the auto-derive — exactly as you noted. They don't change the Send/Sync semantics of the underlying data.

2. No aliasing is possible.
BufferIterator is neither Copy nor Clone (no derive, no manual impl). To get it onto another thread you must move it (Send), which leaves nothing behind. You cannot end up with two cursors over the same buffer from a single iterator.

3. Iteration is read-only.
next() (lines 725-733) does self.ptr.read() (a copy-out, valid because T: Copy) and advances its own owned cursor. It never writes into the backing Buffer<T>. Even two independent iterators over the same buffer would just be two concurrent readers of shared immutable memory — already guaranteed safe by Buffer<T>: Sync.

4. Sync only exposes non-mutating &self access.
Through a shared &BufferIterator the only reachable method is size_hint() (lines 736-740), which reads ptr/end immutably. Advancing requires next(&mut self), i.e. exclusive access — the borrow checker forbids two threads holding &mut to the same iterator, so the alias-and-race scenario can't be constructed in safe code.

One optional nit (not a soundness issue): size_hint uses unsafe { self.end.offset_from(self.ptr) } without a // SAFETY: comment, while the other unsafe blocks in this struct have one. Adding a short justification (both pointers derive from the same allocation, end >= ptr is an invariant) would keep it consistent with the surrounding style.

So: the impls are sound, and equivalent to why Vec::into_iter() is Send + Sync.

@robert3005
robert3005 merged commit 3816cb3 into develop Jul 23, 2026
104 of 105 checks passed
@robert3005
robert3005 deleted the rk/bufferiter branch July 23, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/chore A trivial change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] BufferIterator should have implemented Send and Sync

2 participants