13

I'm writing an application in Rust that will have to use vector arithmetic intensively and I stumbled upon a problem of designing operator overload for a structure type.

So I have a vector structure like that:

struct Vector3d {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

and I want to be able to write something like that:

let x = Vector3d {x:  1.0, y: 0.0, z: 0.0};
let y = Vector3d {x: -1.0, y: 0.0, z: 0.0};

let u = x + y;

As far as I can see, there are three different ways to do it:

  1. Implement std::ops::Add trait for Vector3d directly. That works, but this trait's method signature is:

    fn add(self, other: Vector3d)
    

So it will invalidate its arguments after usage (because it moves them) which is undesirable in my case since many vectors will be used in multiple expressions.

  1. Implement Add trait for Vector3d and also implement the Copy trait. This works, but I feel iffy on that since Vector3d isn't exactly a lightweight thing (24 bytes at least) that can be copied quickly, especially when there are many calls to arithmetic functions.

  2. Implement Add for references to Vector3d, as suggested here. This works, but in order to apply the operator, I will have to write

    let u = &x + &y;
    

I don't like this notation because it doesn't exactly looks like its mathematic equivalent, just u = x + y.

I'm not sure which variant is optimal. So, the question is: is there a way to overload the '+' operator in such a way that

  1. It accepts its arguments as references instead of copying or moving them;
  2. It allows to write just u = x + y instead of u = &x + &y?
Community
  • 1
  • 1
Month
  • 293
  • 1
  • 11
  • Have you measured the performance impact of either strategy? Before worrying about "optimal", I'd check whether the baseline is good enough. – Matthieu M. Oct 28 '16 at 10:45
  • I'm afraid all 'artificial' tests that I've comed up with (like adding a billion or so pairs of vectors) are not conclusive. They provide near equal results for all three strategies. I suspect this is because of the simplicity of the test: the compiler optimizes it each time to almost the same result. The question is whether will it hold when applied to something that is not so easy for compiler to optimize. – Month Oct 28 '16 at 11:27
  • 1
    Indeed micro-benchmarks being optimized may indeed not always translate into real word optimizations. However I was not talking about micro-benchmarks specifically. My advice about writing code is always to first start with idiomatic code (strongly compile-time checked, unit-tested, ...) and then profile. *If* the profiler points at an area where a micro-optimization is worth it, then try it out (Rust touts fearless refactoring after all!), however the biggest optimization gains generally come from algorithmic changes. – Matthieu M. Oct 28 '16 at 11:36
  • This is a logic that I'm usually is trying to follow =) However, I'm still a bit new to Rust, so I'm not always sure which code can be considered ideomatic (or, as in this question, whether or not the desired effect can be achived within the ideomatic means). I have a C/C++ background, and there I've always considered questions like that (reference, copy, or const T& a parameter) to be something basic and straightforward rather than a 'micro-optimization'. – Month Oct 28 '16 at 11:44
  • C++ is a bit different here because of the copy constructor, so that a copy in C++ can be very costly. And also because the const-ness is only skin-deep. On the other hand, Rust's `&T` has transitive constness and a copy in Rust is guaranteed to be a simple bitwise (shallow) copy. – Matthieu M. Oct 28 '16 at 11:47

1 Answers1

11

Is there a way to overload the '+' operator in such a way that

  1. It accepts its arguments as references instead of copying or moving them;
  2. It allows to write just u = x + y instead of u = &x + &y?

No, there is no way to do that. Rust greatly values explicitness and hardly converts between types automatically.

However, the solution to your problem is simple: just #[derive(Copy)]. I can assure you that 24 bytes are not a lot. Computers these days love to crunch a lot of data at once instead of working on little chunks of data.


Apart from that, Copy is not really about the performance overhead of copying/cloning:

Types that can be copied by simply copying bits (i.e. memcpy).

And later in the documentation:

Generally speaking, if your type can implement Copy, it should.

Your type Vector3d can be copied by just copying bits, so it should implement Copy (by just #[derive()]ing it).

The performance overhead is a different question. If you have a type that can (and thus does) implement Copy, but you still think the type is too big (again: 24 bytes aren't!), you should design all your methods in a way that they accept references (it's not that easy; please read Matthieu's comment). This also includes the Add impl. And if you want to pass something to a function by reference, the programmer shall explicitly write it. That's what Rust's philosophy would say anyway.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • 5
    *you should design all your methods in a way that they accept references* => I'd first check the IR. Compilers do not pass large types by value, they actually pass references to temporary copies (because registers), and if the called function is inlined the optimizer might simply elide the temporary copy and directly refer to the original. In which case, while you semantically pass by copy, at assembly level you are passing by immutable reference. – Matthieu M. Oct 28 '16 at 10:44