Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I borrow or copy my small data types?

Tags:

rust

struct Vec {
    data: [f32; 3],
}

impl Vec {
    fn dot(&self, other: &Vec) -> f32 {
        ..
    }
    // vs
    fn dot(self, other: Vec) -> f32 {
        ..
    }
}

I am currently writing a vector math library and I am wondering if I should borrow or copy my vector types.

At the moment I implement Copy for Vec which makes the API a bit nicer because you don't have to write & all the time.

But it requires much more complex constraints, because now all my constraints also need to satisfy Copy.

Which one potentially yields better performance and why?

Which one potentially yields better ergonomics and why?

Edit:

I have created a small microbenchmark

test bref_f32 ... bench:   2,736,055 ns/iter (+/- 364,885)
test bref_f64 ... bench:   4,872,076 ns/iter (+/- 436,928)
test copy_f32 ... bench:   2,708,568 ns/iter (+/- 31,162)
test copy_f64 ... bench:   4,890,014 ns/iter (+/- 553,050)

It seems that there is no difference between ref and copy for this example in terms of performance.

Copy seems to yield better ergonomics for library users.

like image 288
Maik Klein Avatar asked Jun 28 '16 00:06

Maik Klein


People also ask

Why borrow checker Rust?

The borrow check is Rust's "secret sauce" – it is tasked with enforcing a number of properties: That all variables are initialized before they are used. That you can't move the same value twice. That you can't move a value while it is borrowed.

Why borrow checker?

The borrow checker is an essential fixture of the Rust language and part of what makes Rust Rust. It helps you (or forces you) to manage ownership.

What is borrowing in Rust?

Rust supports a concept, borrowing, where the ownership of a value is transferred temporarily to an entity and then returned to the original owner entity.

How do you borrow as mutable in Rust?

First, we change s to be mut . Then we create a mutable reference with &mut s where we call the change function, and update the function signature to accept a mutable reference with some_string: &mut String . This makes it very clear that the change function will mutate the value it borrows.


2 Answers

Rust is not a pure academic language with lofty aesthetics goals and a singular purpose. Rust is a systems programming language, which implies pragmatism.

The general rule of thumb is that your interface should correctly document ownership:

  • pass by value when you give up ownership
  • pass by reference (possibly mutable) when you temporarily relinquish ownership

however the Copy trait is the perfect example of pragmatism at play. It recognizes that passing by reference can be cumbersome (does anyone want to type (&1 + &x) * &y?) and therefore creates an escape hatch for types that do not need to be affine (ie, no special action on destruction).

As a result, semantically, if your type can be guaranteed to be and remain Copy, then marking it as such gives users some leeway on its use which can improve ergonomics. I would encourage you to mark it, then, though with the reminder that later removing the Copy trait would be a backward incompatible change.

Once a type is Copy, I would not hesitate to take advantage of the fact and pass it by value. After all, if the type is never passed by value then there was no point in making it Copy in the first place.


The only caveat would be performance reason.

Copy does not force a copy, it merely allows it. This means that an optimizer has all latitude to use a copy... or not.

For small types the performance is unlikely to be much different whatever happens; for bigger types I would encourage benchmarking various interfaces if performance matters. Vec is only 1.5x the size of a pointer/reference on 64-bits architectures, so it really is in a gray area. Sometimes copying will be slower (larger copy) but sometimes the benefits of having a local copy will enable optimizations that would not be triggered with a pointer.

However such benchmarking is fraught with peril, notably because it hugely depends on whether the function is inlined, or not (given more or less leeway for removing copies).

like image 108
Matthieu M. Avatar answered Nov 15 '22 11:11

Matthieu M.


I'd recommend borrowing in this scenario because it does not seem like ownership is a concern. So, I guess your code would look like

struct Vec {
    data: [f32; 3],
}

impl Vec {
    fn dot(&self, other: &Vec) -> f32 {..}
}
like image 23
Eli Sadoff Avatar answered Nov 15 '22 09:11

Eli Sadoff