diff --git a/Cargo.toml b/Cargo.toml index a893d596..481b0cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ default = [] # if symbolica is used as a dynamic library (as is the case for the Python API) faster_alloc = ["tikv-jemallocator"] mathematica_api = ["wolfram-library-link"] -python_api = ["pyo3", "self_cell", "bincode"] +python_api = ["pyo3", "bincode"] # build a module that is independent of the specific Python version python_abi3 = ["pyo3/abi3", "pyo3/abi3-py37"] @@ -57,7 +57,7 @@ rand = "0.8.5" rand_xoshiro = "0.6" rayon = "1.8" rug = "1.23" -self_cell = {version = "1.0", optional = true} +self_cell = "1.0" serde = {version = "1.0", features = ["derive"]} smallvec = "1.13" smartstring = "1.0" diff --git a/examples/nested_evaluation.rs b/examples/nested_evaluation.rs index 972f1c0c..61c8296c 100644 --- a/examples/nested_evaluation.rs +++ b/examples/nested_evaluation.rs @@ -1,9 +1,9 @@ -use std::{process::Command, time::Instant}; +use std::time::Instant; use symbolica::{ atom::{Atom, AtomView}, domains::rational::Rational, - evaluate::{ExpressionEvaluator, FunctionMap}, + evaluate::{CompileOptions, ExpressionEvaluator, FunctionMap}, state::State, }; @@ -77,57 +77,38 @@ fn main() { println!("Op original {:?}", tree.count_operations()); tree.horner_scheme(); println!("Op horner {:?}", tree.count_operations()); - // the compiler seems to do this as well tree.common_subexpression_elimination(); - println!("op CSSE {:?}", tree.count_operations()); + println!("op cse {:?}", tree.count_operations()); tree.common_pair_elimination(); - println!("op CPE {:?}", tree.count_operations()); - - let cpp = tree.export_cpp(); - println!("{}", cpp); // print C++ code - - std::fs::write("nested_evaluation.cpp", cpp).unwrap(); - - let r = Command::new("g++") - .arg("-shared") - .arg("-fPIC") - .arg("-O3") - .arg("-ffast-math") - .arg("-o") - .arg("libneval.so") - .arg("nested_evaluation.cpp") - .output() + println!("op cpe {:?}", tree.count_operations()); + + let ce = tree + .export_cpp("nested_evaluation.cpp") + .unwrap() + .compile("libneval.so", CompileOptions::default()) + .unwrap() + .load() .unwrap(); - println!("Compilation {}", r.status); - - unsafe { - let lib = libloading::Library::new("./libneval.so").unwrap(); - let func: libloading::Symbol = - lib.get(b"eval_double").unwrap(); - let params = vec![5.]; - let mut out = vec![0., 0.]; - func(params.as_ptr(), out.as_mut_ptr()); - println!("Eval from C++: {}, {}", out[0], out[1]); - - // benchmark + let params = vec![5.]; + let mut out = vec![0., 0.]; + ce.evaluate(¶ms, &mut out); + println!("Eval from C++: {}, {}", out[0], out[1]); - let t = Instant::now(); - for _ in 0..1000000 { - let _ = func(params.as_ptr(), out.as_mut_ptr()); - } - println!("C++ time {:#?}", t.elapsed()); - }; + // benchmark + let t = Instant::now(); + for _ in 0..1000000 { + let _ = ce.evaluate(¶ms, &mut out); + } + println!("C++ time {:#?}", t.elapsed()); let t2 = tree.map_coeff::(&|r| r.into()); let mut evaluator: ExpressionEvaluator = t2.linearize(params.len()); - let mut out = vec![0., 0.]; - evaluator.evaluate_multiple(&[5.], &mut out); + evaluator.evaluate_multiple(¶ms, &mut out); println!("Eval: {}, {}", out[0], out[1]); - // benchmark let params = vec![5.]; let t = Instant::now(); for _ in 0..1000000 { diff --git a/src/evaluate.rs b/src/evaluate.rs index d1f908b9..0f723496 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -1,4 +1,5 @@ use ahash::HashMap; +use self_cell::self_cell; use crate::{ atom::{representation::InlineVar, Atom, AtomOrView, AtomView, Symbol}, @@ -1402,8 +1403,122 @@ impl EvalTree { } } +pub struct ExportedCode(String); +pub struct CompiledCode(String); + +impl CompiledCode { + /// Load the evaluator from the compiled shared library. + pub fn load(&self) -> Result { + CompiledEvaluator::load(&self.0) + } +} + +type L = libloading::Library; +type TR<'a> = libloading::Symbol<'a, unsafe extern "C" fn(params: *const f64, out: *mut f64)>; + +self_cell!( + pub struct CompiledEvaluator { + owner: L, + + #[covariant] + dependent: TR, + } + + impl {Debug} +); + +impl CompiledEvaluator { + /// Load a compiled evaluator from a shared library. + pub fn load(file: &str) -> Result { + unsafe { + let lib = match libloading::Library::new(file) { + Ok(lib) => lib, + Err(_) => { + libloading::Library::new("./".to_string() + file).map_err(|e| e.to_string())? + } + }; + + CompiledEvaluator::try_new(lib, |lib| { + lib.get(b"eval_double").map_err(|e| e.to_string()) + }) + } + } + + /// Evaluate the compiled evaluator. + #[inline(always)] + pub fn evaluate(&self, args: &[f64], out: &mut [f64]) { + unsafe { self.borrow_dependent()(args.as_ptr(), out.as_mut_ptr()) } + } +} + +/// Options for compiling exported code. +pub struct CompileOptions { + pub optimization_level: usize, + pub fast_math: bool, + pub unsafe_math: bool, + pub compiler: String, + pub custom: Vec, +} + +impl Default for CompileOptions { + fn default() -> Self { + CompileOptions { + optimization_level: 3, + fast_math: true, + unsafe_math: true, + compiler: "g++".to_string(), + custom: vec![], + } + } +} + +impl ExportedCode { + /// Compile the code to a shared library. + pub fn compile( + &self, + out: &str, + options: CompileOptions, + ) -> Result { + let mut builder = std::process::Command::new(&options.compiler); + builder + .arg("-shared") + .arg("-fPIC") + .arg(format!("-O{}", options.optimization_level)); + if options.fast_math { + builder.arg("-ffast-math"); + } + if options.unsafe_math { + builder.arg("-funsafe-math-optimizations"); + } + for c in &options.custom { + builder.arg(c); + } + + let r = builder.arg("-o").arg(out).arg(&self.0).output()?; + + if !r.status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Could not compile code: {}", + String::from_utf8_lossy(&r.stderr) + ), + )); + } + + Ok(CompiledCode(out.to_string())) + } +} + impl EvalTree { - pub fn export_cpp(&self) -> String { + /// Create a C++ code representation of the evaluation tree. + pub fn export_cpp(&self, filename: &str) -> Result { + let cpp = self.export_cpp_str(); + std::fs::write(filename, cpp)?; + Ok(ExportedCode(filename.to_string())) + } + + fn export_cpp_str(&self) -> String { let mut res = "#include \n#include \n\n".to_string(); for (name, arg_names, body) in &self.functions { @@ -1418,7 +1533,6 @@ impl EvalTree { name, args.join(",") ); - // our functions are all expressions so we return the expression for (i, s) in body.subexpressions.iter().enumerate() { res += &format!("\tT Z{}_ = {};\n", i, self.export_cpp_impl(s, arg_names));