The Add
trait is defined as seen in the documentation.
When implementing it for a Vector, it was required to copy it into the add method to allow syntax like v1 + v2
. If the add
implementation is changed to support borrowed references and thus prevent a copy, one has to write &v1 + &v2
which is undesirable.
What is the preferred or best performing way of doing this ?
(In C++, self would be a const Vector<T>&
, as well as rhs
, but still allow the desired v1 + v2
semantics.)
The Code
For completeness, an excerpt of the code I am using right now
use std::num::Float;
use std::ops::Add;
#[derive(Debug, PartialEq, Eq, Copy)]
pub struct Vector<T: Float> {
x: T,
y: T,
z: T,
}
impl<T: Float> Add for Vector<T> {
type Output = Vector<T>;
// Probably it will be optimized to not actually copy self and rhs for each call !
#[inline(always)]
fn add(self, rhs: Vector<T>) -> Vector<T> {
Vector { x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z }
}
}
#[cfg(test)]
#[test]
fn basics() {
let v32 = Vector { x: 5.0f32, y: 4.0f32, z: 0.0f32 };
let v32_2 = v32 + v32;
assert_eq!(v32_2.x, v32.x + v32.x);
assert_eq!(v32_2.y, v32.y + v32.y);
assert_eq!(v32_2.z, v32.z + v32.z);
}
Since your Vector
only contains three values implementing Float
trait (which means that they are either f64
or f32
) you shouldn't really bother that they are copied unless you have profiled your program and determined that multiple copies cause performance drop.
If your type was not copyable and required allocations on construction (like big integers and big floats, for example), you could implement all possible combinations of by-value and by-reference invocations:
impl Add<YourType> for YourType { ... }
impl<'r> Add<YourType> for &'r YourType { ... }
impl<'a> Add<&'a YourType> for YourType { ... }
impl<'r, 'a> Add<&'a YourType> for &'r YourType { ... }
and reuse the allocated storage in implementations which accept at least one argument by value. In that case, however, you will need to use &
operator if you don't want to move your values into the call. Rust prefers explicit over implicit; if you need reference semantics, you have to write it explicitly.
FWIW, you can take a look at this program and especially its assembly output. This piece of assembly, I believe, is responsible for all arithmetic operations:
shrq $11, %r14
cvtsi2sdq %r14, %xmm0
mulsd .LCPI0_0(%rip), %xmm0
shrq $11, %r15
cvtsi2sdq %r15, %xmm1
mulsd .LCPI0_0(%rip), %xmm1
shrq $11, %rbx
cvtsi2sdq %rbx, %xmm2
mulsd .LCPI0_0(%rip), %xmm2
movaps %xmm0, %xmm3
addsd %xmm1, %xmm3
movaps %xmm1, %xmm4
addsd %xmm2, %xmm4
movaps %xmm0, %xmm5
addsd %xmm2, %xmm5
addsd %xmm2, %xmm3
addsd %xmm0, %xmm4
addsd %xmm1, %xmm5
movsd %xmm3, 24(%rsp)
movsd %xmm4, 32(%rsp)
movsd %xmm5, 40(%rsp)
leaq (%rsp), %rdi
leaq 24(%rsp), %rsi
callq _ZN13Vec3$LT$T$GT$9to_string20h7039822990634233867E
Looks neat to me - the compiler has inlined all operations very nicely.