What is the preferred way to implement the Add tra

2020-04-17 05:10发布

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);
}

标签: rust
1条回答
对你真心纯属浪费
2楼-- · 2020-04-17 06:07

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.

查看更多
登录 后发表回答