I’m studying Rust and want to implement matrix multiplication.
Since matrices can be very large, I don’t want to implement Copy for my Matrix type.
fn main() {
let mut mat_a = MyMatrix::new();
let mat_b = MyMatrix::new();
// This works fine
mat_a.mul_assign(&mat_b);
// Compile error: cannot borrow `mat_a` as mutable because it is also borrowed as immutable
// mat_a.mul_assign(&mat_a);
mat_a.mul_assign_self();
}
struct MyMatrix {
// some data
}
impl MyMatrix {
pub fn new() -> Self {
Self {}
}
pub fn mul_assign(&mut self, _rhs: &Self) {
println!("mul_assign called");
// matrix multiplication logic
}
pub fn mul_assign_self(&mut self) {
println!("mul_assign_self called");
// exactly the same logic, except rhs is self
}
}
I understand why Rust does not allow mat_a.mul_assign(&mat_a): self is mutably borrowed while rhs is immutably borrowed, and both refer to the same value. That could be unsafe if self is modified during the computation.
However, in my implementation of mul_assign, the first thing I do is transpose rhs into temporary memory before making any changes to self.
Given that, is there a way to reuse the same multiplication function for both multiplying by another matrix and multiplying by itself?
it is IMPOSSIBLE to call a function with a signature fn(a: &mut T, b: &T) -> Ret where a and b point to the same variable: by definition, the type &mut T is an exclusive reference, although the keyword mut is usually taught to be a shorthand for "mutable", which I think is an unfortunate misnomer.
as explained, if the function haa an argument of type &mut Self. then it's impossible to have other reference arguments pointing to the same variable.
you can reuse the code of the multiplication kernel, but you need a wrapper for the self-multiply case:
pub fn mul_assign_self(&mut self) {
// create a transposed copy as the rhs
// assuming the signature: fn transpose(&MyMatrix) -> MyMatrix
let t = transpose(&*self);
// forward to the multiply kernel using the temporary rhs
self.mult_assign(&t)
}
then take rhs by-value instead of by-ref, and let the caller construct the temporary value. this also works around the self-multiply issue, thanks to two-phase borrowing:
// signature:
pub fn mul_assign(&mut self, _rhs: Self);
// this works as always
mat_a.mul_assign(mat_b);
// or, if `mat_b` is used later, clone it
//mat_a.mul_assign(mat_b.clone());
// this also works
mat_a.mul_assign(mat_a.clone());
Could you elaborate on it, please? You already seem to transpose rhs into temporary memory, so changing where the allocation happens shouldn't change the cost much. You could have a common function which takes the already-transposed right-side matrix.
If you want the same code and without raw pointer, and the multiplication result must be stored to self because I see &mut there that you may want to edit the self with the result of the multiplication, the solution that I can think of is runtime branch
fn main() {
let mut mat_a = MyMatrix::new();
let mat_b = MyMatrix::new();
// this works fine
mat_a.mul_assign(Some(&mat_b));
// no compile error cannot borrow `mat_a` as mutable because it is also borrowed as immutable
mat_a.mul_assign(None);
mat_a.mul_assign_self();
}
struct MyMatrix{
// some data value
}
impl MyMatrix {
pub fn new() -> Self {
Self{}
}
pub fn mul_assign(&mut self, _rhs: Option<&Self>) {
println!("mul_assign called")
// do Matrix multiple
// if some then a and b is different matrix
// if none then both a and b is self, muliply with itself
}
pub fn mul_assign_self(&mut self) {
println!("mul_assign self called")
// exactly the same code, just replace rhs with self
}
}
There is no clone. But there is 1x branching, if it is called multiple times then it becomes 1 x n times branching
If you accept 2 different method solution, you can have code without branching while still get noalias optimization and other optimizations that rely on noalias guarantee because it is reference not pointer
I want to hide the implementation details from the caller, so the caller does not need to worry about how the multiplication is performed or whether it requires a transposed right-hand side.
If I take an immutable reference to a clone, as Schard mentioned before, there will be two memory allocations.
fn main() {
// ...
// caller
let clone = self.clone();
self.mul_assign(&clone); // memory allocation
// ...
}
pub fn mul_assign(&mut self, rhs: &Self) {
let t = transpose(&*self); // memory allocation
// I cannot transpose in place because rhs is an immutable reference
// ...
}
pub fn mul(self, rhs: &Self)->Self {
traspose_inplace(self)
//actual trasposed_self*rhs
self
}
pub fn main(){
let mut mat_a = MyMatrix::new();
let mat_b = MyMatrix::new();
mat_a=mat_a.clone().mul(&mat_b);//1 alloc like the original mul_assign
mat_a=mat_a.clone().mul(&mat_a);//1 alloc like the original mul_assign
let mat_a =mat_a.mul(&mat_b);//works with 0 allocs
}