Search code examples
ruststaticinitialization

How to initialize immutable globals with non-const initializer in Rust?


I am trying to get a variable that is only initialized once, at runtime. In C/C++ static would be the keyword that I would be looking for, yet in Rust, it must be initialized by a constant.

static mut is unsafe, and I can see why, but it doesn't conceptually capture what I want, I want an immutable variable.

Take this trivial example of a tribonacci function:

static sqrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);

static tribonacci_constant: f64 = 1.0
+ (19.0 - sqrt_33_mul_3).powf(1.0 / 3.0)
+ (19.0 + sqrt_33_mul_3).powf(1.0 / 3.0);

fn tribonacci(n: f64) -> f64 {
    return (
        (tribonacci_constant / 3.0).powf(n)
        / (
            (4.0 / 3.0)
            * tribonacci_constant
            - (1.0 / 9.0)
            * tribonacci_constant.powf(2.0) - 1.0
        )
    ).round();
}

I want the two static variables outside of the function to be initialized only once, and powf to not be called with every run of the function

I am incredibly new to Rust and do not know what may be common knowledge to the average, experienced user.

Is this possible, if so, how can it be done?


Solution

  • If f64::powf was a const function then the compiler should convert things like 3.0 * 33.0f64.powf(0.5) down to a single fixed value.

    While lazy_static can be used to solve this problem, there is a cost behind using lazy_statics, because they're designed to support more than just simple floating-point constants.

    You can see this cost by benchmarking the two implementations using Criterion:

    pub mod ls {
        use lazy_static::lazy_static; // 1.4.0
    
        lazy_static! {
            //TODO: Should this be a pow(1.0/3.0)?
            pub static ref cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
            
            pub static ref tribonacci_constant: f64 = 1.0
            + (19.0 - *cbrt_33_mul_3).powf(1.0 / 3.0)
            + (19.0 + *cbrt_33_mul_3).powf(1.0 / 3.0);
        }
    
        pub fn tribonacci(n: f64) -> f64 {
            return (
                (*tribonacci_constant / 3.0).powf(n)
                / (
                    (4.0 / 3.0)
                    * *tribonacci_constant
                    - (1.0 / 9.0)
                    * tribonacci_constant.powf(2.0) - 1.0
                )
            ).round();
        }
    }
    
    pub mod hc {
        pub fn tribonacci(n: f64) -> f64 {
            let p = 1.839286755214161;
            let s = 0.3362281169949411;
            return (s * p.powf(n)).round();
        }
    }
    
    fn criterion_benchmark(c: &mut Criterion) {
        c.bench_function("trib 5.1 ls", |b| b.iter(|| ls::tribonacci(black_box(5.1))));
        c.bench_function("trib 5.1 hc", |b| b.iter(|| hc::tribonacci(black_box(5.1))));
    }
    
    criterion_group!(benches, criterion_benchmark);
    criterion_main!(benches);
    

    The cost is small, but may be significant if this is in your core loops. On my machine, I get (after removing unrelated lines)

    trib 5.1 ls             time:   [47.946 ns 48.832 ns 49.796 ns]                         
    trib 5.1 hc             time:   [38.828 ns 39.898 ns 41.266 ns]                         
    

    This is about a 20% difference.

    If you don't like having hardcoded constants in your code, you can actually generate these at build time using a build.rs script.

    My complete example for benchmarking looks like this:

    build.rs

    use std::env;
    use std::fs;
    use std::path::Path;
    
    fn main() {
        let out_dir = env::var_os("OUT_DIR").unwrap();
        let dest_path = Path::new(&out_dir).join("constants.rs");
    
        //TODO: Should this be a pow(1.0/3.0)?
        let cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
        
        let tribonacci_constant: f64 = 1.0 
            + (19.0 - cbrt_33_mul_3).powf(1.0 / 3.0)
            + (19.0 + cbrt_33_mul_3).powf(1.0 / 3.0);
    
        let p = tribonacci_constant / 3.0;
        let s = 1.0 / (
            (4.0 / 3.0)
            * tribonacci_constant
            - (1.0 / 9.0)
            * tribonacci_constant.powf(2.0) - 1.0
        );
    
        fs::write(
            &dest_path,
            format!("\
            pub mod tribonacci {{\n\
                pub const P: f64 = {:.32};\n\
                pub const S: f64 = {:.32};\n\
            }}\n", p, s)
        ).unwrap();
        println!("cargo:rerun-if-changed=build.rs");
    }
    

    src/lib.rs

    pub mod constants {
        include!(concat!(env!("OUT_DIR"), "/constants.rs"));
    }
    
    pub mod ls {
        use lazy_static::lazy_static; // 1.4.0
    
        lazy_static! {
            //TODO: Should this be a pow(1.0/3.0)?
            pub static ref cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
            
            pub static ref tribonacci_constant: f64 = 1.0
            + (19.0 - *cbrt_33_mul_3).powf(1.0 / 3.0)
            + (19.0 + *cbrt_33_mul_3).powf(1.0 / 3.0);
        }
    
        pub fn tribonacci(n: f64) -> f64 {
            return (
                (*tribonacci_constant / 3.0).powf(n)
                / (
                    (4.0 / 3.0)
                    * *tribonacci_constant
                    - (1.0 / 9.0)
                    * tribonacci_constant.powf(2.0) - 1.0
                )
            ).round();
        }
    
    }
    
    pub mod hc {
        pub fn tribonacci(n: f64) -> f64 {
            let p = super::constants::tribonacci::P;
            let s = super::constants::tribonacci::S;
            return (s * p.powf(n)).round();
        }
    }
    

    benches/my_benchmark.rs

    use criterion::{black_box, criterion_group, criterion_main, Criterion};
    use rust_gen_const_vs_lazy_static::ls;
    use rust_gen_const_vs_lazy_static::hc;
    
    fn criterion_benchmark(c: &mut Criterion) {
        c.bench_function("trib 5.1 ls", |b| b.iter(|| ls::tribonacci(black_box(5.1))));
        c.bench_function("trib 5.1 hc", |b| b.iter(|| hc::tribonacci(black_box(5.1))));
    }
    
    criterion_group!(benches, criterion_benchmark);
    criterion_main!(benches);
    

    Cargo.toml

    [package]
    name = "rust_gen_const_vs_lazy_static"
    version = "0.1.0"
    edition = "2018"
    
    [dependencies]
    "lazy_static" = "1.4.0"
    
    [dev-dependencies]
    criterion = "0.3"
    
    [[bench]]
    name = "my_benchmark"
    harness = false
    

    $OUTDIR/constants.rs (generated)

    pub mod tribonacci {
    pub const P: f64 = 1.83928675521416096216853475198150;
    pub const S: f64 = 0.33622811699494109527464047459944;
    }
    

    As suggested by Dilshod Tadjibaev it is possible to achieve a similar result using proc-macros, though it requires a little more work in this case. This gives exactly the same speed as build-time generation.

    To set this up I created a new crate for the macros trib_macros, as proc-macros need to be in their own crate. This new crate contained just two files Cargo.toml and src/lib.rs

    Cargo.toml

    [package]
    name = "trib_macros"
    version = "0.1.0"
    edition = "2018"
    
    
    [lib]
    proc-macro = true
    

    src/lib.rs

    extern crate proc_macro;
    use proc_macro::TokenStream;
    
    #[proc_macro]
    pub fn tp(_item: TokenStream) -> TokenStream {
        let cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
        
        let tribonacci_constant: f64 = 1.0 
            + (19.0 - cbrt_33_mul_3).powf(1.0 / 3.0)
            + (19.0 + cbrt_33_mul_3).powf(1.0 / 3.0);
    
        let p = tribonacci_constant / 3.0;
        format!("{}f64",p).parse().unwrap()
    }
    
    #[proc_macro]
    pub fn ts(_item: TokenStream) -> TokenStream {
        let cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
        
        let tribonacci_constant: f64 = 1.0 
            + (19.0 - cbrt_33_mul_3).powf(1.0 / 3.0)
            + (19.0 + cbrt_33_mul_3).powf(1.0 / 3.0);
    
        let s = 1.0 / (
            (4.0 / 3.0)
            * tribonacci_constant
            - (1.0 / 9.0)
            * tribonacci_constant.powf(2.0) - 1.0
        );
        format!("{}f64",s).parse().unwrap()
    }
    

    Then we need to adjust the Cargo.toml of the original crate to pull this in.

    [dependencies]
    ...
    trib_macros = { path = "path/to/trib_macros" }
    

    And finally using it is relatively clean:

    pub mod mc {
        use trib_macros::{ts,tp};
    
        pub fn tribonacci(n: f64) -> f64 {
            return (ts!() * tp!().powf(n)).round();
        }
    }
    

    There's definitely a neater way to output the float literal tokens, but I couldn't find it.


    You can find a complete repository for these tests at https://github.com/mikeando/rust_code_gen_example