Help removing clones on AddAssign

Hello. I've implemented a simple RK4 solver and attempted to do as much of the calculations in place as I can. However, I need to add the results of the in place calculations during the stages. The Ks and X in the code are all of generic type T, which needs to support being a dynamically sized Vec at compile time, so does not generally implement Copy. I'm having trouble figuring out a strategy for removing the clones on the AddAssigns in the while loop. Any suggestions?

pub trait Integrable:
    AddAssign<Self> + MulAssign<f64> + Sized + Clone + Debug    
{
}

pub fn solve_fixed_rk4<F, P, T>(solver: &mut Solver<F, P, T>) -> (Vec<f64>, Vec<T>)
where
    F: OdeFunction<P, T>,
    T: Integrable,
{
    let Solver {
        func,
        x0,
        parameters,
        tstart,
        tstop,
        dt,
        ..
    } = solver;

    assert!(dt.abs() > f64::EPSILON, "0.0 dt not allowed!");

    let mut half_dt = *dt / 2.0;
    let mut dt_6 = *dt / 6.0;
    let mut x = x0.clone();
    let mut t = *tstart;

    // using resize instead of with_capacity so we can clone_from results instead of .push(clone())
    let result_length = ((*tstop - *tstart) / *dt).floor() as usize + 1;
    let mut result = Vec::new();
    result.resize(result_length, x0.clone());

    let mut time = Vec::new();
    time.resize(result_length, 0.0);

    result[0].clone_from(&x0);
    time[0].clone_from(&t);

    // one time clone for initialization
    let mut k1 = x0.clone();
    let mut k2 = x0.clone();
    let mut k3 = x0.clone();
    let mut k4 = x0.clone();
    let mut tmp = x0.clone();    

    for i in 1..result_length {
        // change dt near end of sim to capture end point
        if (*tstop - t) < *dt && (*tstop - t) > f64::EPSILON {
            *dt = *tstop - t;
            half_dt = *dt / 2.0;
            dt_6 = *dt / 6.0;
        }

        // calculate k1 = f(x,t)
        tmp.clone_from(&x);
        func(&mut k1, &tmp, parameters, t);

        // calculate k2 = f(x + 0.5*k1 , t + 0.5*dt)
        tmp.clone_from(&k1);
        tmp *= 0.5;
        tmp += x.clone();
        func(&mut k2, &tmp, parameters, t + half_dt);

        // calculate k3 = f(x + 0.5*k2 , t + 0.5*dt)
        tmp.clone_from(&k2);
        tmp *= 0.5;
        tmp += x.clone();
        func(&mut k3, &tmp, parameters, t + half_dt);

        // calculate k4 = f(x + k3 , t + dt)
        tmp.clone_from(&k3);
        tmp *= *dt;
        tmp += x.clone();
        func(&mut k4, &tmp, parameters, t + *dt);

        // calculate x = x + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * dt / 6.0;
        
        k1 *= dt_6;
        x += k1.clone();

        k2 *= 2.0 * dt_6;
        x += k2.clone();

        k3 *= 2.0 * dt_6;
        x += k3.clone();

        k4 *= dt_6;
        x += k4.clone();

        t += *dt;

        // call any callbacks for the new state now
        for cb in solver.callbacks.iter_mut() {
            cb(&mut x, parameters, &mut t);
        }

        result[i].clone_from(&x);
        time[i].clone_from(&t);
    }

    (time, result)
}


Have it take the RHS by reference: use for<'a> AddAssign<&'a Self> as the bound. Compilable example:

use core::fmt::Debug;
use core::ops;

pub trait Integrable:
    for<'a> ops::AddAssign<&'a Self> + ops::MulAssign<f64> + Sized + Clone + Debug    
{
}
3 Likes

Why not AddAssign<&Self> instead of AddAssign<Self>, then?

2 Likes

I had thought of doing AddAssign<&'a Self>, but wasn't sure if performing the add_assign() for a non copy type would still incur a clone(). I have to go change all of my Adds to AddAssigns now since I was doing things out of place, but will give it a shot. Thanks!

I don't really get the connection here. If you require AddAssign<&Self>, that means the RHS can be a reference, and you don't have to provide a clone of the owned value.

1 Like

You don't need to do the clone, and you should assume that the AddAssign implementation doesn't do unnecessary clones (anything else would be crazy).

So, for example, the following code does not contain any unnecessary clones or copies, because impl AddAssign<&f64> for f64 does not clone (because it'd be crazy otherwise):

pub fn add_ref(mut in1: f64, in2: &[f64]) -> f64 {
    for v in in2 {
        in1 += v;
    }
    in1
}

fn main() {
    let v = vec![1.0, 2.0, 3.0, 4.0, 5.0, 0.5, 0.25, 0.125];
    println!("{}", add_ref(42.0, &v));
}

The only time a clone is needed is when you're changing the left-hand side of the AddAssign - if I needed the input value of in1 after the for loop, for example, I'd need to copy it (in this case, because I know the type implements Copy, I wouldn't need a clone).

1 Like

Yea it seems obvious now but I think my initial train of thought was that because I was passing rhs as a reference, I would have to dereference it to get the value, but as I understand it dereferencing either moves or copy's. Since I don't want to move and can't copy, I'd have to clone. But now I think it makes sense that I don't have to dereference rhs itself since I'm just accessing its fields in the addition. Thanks all for the feedback!

I was able to run my simple model in place, though some bugs showed up from my conversion from out of place. If anyone sees this in the future, note there are some obvious bugs in the rk4 code above.

No, dereferencing results in a place. That doesn't move or copy in itself.

Note that, ultimately, something, somewhere must be copied. The CPU will have to read the actual bits in order to perform the operation at the end of the day. That's physically unavoidable.

What you are avoiding by not calling the literal Clone method is any potential resource allocation (eg. heap buffers) on the RHS. You seem to be conflating this with the actual copying of the underlying bits. They are two distinct and separate operations.

3 Likes

In general, Rust is ideologically committed to not calling Clone::clone unless you explicitly write out .clone() or .cloned() or something of the sort.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.