rustrounding-error

Rust: Strange state-based rounding behaviour on f32


When computing the dot-product of two nalgebra::Vector3 structs using specific values, I get the following behaviour (link to playground):

use nalgebra::{Point3, Vector3}; // 0.31.0

fn dot(v1: Vector3<f32>, v2: Vector3<f32>) -> f32 {
    v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
    
}

fn main() {
    println!("Run 1:");
    let u = Vector3::new(1000., -1000., 0.);
    let v = Vector3::new(-0.69294637441651, 0.720989108085632, 0.);
    println!(
        "self-written dot-product: \t{:.32}",
        dot(u, v)
    );
    println!(
        "nalgebra dot-product: \t\t{:.32}",
        u.dot(&v)
    );
    println!("\nRun2:");
    let u = Vector3::new(1000., -1000., 0.);
    let v = Vector3::new(-0.69294637441651, 0.720989108085632, 0.);
    println!(
        "nalgebra dot-product: \t\t{:.32}",
        u.dot(&v)
    );
}

Output:

Run 1:
self-written dot-product:   -1413.93554687500000000000000000000000
nalgebra dot-product:       -1413.93554687500000000000000000000000
Run2:
nalgebra dot-product:       -1413.93548250214189465623348951339722

I must be able to rely on the computation to always be the same. Any thoughts?

Related to my previous question, which I closed due to non-working examples previous question


Solution

  • As @aedm has mentioned in the comment, your dot() function is the cause for this behavior. As a beginner rustacean it wasn't quite obvious to me how it is exactly a cause, so I put an explanation here.

    When you define variables for the first time,

     9| println!("Run 1:");
    10| let u = Vector3::new(1000., -1000., 0.);
    11| let v = Vector3::new(-0.69294637441651, 0.720989108085632, 0.);
    

    Rust compiler doesn't know exact type of the values, it only understands that it's float. And if there would be no extra information, the compiler would fall for f64 as a default float type in Rust.

    When you call dot(u, v) - you're letting the compiler know the exact types because you specified them on function declaration:

     3| fn dot(v1: Vector3<f32>, v2: Vector3<f32>) -> f32 {
    

    The compiler is now certain that the values of u and v are of a type f32.

    Then you're using .dot() method, which can handle both f32 and f64. The type of u and v is already defined as f32, and the type of variables cannot be changed, but cause .dot() can handle f32, it makes the compiler happy. At this point you get:

    Run 1:
    -1413.93554687500000000000000000000000
    -1413.93554687500000000000000000000000
    

    After that you're defining new variables with the same names - again the compiler has no explicit information about the type of the variables. But this time there's no dot(u, v) call, only .dot() and the latter one doesn't require f32, so the compiler goes for the default f64. In the end you get:

    Run2:
    -1413.93548250214189465623348951339722