Skip to content

Duration::from_secs_f32 gives wrong results #90225

Closed

Description

@manio

Hello.
I am diagnosing why I've got a wrong duration computed from Duration::from_secs_f32()...

I tried this code:

let val1 = Duration::from_secs_f32(30.0);
println!("{}", format_duration(val1).to_string());

I expected to see an exact 30-second duration.
Instead, I've got this:
"30s 1us 24ns"
which is: 30 seconds and 1024 nanos.

I dig deeper into rust code and I've prepared a sample code which is computing it based on the rust/time.rs source code:

    let secs: f32 = 30.0;
    const NANOS_PER_SEC: u32 = 1_000_000_000;
    let nanos = secs * (NANOS_PER_SEC as f32);
    let nanos = nanos as u128;
    let s = (nanos / (NANOS_PER_SEC as u128)) as u64;
    let nano = (nanos % (NANOS_PER_SEC as u128)) as u32;
    let secs = Duration::new(s, nano);
    println!("sec={}, nanos={}", s, nano);

As the result I've got:
sec=30, nanos=1024

Instead of exact 30 seconds and 0 nanos.

Activity

  1. klensy commented on Oct 24, 2021

    @klensy
    Contributor

    rustc -vV ?

  2. manio commented on Oct 24, 2021

    @manio
    Author
    rustc -vV
    rustc 1.50.0-nightly (f76ecd066 2020-12-15)
    binary: rustc
    commit-hash: f76ecd0668fcdb289456cdc72a39ad15467cc454
    commit-date: 2020-12-15
    host: x86_64-unknown-linux-gnu
    release: 1.50.0-nightly
    
  3. klensy commented on Oct 24, 2021

    @klensy
    Contributor

    Works on current stable and nightly: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=5c01f41664bf7f4c81861033f80b6578

    I mean, that this issue exist for that versions.

  4. manio commented on Oct 24, 2021

    @manio
    Author

    @klensy
    yep, I also confirmed on:

    rustc 1.58.0-nightly (91b931926 2021-10-23)
    binary: rustc
    commit-hash: 91b931926fd49fc97d1e39f2b8206abf1d77ce7d
    commit-date: 2021-10-23
    host: x86_64-unknown-linux-gnu
    release: 1.58.0-nightly
    LLVM version: 13.0.0
    
  5. manio commented on Oct 24, 2021

    @manio
    Author

    What is interesting the function: from_secs_f64 doesn't seem to have this problem...
    so a quick workaround would be use it and cast from f32 to f64

  6. klensy commented on Oct 24, 2021

    @klensy
    Contributor

    Extending(?) f32 to u128 gives this addition in 1024:

    let nanos = nanos as u128;

  7. added a commit that references this issue on Oct 24, 2021
    bad2c6d
  8. manio commented on Oct 24, 2021

    @manio
    Author

    CC: @newpavlov
    CC: @mbartlett21
    Adding authors ... guys please take a quick look if you have a time...

  9. nagisa commented on Oct 24, 2021

    @nagisa
    Member

    f32 will naturally have trouble representing all possible nanosecond values and so doing calculations in f64 may help here somewhat. However the most appropriate approach would be to decompose the float into the whole part and the fractional part and then operate on the two separately to produce the values necessary to create the Duration.

    f*::fract and f*::trunc may work sufficiently well for the purpose.

    EDIT: I think we would still need to be careful about how the f32 nanosecond fraction is handled. We cannot simply multiply the number by 1_000_000_000 and then convert to an integer, because f32 is not capable of representing integers of such degree losslessly.

  10. newpavlov commented on Oct 25, 2021

    @newpavlov
    Contributor

    We cannot simply multiply the number by 1_000_000_000 and then convert to an integer, because f32 is not capable of representing integers of such degree losslessly.

    Small correction: 1_000_000_000 is represented exactly by f32, the problem is with 30_000_000_000. The closest float is 30_000_001_024, which results in the 1024 ns error. For example, Duration::from_secs_f32(32.0); would have produced an exact value.

    I would say such loss of precision is expected for from_secs_f32 and if you want exact values, you should use the integer based methods. So I don't think this issue should be marked as a bug.

    It could be worthwhile to improve the code by processing the fractional part separately, but note that it will not solve this issue fundamentally. We still have the issue of mapping at most 23 fractional bits into ~30 bits, e.g. calling Duration::from_secs_f32(30.3); would still result in an inexact result.

  11. nagisa commented on Oct 25, 2021

    @nagisa
    Member

    1_000_000_000 specifically may be representable exactly, but f32 cannot represent all integers 0..1_000_000_000 exactly. Though maybe its not a huge deal if after trunc we end up roundtripping back to the actual integer in all cases.

  12. newpavlov commented on Oct 25, 2021

    @newpavlov
    Contributor

    Argh, I forgot that the trunc method is not available on core, so the linked PR will not work out of box.

    UPD: I have implemented manual fraction/exponent splitting and their conversion to integers.

  13. added a commit that references this issue on Jan 26, 2022
    faf2b7f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions