Symjit is a lightweight just-in-time (JIT) optimizer compiler for mathematical expressions written in Rust. It was originally designed to compile SymPy (Python’s symbolic algebra package) expressions into machine code and to serve as a bridge between SymPy and numerical routines provided by NumPy and SciPy libraries.
Symjit emits AMD64 (x86-64), ARM64 (aarch64), and 64-bit RISC-V (riscv64) machine codes on Linux, Windows, and macOS platforms. SIMD is supported on x86-64 CPUs with AVX instruction sets.
Symbolica (https://symbolica.io/) is a fast Rust-based Computer Algebra System. Symbolica usually generate fast code using external compilers (e.g., using gcc to compile synthetic c++ code). Symjit accepts Symbolica expressions and can act as an optional code-generator for Symbolica.
Symjit-bridge crate acts as a bridge between Symbolica and Symjit to ease generating JIT code for Symbolica expressions.
The main workflow is using different Runners. A runner corresponds to a Symbolica
CompiledEvaluator object. The main runners are:
CompiledRealRunner, corresponding toCompiledRealEvaluator.CompiledComplexRunner, corresponding toCompiledComplexEvaluator.InterpretedRealRunner, bytecode interpreter, generally similar toExpressionEvaluator.InterpretedComplexRunner, bytecode interpreter, generally similar toExpressionEvaluator.
Each runner has four main methods:
compile(ev: &ExpressionEvaluator<T>, config: Config): the main constructor.Tis eitherf64orComplex<f64>, andconfigis an object of typeConfig. For most applications, the default config suffices. However,Config.use_threads(bool)is useful to enable multi-threading.compile_with_funcs(ev: &ExpressionEvaluator<T>, config: Config, df: &Defuns): Same ascompilebut with the additional of external functions defined in aDefunsstructure.evaluate(args, outs): similar to the corresponding method of theEvaluators.save(filename).load(filename).
Both CompiledRealRunner and CompiledComplexRunner may use SIMD instructions if it is available
and the number of input rows is equal or more than the number of SIMD lanes (4 in AVX, 2 in aarch64).
use anyhow::Result;
use symjit_bridge::{compile, Config};
use symbolica::{
atom::AtomCore,
evaluate::{FunctionMap, OptimizationSettings},
parse, symbol,
};
fn test_real_runner() -> Result<()> {
let params = vec![parse!("x"), parse!("y")];
let f = FunctionMap::new();
let ev = parse!("x + y^3")
.evaluator(&f, ¶ms, OptimizationSettings::default())
.unwrap()
.map_coeff(&|x| x.re.to_f64());
let mut runner = CompiledRealRunner::compile(&ev, Config::default())?;
let mut outs: [f64; 1] = [0.0];
runner.evaluate(&[3.0, 5.0], &mut outs);
assert_eq!(outs[0], 128.0);
Ok(())
}Symjit has a rich set of transcendental, conditional, and logical functions (refer to
Symjit for details). It is possible to expose
these functions to Symbolica by using add_external_function:
fn test_external() -> Result<()> {
let params = vec![parse!("x"), parse!("y")];
let mut f = FunctionMap::new();
f.add_external_function(symbol!("sinh"), "sinh".to_string())
.unwrap();
let ev = parse!("sinh(x+y)")
.evaluator(&f, ¶ms, OptimizationSettings::default())
.unwrap()
.map_coeff(&|x| Complex::new(x.re.to_f64(), x.im.to_f64()));
let mut runner = CompiledComplexRunner::compile(&ev, Config::default())?;
let args = [Complex::new(1.0, 2.0), Complex::new(2.0, -1.0)];
let mut outs = [Complex::<f64>::default(); 1];
runner.evaluate(&args, &mut outs);
assert_eq!(outs[0], Complex::new(3.0, 1.0).sinh());
Ok(())
}