Simple recursive sequence, problems with ownership

hi :slight_smile:
I decided to play with big numbers in Rust. I went with num-bigint (0.2.6) but am having issues with whole ownership thing.

The sequence I want to generate is defined:
a_next = a_current ^2 + 85 , a_0 is 36

The code is quite simple

extern crate num_bigint;
use num_bigint::BigUint;
    
fn main() {
    println!("seq(10) = {}", seq(10));
}

fn seq(n: u32) -> BigUint {
    let mut res: BigUint = BigUint::from(36u32);
    let add = BigUint::from(85u32);
    
    for _ in 2..n {
        res = res * res + add;
    }
    return res;
}

When I try to build I get errors like:

move occurs because res has type num_bigint::BigUint, which does not implement the Copy trait
value used here after move

I have no idea where to start :confused:. The bigint has similar example with fibonacci sequence but I can't connect the two. Help :cold_face:

Try changing this:

res = res * res + add

to this:

res = res.clone() * res.clone() + add

I haven't looked at the source for BigUint, but that's what I'd try first in your situation.

If it works, then the reason is likely to be that in order to allocate numbers that are arbitrarily long, the author chose to use heap storage (eg Vec<T>) rather than stack storage, which in turn means that those types cannot implement the Copy trait required to make your example work as-is.

I added .clone() to both res and add and now it works. But I am still baffled by the whole ownership thing or the reason why it doesn't work.

Regular numbers (eg of type usize, i8 etc) implement the Copy trait.

When a type implements this trait, it means that you can pass a value of that type by value to an expression or function call, without consuming the original value in the process.
If a type does not implement the Copy trait, such usage will consume the original value, that is it won't be available anymore after the expression or function call.
But with values of types that do implement Copy, the original value stays available i.e. it is copied rather than moved.

But in order to implement Copy, all the information of each value a type can have must be allocated on the stack rather than the heap (this is true for usize, i8, u128 etc) This is where what I said before comes in: if the author used eg a Vec<Digit> encoding for large numbers, that means that each digit of such a number is allocated on the heap, and thus the type cannot implement Copy.

Hopefully that helps clarify it a bit.

I tried a little example

let mut res: BigUint = BigUint::from(36u32);
res += BigUint::from10u32); 
res += BigUint::from(10u32);

and also with a loop

for _ in 2..6 {
    res += BigUint::from(10u32);
}

.. and this works without ownership issue.
But when I try...

let mut res: BigUint = BigUint::from(36u32);
res += res + BigUint::from(10u32); 
res += BigUint::from(10u32);

... I get the move/borrow thing again.

So what I get from it is when variable is used in expression (without pointers) it trys to copy itself, but if it cannot be copied it is moved, and since it is moved it has a new address.. and if any other piece of code would rely on information at the previous address, at this point it would be worthless.. so compiler spots it and prevents it from compiling since it might be potentialy unsafe under some circumstances.

Now the question is.. can the same thing be accomplished using pointers. It seems to me that num_bigint somehow overrides/expands the * and + operators. It does seem to have multiple options, like
impl<'a> Add<&'a BigUint> for BigUint
impl<'a, 'b> Add<&'b BigUint> for &'a BigUint
and also value+value
impl Add<BigUint> for BigUint

Just have to check how the Rust syntax works at this point. It seems to me that Add operation can also be achieved over 2 addresses that corespond to 2 BigUint variables .. will check tomorow.

I red the documentation and used impl<'a, 'b> Mul<&'b BigUint> for &'a BigUint.
res = res.clone() * res.clone() + add.clone() was changed to res = &res * &res + &add. After some digging thru the source code I noticed .clone() is also used in some places when values are passed.

The goal of this was to create a Mersenne prime tester using Lucas-Lehmer method. I re-tested some small results from GIMPS up to Mersenne prime number 30 - 132 049 .

I got times - sec
9686 - 5.30
9941 - 5.61
11213 - 8.72
19937 - 44.45
21701 - 56.19
23209 - 70.33
44497 - 469.96

Then I decided to plot it to derive time for some random p. I used mycurvefit.com with non-linear power fitting and got equation y = 1.102431e-11*x^2.932175 which I pasted to wolframalpha.com and predicted times for next 3 Mersenne primes - 86 243, 110503 and 132 049. then I ran the tests.

The results are:
86 243 - eq: 3271.58 - actual:3162.42
110503 - eq: 6767.17 - actual: 6599.85
132049 - eq: 11408.90 - actual 11257.44

So the fit was quite good. Now I will try to use ramp crate which is listed as a num-bigint alternative and post both codes and results in some reasonable time.

1 Like

I'm sure that I would have gotten it wrong too on first try, being too used to automatic memory management from the JVM. :wink:

After reading all the explanations on the borrowing I do get though why the original code doesn't work.
Changing it to use references does work however:
res = &res * &res + &add;

Compiles and runs, with n=10 that prints:
883140121731708497341866930020235680985241979545012366307484387818434268911884633667569186958462146183914960196709355869390590478650673212980058840486188609844577149365953963439393759290366007112334707192051102071327834795611566708899847183196905208639208084617000703737848525646891211541340791844720370972173868067535536349741686036421030245763161780327249187293228212911761354890124026811728805457286

1 Like

I implemented Lucas-Lehmer Mersenne testing algorithm with num-bigint and ramp crates. TLDR; version is that ramp is about 8 times faster, the time taken/time complexity rises roughly with n^3 as mentioned in a prior post.

Bellow are both source codes with times.

num-bigint, v0.2.6 , compiled with rustc 1.43.0 - current stable version

extern crate num_bigint;
use num_bigint::BigUint;
use std::env;
use std::time::Instant;

fn main() {
    // reading argument
    let args: Vec<String> = env::args().collect();
    let p: u32 = args[1].parse().unwrap();

    let start = Instant::now();
    let val: BigUint = mersenne(p);
    let duration = start.elapsed();

    println!("mersenne({}) = {}", p, val);
    println!("duration: {:?}", duration);
}

fn mersenne(p: u32) -> BigUint {
    // Lucas-Lehmer test
    // check if (p-1)-th place LL sequence member (mod 2^p - 1) is divisible by 2^p - 1
    let mut ll: BigUint = BigUint::from(4_u8);
    let two: BigUint = BigUint::from(2_u8);
    let mut m: BigUint = BigUint::from(1_u8);

    for _ in 0..p {
        m *= &two;
    }

    m = &m - BigUint::from(1_u8);

    //println!("m = {}", m);
    //println!("ll[{}] = {}", 1, ll);
    for i in 2..p {
        if i % 1000 < 1 { println!("Logging {}/{} - {:.2} %", i, p, ((i as f64) * 100.)/(p as f64)); }
        ll = &ll * &ll - &two;
        ll %= &m;
        //println!("ll[{}] = {}", i, ll);
    }

    return ll;
}

ramp, v0.5.7 , compiled with rustc 1.45.0-nightly (7ced01a73 2020-04-30) - current nightly version
NOTE There was an issue with ramp v0.5.6 and some std::ptr interface changes in latest nightyl version - in v0.5.7 it was fixed.

extern crate ramp;
use ramp::Int;
use std::env;
use std::time::Instant;

fn main() {
    // reading argument
    let args: Vec<String> = env::args().collect();
    let p: u32 = args[1].parse().unwrap();

    let start = Instant::now();
    let val: Int = mersenne(p);
    let duration = start.elapsed();

   println!("mersenne({}) = {}", p, val);
   println!("duration: {:?}", duration);
}

fn mersenne(p: u32) -> Int {
    // Lucas-Lehmer test
    // check if (p-1)-th place LL sequence member (mod 2^p - 1) is divisible by 2^p - 1
    let mut ll: Int = Int::from(4_u8);
    let two: Int = Int::from(2_u8);
    let mut m: Int = Int::from(1_u8);

    for _ in 0..p {
        m *= &two;
    }

    m = &m - Int::from(1_u8);

    //println!("m = {}", m);
    //println!("ll[{}] = {}", 1, ll);
    for i in 2..p {
        if i % 1000 < 1 { println!("Logging {}/{} - {:.2} %", i, p, ((i as f64) * 100.)/(p as f64)); }
        ll = &ll * &ll - &two;
        ll %= &m;
        //println!("ll[{}] = {}", i, ll);
    }

    return ll;
}

The measured times are - M = 2^p - 1, p - num-bigint - ramp
9686 - 5.30 - 0.73
9941 - 5.61 - 0.81
11213 - 8.72 - 1.05
19937 - 44.45 - 5.35
21701 - 56.19 - 6.69
23209 - 70.73 - 8.11
44 497 - 469.96 - 53.09
86243 - 3162.42 - 417.78
110503 - 6599.85 - didn't do
132049 - 11257.44 - didn't do

Pro tip: you can enable even more compiler optimizations with the following settings:

Cargo.toml
[profile.release]
codegen-units = 1
.cargo/config
[build]
rustflags = ["-C", "target-cpu=native"]

These together had the following timing improvements on my laptop with the ramp version:

input prime --release +extra optimizations
9686 0.47s 0.38s
9941 0.54s 0.41s
11213 0.82s 0.57s
19937 3.81s 2.79s
21701 4.86s 3.60s
44497 38.26s 28.37s
86243 267.55s 190.25s
110503 558.62s 409.74s
132049 909.69s 677.62s

About 25% faster.

You can simplify the code. Replace these lines:

    let mut m: Int = Int::from(1_u8);

    for _ in 0..p {
        m *= &two;
    }

    m = &m - Int::from(1_u8);

With this one liner:

    let m = two.pow(p as usize) - Int::from(1);

This also seems to increase the speed by another 1.5% or so. Bonus!

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.