Skip to content

switch the buffer type to bytes instead of bigarray/cstruct - #153

Draft
avsm wants to merge 3 commits into
ocaml-multicore:mainfrom
avsm:switch-to-bytes
Draft

switch the buffer type to bytes instead of bigarray/cstruct#153
avsm wants to merge 3 commits into
ocaml-multicore:mainfrom
avsm:switch-to-bytes

Conversation

@avsm

@avsm avsm commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

This is a surprisingly small diff, since most of what we need to do is just make sure the backing bytes is >2KB.

@talex5

talex5 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Does benchmarking show an improvement?

@avsm

avsm commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

"Better", but I'm working on quantifying this. Stay tuned.

@avsm

avsm commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

I ran some benchmarks against both branches, and the microbenchmarks show:

  • no regression in I/O throughput for the filesystem benchmarks, and an obvious improvement when the library uses bytes (and so avoids a Cstruct.to_bytes as most of my e.g. http libraries do)
  • improves large-buffer allocation cost >2kb is better with bytes as the bigarray finalizer and C proxy is not needed anymore. Also I'd forgotten that Cstruct zeros buffers whereas we use Bytes.create which leaves them uninitialised (probably a better default given we're about to overwrite it anyway?)
  • small buffers are worse with bytes since we clamp allocations to 2KB to make them immovable. Callers to this library will need to be careful to coalesce small writes before sending them to the kernel, but this is good practise anyway.

@talex5

talex5 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Bytes.create which leaves them uninitialised (probably a better default given we're about to overwrite it anyway?)

It only gets overwritten if there's enough data to fill the buffer. Otherwise you're leaking private data. Could have a create_unsafe though.

I'm wondering if we should have an Iovec.to_cstruct that shares the underlying memory. Then the buffer could be accessed by whatever API you had to use without any copying.

@avsm

avsm commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

I'm thinking that if the caller library provides buffers that are 2kb+, then we avoid this issue as the caller can design how to provide memory that meets their own criteria. All we need to do in uring is to verify that any buffer coming in is of a suitable size, which seems like a cheap check to do.

avsm added 3 commits August 19, 2026 18:22
We need a custom bigarray here in order to alias an existing bytes buffer in
the heap. We need to ensure that no copy of the bytes is made, but also that
the bytes arent freed while the bigarray (and any slices thereof) are also
still live.

A 'normal' bigarray cant express this aliasing. A CAML_BA_EXTERNAL bigarray
doesnt do any lifetime tracking, while a CAML_BA_MANAGED bigarray has the
runtime call free() which would corrupt the heap when the storage is
`bytes`.

We therefore allocate the bigarray as CAML_BA_MANAGED | CAML_BA_SUBARRAY
with a hand-built proxy attached at creation. This is morally ok since the
bigarray is indeed a subarray, just not of another bigarray!  Managed mode
allows every derived bigarray slice to be tracked via the runtime's proxy.
Pre-attaching the proxy with a NULL data pointer halts the free paths when
the refcount reaches zero, while the BA_MANAGED status stops free being
called on the bytes.

Keeping the bytes alive is then done purely on the OCaml side. A finaliser
closure that captures the bytes is attached to the original bigarray, and
serves as the GC root. When the original becomes unreachable while the proxy
refcount shows other family members are still live, the finaliser
re-registers itself on the same value. The chain terminates when the
refcount is 0, after which the bytes become collectable as normal since the
bigarray will be GCed.

This only works if the bytes themselves dont move, which is guaranteed by
the OCaml 5 runtime minimum size. Phew!
@avsm
avsm force-pushed the switch-to-bytes branch from a81cc4a to 4116ced Compare August 20, 2026 07:59
@avsm

avsm commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@talex5 wrote:

I'm wondering if we should have an Iovec.to_cstruct that shares the underlying memory. Then the buffer could be accessed by whatever API you had to use without any copying.

This took a few iterations, but I have a glorious hack in 4116ced that does successfully alias non-moving bytes. It create a proxy managed bigarray and a finaliser that ties the bytes to the bigarray. This scheme has the advantage that there's no performance impact if bigarrays aren't used (which is our eventual end goal).

Still stress testing it but it doesn't seem to obviously crash...

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants