Skip to content

Commit

Permalink
Add compilation and library loading framework
Browse files Browse the repository at this point in the history
  • Loading branch information
benruijl committed Jul 4, 2024
1 parent 57ce732 commit 4d0ce5b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 45 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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"
Expand Down
63 changes: 22 additions & 41 deletions examples/nested_evaluation.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down Expand Up @@ -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<unsafe extern "C" fn(params: *const f64, out: *mut f64)> =
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(&params, &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(&params, &mut out);
}
println!("C++ time {:#?}", t.elapsed());

let t2 = tree.map_coeff::<f64, _>(&|r| r.into());
let mut evaluator: ExpressionEvaluator<f64> = t2.linearize(params.len());

let mut out = vec![0., 0.];
evaluator.evaluate_multiple(&[5.], &mut out);
evaluator.evaluate_multiple(&params, &mut out);
println!("Eval: {}, {}", out[0], out[1]);

// benchmark
let params = vec![5.];
let t = Instant::now();
for _ in 0..1000000 {
Expand Down
118 changes: 116 additions & 2 deletions src/evaluate.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ahash::HashMap;
use self_cell::self_cell;

use crate::{
atom::{representation::InlineVar, Atom, AtomOrView, AtomView, Symbol},
Expand Down Expand Up @@ -1402,8 +1403,122 @@ impl<T: Real> EvalTree<T> {
}
}

pub struct ExportedCode(String);
pub struct CompiledCode(String);

impl CompiledCode {
/// Load the evaluator from the compiled shared library.
pub fn load(&self) -> Result<CompiledEvaluator, String> {
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<CompiledEvaluator, String> {
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<String>,
}

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<CompiledCode, std::io::Error> {
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<T: NumericalFloatLike> EvalTree<T> {
pub fn export_cpp(&self) -> String {
/// Create a C++ code representation of the evaluation tree.
pub fn export_cpp(&self, filename: &str) -> Result<ExportedCode, std::io::Error> {
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 <iostream>\n#include <cmath>\n\n".to_string();

for (name, arg_names, body) in &self.functions {
Expand All @@ -1418,7 +1533,6 @@ impl<T: NumericalFloatLike> EvalTree<T> {
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));
Expand Down

0 comments on commit 4d0ce5b

Please sign in to comment.