Vec<f32> -> Vec<Cell<f32>> zero copy

This is a follow up to N f32's <-> Rc pointers <-> *mut f32 for blas/C - #12 by zeroexcuses

I want to write a function with type:

pub fn foo(input: Vec<f32>) -> Vec<Cell<f32>> ...

I want to do this in a zero-copy manner -- to have the Vec just 'vanish' (without dropping), and to have the Vec<Cell> to it's memory.

Context: I have functions that return Vec. I don't think it makes sense to change them to return Vec<Cell>, but there are situations where I want the returned result changed into a Vec<Cell>, perferably without copying.

You can do this with mem::transmute.

Given taht mem::transmute can take arbitrary types as inputs, how does it know that Vecs have a length field and to call it? Or does it not matter, and something like the following happens:

  1. each Vec has a "fixed size component" (length, capacity, pointer to heap addr that contains actual Vec elements)

  2. transmute never touches the heap elements; it just looks at the "Vec header" and pretends the bits forms a "Vec<Cell> header" -- so it never follows the pointer to the heap, and it never goes the heap, performing any op per element

If you don't mind nightly APIs, you can also get a &mut [f32] from the Vec, then construct a &Cell<[f32]> from it using from_mut, and then subsequently construct a &[Cell<f32>] from that using as_slice_of_cells. It's a bit convoluted, but it may do the job you want of it if you can use a slice of cells for the thing you want to do.

transmute doesn't understand anything about Vec or any other type. It merely cheats the type system (it's a syntax sugar for casting pointers).

However, it should work, because Cell<f32> and f32 have identical size and memory representation, so vec's length/capacity will be the same, and pointer to the data will have the same alignment.

2 Likes

Note that using a transmute here is officially UB right now, because Vec<f32> and Vec<Cell<f32>> are different repr(Rust) types which the compiler is thus free to layout in different ways.

The correct thing to do here is to use Vec::from_raw_parts like so

use std::cell::Cell;
use std::mem::ManuallyDrop;

pub fn cellify<T>(v: Vec<T>) -> Vec<Cell<T>> {
    let v = ManuallyDrop::new(v);
    unsafe { Vec::from_raw_parts(v.as_ptr() as _, v.len(), v.capacity()) }
}

(The pointer cast is sound because Cell and UnsafeCell are repr(transparent).)

2 Likes

@scottmcm : Thanks for your insightful post. Let us see if I can take it apart step by step.

  1. Cell, UnsafeCell being repr(transparent) means that Cell and UnsafeCell are laid out exactly the same way as T. This is why it's okay for us to do a Vec.as_ptr() and interpret it as a pointer for Cell.

  2. Vec, Vec<Cell> have type repr(Rust). This means Rust is hypothetically allowed to do specialized layout for one or both of them, thus resulting in the undefined behaviour. We get around this potential UB by constructinga new Vec directly via Vec::from_raw_parts

  3. In our actual function, we do:

pub fn cellify<T>(v: Vec<T>) -> Vec<Cell<T>> {
    let v2 = ManuallyDrop::new(v);
    let v3 = unsafe { Vec::from_raw_parts(v2.as_ptr() as _, v2.len(), v2.capacity()) }
    v3
}```

After the "let v2 = ..." line, "v" is no longer valid, as it has been moved. However, when v2 goes out of scope, there is no auto drop since it's a ManuallyDrop, and we do not have a ManuallyDrop::drop(v2) call.

We construct the v3, which if of type Vec<Cell<T>>. When v3 drops, all the objects in the vec will be freed. v3 gets returned at the end of the function.

This process is O(1) time. We construct a new Vec, but we don't do anything to the elements of the Vec.

Is this all correct?

I think for Vecs, it can be done because vectors can be made with raw parts, but what about HashMap or BTreemap where we need a conversion from , say, Hashmap<i32, Cell<T>> to Hashmap<i32, T>

We might need this kind of conversion when we want the Hashmap modified in a single-thread with internal-mutability and when we are done with that, convert it and then access it from multiple threads, which is safe.

but according to @ucottmcm transmuting directly causes UB.
So, this problem can be pretty much generalized to a container of cells situation, and I'm wondering if there is any best practice of it.

1 Like

Disclaimer: I'm the author of thincollections. (I'm new to the site... is this ok?)

If you use a ThinVec from thincollections, you can use a transmute method on there.

The method is marked as unsafe, but does have some safeguards and it should be fine in your case.

Edit: oops, for safety, I have constrained the types to be Copy, but Cell is not. What I really wanted was for there to be no Drop implementation, but I don't think I can do that with the compiler. So I moved the check into the method, and released 0.5.1 with this change.

1 Like

Yep, that's all correct.