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.
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.
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.
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.
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.
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:
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).
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 {..}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With