First, let me begin with a small discussion about C++ rvalue references. As some of you know, they were introduced to C++ in part to solve problems like this:

Matrix m;
m.data = {1.0, 2.0, 3.0};
Matrix m2 = m * 2.0 * 5.0 * 10.0;

Before C++11, most implementations the multiplications on the third line would create two (unnecessary) temporary copies of the Matrix, causing widespread inefficiency if Matrix was large. By using rvalue references (see the implementation in this gist: https://gist.github.com/SiegeLord/85ced65ab220a3fdc1fc we can reduce the number of copies to one. What the C++ does is that the first multiplication (* 2.0) creates a copy of the matrix, and the remaining multiplications move that copy around.

If you look at the implementation, you'll note how complicated the C++ move semantics are compared to Rust's (you have to use std::move everywhere, define move-constructors and move-assignment with easy-to-get-wrong implementations etc.). Since Rust has simpler move semantics, can we do the same thing in Rust?

It turns out we cannot, because the operator overloading in Rust is done by overloading a trait with a method that takes self by reference:

pub trait Mul<RHS, Result>
{
    fn mul(&self, rhs: &RHS) -> Result;
}

This means that the crucial step of moving out from the temporary cannot be done without complicated alternatives (explained at the end of this email). If we define an a multiplication trait that takes self by value, however then this is possible and indeed relatively trivial (see implementation here: https://gist.github.com/SiegeLord/11456760237781442cfe ). This code will act just like the C++ did: it will copy during the first move_mul call, and then move the temporary around:

let m = Matrix{ data: vec![1.0f32, 2.0, 3.0] };
let m2 = (&m).move_mul(2.0).move_mul(5.0).move_mul(10.0);

So there's nothing in Rust move semantics which prevents this useful pattern, and it'd be possible to do that with syntax sugar if the operator overload traits did not sabotage it. Pretty much all the existing users (e.g. num::BigInt and sebcrozet's nalgebra) of operator overloading traits take the inefficient route of creating a temporary copy for each operation (see https://github.com/mozilla/rust/blob/master/src/libnum/bigint.rs#L283 and https://github.com/sebcrozet/nalgebra/blob/master/src/structs/dmat.rs#L593 ). If the operator overloading traits do not allow you to create efficient implementations of BigNums and linear algebra operations, the two use cases why you'd even *have* operator overloading as a language feature, why even have that feature?

I think this goes beyond just operator overloading, however, as these kinds of situations may arise in many other traits. By defining trait methods as taking &self and &mut self, we are preventing these useful optimizations.

Aside from somewhat more complicated impl's, are there any downsides to never using anything but by value 'self' in traits? If not, then I think that's what they should be using to allow people to create efficient APIs. In fact, this probably should extend to every member generic function argument: you should never force the user to tie their hands by using a reference. Rust has amazing move semantics, I just don't see what is gained by abandoning them whenever you use most traits.

Now, I did say there are complicated alternatives to this. First, you actually *can* move out through a borrowed pointer using RefCell<Option<T>>. You can see what this looks like here: https://gist.github.com/SiegeLord/e09c32b8cf2df72b2422 . I don't know how efficient that is, but it is certainly more fragile. With my by-value MoveMul implementation, the moves are checked by the compiler... in this case, they are not. It's easy to end up with a moved-out, dangling Matrix. This is what essentially has to be done, however, if you want to preserve the general semantic of the code.

Alternatively, you can use lazy evaluation/expression templates. This is the route I take in my linear algebra library. Essentially, each operation returns a struct (akin to what happens with many Iterator methods) that stores the arguments by reference. When it comes time to perform assignment, the chained operations are performed element-wise. There are no unnecessary copies and it optimizes well. The problem is that its a lot more complicated to implement and it pretty much forces you to use interior mutability (just Cell this time) if you don't want a crippled API. The latter bit introduces a whole slew of subtle bugs (in my opinion they are less common than the ones introduced by RefCell). Also, I don't think expression templates are the correct way to wrap, e.g., a LAPACK library. I.e. they only work well when you're implementing the math yourself which is not ideal for the more complicated algorithms. Along the same lines, it is not immediately obvious to me how to extend this lazy evaluation idea to something like num::BigInt. So far, it seems like lazy evaluation will force dynamic dispatch in that case which is a big shame (i.e. you'd store the operations in one array, arguments in another and then play them back at the assignment time).

So, I think the situation is pretty bad. What can be done to fix it?

-SL
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to