Reputation:
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?
Upvotes: 11
Views: 3709
Reputation: 73470
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
Upvotes: 9
Reputation: 42678
You can use lazy_static
:
use lazy_static::lazy_static; // 1.4.0
lazy_static! {
static ref cbrt_33_mul_3: f64 = 3.0 * 33.0f64.powf(0.5);
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);
}
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();
}
fn main() {
println!("Hello, world!");
}
Note the usage of ref
and the dereference needed for the tribonacci_constant
.
Upvotes: 1