use std::ffi::{CStr, CString};
use std::ptr;
use inkwell::context::Context;
use inkwell::memory_buffer::MemoryBuffer;
use inkwell::module::Module;
use inkwell::targets::{InitializationConfig, Target};
use llvm_sys::error::{
LLVMConsumeError, LLVMDisposeErrorMessage, LLVMErrorRef, LLVMGetErrorMessage,
};
use llvm_sys::orc2::lljit::{
LLVMOrcCreateLLJIT, LLVMOrcDisposeLLJIT, LLVMOrcLLJITAddLLVMIRModule,
LLVMOrcLLJITAddObjectFile, LLVMOrcLLJITGetMainJITDylib, LLVMOrcLLJITLookup,
LLVMOrcLLJITMangleAndIntern, LLVMOrcLLJITRef,
};
use llvm_sys::orc2::{
LLVMJITEvaluatedSymbol, LLVMJITSymbolFlags, LLVMJITSymbolGenericFlags, LLVMOrcAbsoluteSymbols,
LLVMOrcCSymbolMapPair, LLVMOrcCreateNewThreadSafeContextFromLLVMContext,
LLVMOrcCreateNewThreadSafeModule, LLVMOrcDisposeMaterializationUnit,
LLVMOrcDisposeThreadSafeContext, LLVMOrcExecutorAddress, LLVMOrcJITDylibDefine,
LLVMOrcJITDylibRef, LLVMOrcThreadSafeContextRef,
};
use llvm_sys::prelude::LLVMContextRef;
use crate::error::LoweringError;
struct ThreadSafeContext(LLVMOrcThreadSafeContextRef);
impl ThreadSafeContext {
fn from_raw_owned(ctx: LLVMContextRef) -> Self {
let raw = unsafe { LLVMOrcCreateNewThreadSafeContextFromLLVMContext(ctx) };
ThreadSafeContext(raw)
}
fn as_raw(&self) -> LLVMOrcThreadSafeContextRef {
self.0
}
}
impl Drop for ThreadSafeContext {
fn drop(&mut self) {
unsafe { LLVMOrcDisposeThreadSafeContext(self.0) };
}
}
pub struct JitBackend {
jit: LLVMOrcLLJITRef,
main_jd: LLVMOrcJITDylibRef,
}
unsafe impl Send for JitBackend {}
unsafe impl Sync for JitBackend {}
impl JitBackend {
pub fn new() -> Result<Self, LoweringError> {
Target::initialize_native(&InitializationConfig::default()).map_err(|e| LoweringError {
message: format!("failed to initialize native target: {e}"),
at: "orc::JitBackend::new".to_string(),
})?;
let mut jit: LLVMOrcLLJITRef = ptr::null_mut();
let builder = jit_builder_from_env()?;
let err = unsafe { LLVMOrcCreateLLJIT(&mut jit, builder) };
check_error(err, "LLVMOrcCreateLLJIT")?;
if jit.is_null() {
return Err(LoweringError {
message: "LLVMOrcCreateLLJIT reported success but returned a null LLJIT"
.to_string(),
at: "orc::JitBackend::new".to_string(),
});
}
let main_jd = unsafe { LLVMOrcLLJITGetMainJITDylib(jit) };
Ok(JitBackend { jit, main_jd })
}
pub fn add_module_from_bitcode(&self, bitcode: &[u8]) -> Result<(), LoweringError> {
self.add_module_from_bitcode_inner(bitcode)
}
pub fn add_module_split(&self, bitcode: &[u8]) -> Result<(), LoweringError> {
use std::collections::HashSet;
let threads = compile_threads()?;
if threads < 2 || bitcode.len() < SPLIT_MIN_BITCODE_BYTES {
return self.add_module_from_bitcode(bitcode);
}
let at = "orc::JitBackend::add_module_split";
let err = |message: String| LoweringError {
message,
at: at.to_string(),
};
fn parse<'c>(bitcode: &[u8], ctx: &'c Context) -> Result<Module<'c>, LoweringError> {
let buf = MemoryBuffer::create_from_memory_range_copy(bitcode, "jit_split");
Module::parse_bitcode_from_buffer(&buf, ctx).map_err(|e| LoweringError {
message: format!("failed to parse bitcode: {e}"),
at: "orc::JitBackend::add_module_split".to_string(),
})
}
let t_plan = crate::timing::start();
let planned = {
let ctx = discarding_names_context();
parse(bitcode, &ctx)
.ok()
.and_then(|module| crate::jit_split::plan(&module, threads))
};
release_freed_memory();
crate::timing::end(
t_plan,
"split plan (parse + call graph)",
&format!(
"{threads} threads, {:?} bins",
planned.as_ref().map(|p| p.bins.len())
),
);
let Some(plan) = planned else {
return self.add_module_from_bitcode(bitcode);
};
let gate = (
std::sync::Mutex::new(parse_permits(bitcode.len(), threads)),
std::sync::Condvar::new(),
);
let plan = &plan;
let gate = &gate;
let objects: Vec<Result<Option<Vec<u8>>, LoweringError>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..plan.bins.len())
.map(|bin| {
std::thread::Builder::new()
.stack_size(JIT_THREAD_STACK_BYTES)
.spawn_scoped(s, move || {
let ctx = discarding_names_context();
let owned: HashSet<String> = plan.bins[bin].iter().cloned().collect();
let module = {
let (permits, cv) = gate;
let mut n = permits.lock().unwrap_or_else(|e| e.into_inner());
while *n == 0 {
n = cv.wait(n).unwrap_or_else(|e| e.into_inner());
}
*n -= 1;
drop(n);
let t = crate::timing::start();
let module = parse(bitcode, &ctx);
crate::timing::end(t, &format!("bin {bin}: parse bitcode"), "");
if let Ok(module) = &module {
let t = crate::timing::start();
crate::jit_split::strip_to_bin(
module,
bin,
&owned,
&plan.shared_globals,
);
crate::timing::end(
t,
&format!("bin {bin}: strip"),
&format!("{} owned functions", owned.len()),
);
}
release_freed_memory();
*permits.lock().unwrap_or_else(|e| e.into_inner()) += 1;
cv.notify_one();
module?
};
let t = crate::timing::start();
let obj = module_to_object(&module);
crate::timing::end(
t,
&format!("bin {bin}: codegen"),
&format!("{} bytes", obj.as_ref().map_or(0, Vec::len)),
);
drop(module);
release_freed_memory();
obj.map(Some)
})
.map_err(|e| err(format!("failed to spawn a JIT compile thread: {e}")))
})
.collect();
handles
.into_iter()
.map(|h| {
h?.join()
.unwrap_or_else(|_| Err(err("a JIT compile thread panicked".to_string())))
})
.collect()
});
for object in objects {
if let Some(object) = object? {
self.add_object(&object)?;
}
}
Ok(())
}
fn add_object(&self, object: &[u8]) -> Result<(), LoweringError> {
let name = c"jit_split_object";
let buf = unsafe {
llvm_sys::core::LLVMCreateMemoryBufferWithMemoryRangeCopy(
object.as_ptr().cast(),
object.len(),
name.as_ptr(),
)
};
let err = unsafe { LLVMOrcLLJITAddObjectFile(self.jit, self.main_jd, buf) };
check_error(err, "LLVMOrcLLJITAddObjectFile")
}
fn add_module_from_bitcode_inner(&self, bitcode: &[u8]) -> Result<(), LoweringError> {
let ctx = discarding_names_context();
let buf =
MemoryBuffer::create_from_memory_range_copy(bitcode, "jit_add_module_from_bitcode");
let module = Module::parse_bitcode_from_buffer(&buf, &ctx).map_err(|e| LoweringError {
message: format!("failed to parse bitcode into a fresh context: {e}"),
at: "orc::JitBackend::add_module_from_bitcode".to_string(),
})?;
let raw_module = module.as_mut_ptr();
let raw_ctx = ctx.raw();
std::mem::forget(module);
std::mem::forget(ctx);
let tsctx = ThreadSafeContext::from_raw_owned(raw_ctx);
let tsm = unsafe { LLVMOrcCreateNewThreadSafeModule(raw_module, tsctx.as_raw()) };
if tsm.is_null() {
return Err(LoweringError {
message: "LLVMOrcCreateNewThreadSafeModule returned null".to_string(),
at: "orc::JitBackend::add_module_from_bitcode".to_string(),
});
}
let err = unsafe { LLVMOrcLLJITAddLLVMIRModule(self.jit, self.main_jd, tsm) };
check_error(err, "LLVMOrcLLJITAddLLVMIRModule")
}
pub fn define_absolute_symbol(&self, mangled: &str, addr: usize) -> Result<(), LoweringError> {
let name = CString::new(mangled).map_err(|e| LoweringError {
message: format!("symbol name {mangled:?} contains an interior NUL: {e}"),
at: "orc::JitBackend::define_absolute_symbol".to_string(),
})?;
let interned = unsafe { LLVMOrcLLJITMangleAndIntern(self.jit, name.as_ptr()) };
if interned.is_null() {
return Err(LoweringError {
message: format!("LLVMOrcLLJITMangleAndIntern returned null for {mangled:?}"),
at: "orc::JitBackend::define_absolute_symbol".to_string(),
});
}
let flags = LLVMJITSymbolFlags {
GenericFlags: LLVMJITSymbolGenericFlags::LLVMJITSymbolGenericFlagsExported as u8,
TargetFlags: 0,
};
let mut pair = LLVMOrcCSymbolMapPair {
Name: interned,
Sym: LLVMJITEvaluatedSymbol {
Address: addr as LLVMOrcExecutorAddress,
Flags: flags,
},
};
let mu = unsafe { LLVMOrcAbsoluteSymbols(&mut pair, 1) };
if mu.is_null() {
return Err(LoweringError {
message: format!("LLVMOrcAbsoluteSymbols returned null for {mangled:?}"),
at: "orc::JitBackend::define_absolute_symbol".to_string(),
});
}
let err = unsafe { LLVMOrcJITDylibDefine(self.main_jd, mu) };
if !err.is_null() {
unsafe { LLVMOrcDisposeMaterializationUnit(mu) };
return check_error(err, "LLVMOrcJITDylibDefine");
}
Ok(())
}
pub unsafe fn get_fn<F: Copy>(&self, mangled: &str) -> Option<F> {
let name = CString::new(mangled).ok()?;
let mut addr: LLVMOrcExecutorAddress = 0;
let t_lookup = crate::timing::start();
let err = unsafe { LLVMOrcLLJITLookup(self.jit, &mut addr, name.as_ptr()) };
crate::timing::end_if_over(t_lookup, 0.5, "JIT lookup (lazy codegen)", mangled);
if !err.is_null() {
unsafe { LLVMConsumeError(err) };
return None;
}
if addr == 0 {
return None;
}
Some(unsafe { std::mem::transmute_copy::<usize, F>(&(addr as usize)) })
}
}
impl Drop for JitBackend {
fn drop(&mut self) {
let err = unsafe { LLVMOrcDisposeLLJIT(self.jit) };
if !err.is_null() {
unsafe { LLVMConsumeError(err) };
}
}
}
fn check_error(err: LLVMErrorRef, context: &str) -> Result<(), LoweringError> {
if err.is_null() {
return Ok(());
}
let msg_ptr = unsafe { LLVMGetErrorMessage(err) };
if msg_ptr.is_null() {
return Err(LoweringError {
message: format!("{context} failed with no error message"),
at: "orc::JitBackend".to_string(),
});
}
let message = unsafe { CStr::from_ptr(msg_ptr) }
.to_string_lossy()
.into_owned();
unsafe { LLVMDisposeErrorMessage(msg_ptr) };
Err(LoweringError {
message: format!("{context}: {message}"),
at: "orc::JitBackend".to_string(),
})
}
pub const JIT_THREADS_ENV: &str = "SV_SIM_JIT_THREADS";
const MAX_COMPILE_THREADS: usize = 8;
const SPLIT_MIN_BITCODE_BYTES: usize = 200_000;
pub fn release_freed_memory() {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
{
unsafe extern "C" {
fn malloc_trim(pad: usize) -> i32;
}
unsafe {
malloc_trim(0);
}
}
}
pub const JIT_PARSES_ENV: &str = "SV_SIM_JIT_PARSES";
const PARSE_BYTES_PER_BITCODE_BYTE: u64 = 10;
fn memory_budget() -> Option<u64> {
let kb = |text: &str, key: &str| -> Option<u64> {
text.lines()
.find_map(|l| l.strip_prefix(key))
.and_then(|r| r.split_whitespace().next())
.and_then(|n| n.parse::<u64>().ok())
};
let avail = std::fs::read_to_string("/proc/meminfo")
.ok()
.and_then(|t| kb(&t, "MemAvailable:"))
.map(|k| k * 1024);
let cg = std::fs::read_to_string("/proc/self/cgroup")
.ok()
.and_then(|t| {
t.lines()
.find_map(|l| l.strip_prefix("0::").map(str::to_owned))
})
.and_then(|path| {
let dir = format!("/sys/fs/cgroup{}", path.trim());
let max = std::fs::read_to_string(format!("{dir}/memory.max")).ok()?;
let max = max.trim().parse::<u64>().ok()?;
let cur = std::fs::read_to_string(format!("{dir}/memory.current"))
.ok()?
.trim()
.parse::<u64>()
.ok()?;
Some(max.saturating_sub(cur))
});
match (avail, cg) {
(Some(a), Some(c)) => Some(a.min(c)),
(a, c) => a.or(c),
}
}
fn parse_permits(bitcode_len: usize, threads: usize) -> usize {
if let Some(n) = std::env::var(JIT_PARSES_ENV)
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n >= 1)
{
return n.min(threads);
}
let Some(budget) = memory_budget() else {
return 1;
};
permits_for_budget(budget, bitcode_len, threads)
}
fn permits_for_budget(budget: u64, bitcode_len: usize, threads: usize) -> usize {
let per = (bitcode_len as u64)
.saturating_mul(PARSE_BYTES_PER_BITCODE_BYTE)
.max(1);
((budget / 2 / per) as usize).clamp(1, threads)
}
const JIT_THREAD_STACK_BYTES: usize = 64 << 20;
fn compile_threads() -> Result<usize, LoweringError> {
match std::env::var_os(JIT_THREADS_ENV) {
None => Ok(std::thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.min(MAX_COMPILE_THREADS)),
Some(v) => match v.to_str().and_then(|s| s.parse::<usize>().ok()) {
Some(n) if n >= 1 => Ok(n),
_ => Err(LoweringError {
message: format!("{JIT_THREADS_ENV}={v:?}: expected a thread count of at least 1"),
at: "orc::compile_threads".to_string(),
}),
},
}
}
pub const JIT_OPT_ENV: &str = "SV_SIM_JIT_OPT";
fn opt_level_from_env() -> Result<Option<inkwell::OptimizationLevel>, LoweringError> {
use inkwell::OptimizationLevel;
let Some(v) = std::env::var_os(JIT_OPT_ENV) else {
return Ok(None);
};
match v.to_str() {
Some("0") => Ok(Some(OptimizationLevel::None)),
Some("1") => Ok(Some(OptimizationLevel::Less)),
Some("2") => Ok(Some(OptimizationLevel::Default)),
Some("3") => Ok(Some(OptimizationLevel::Aggressive)),
_ => Err(LoweringError {
message: format!(
"{JIT_OPT_ENV}={v:?}: expected 0, 1, 2 or 3 (JIT code-generation optimization level)"
),
at: "orc::jit_builder_from_env".to_string(),
}),
}
}
fn module_to_object(module: &Module<'_>) -> Result<Vec<u8>, LoweringError> {
use inkwell::OptimizationLevel;
use inkwell::targets::{CodeModel, FileType, RelocMode, TargetMachine};
let err = |message: String| LoweringError {
message,
at: "orc::module_to_object".to_string(),
};
let level = opt_level_from_env()?.unwrap_or(OptimizationLevel::Default);
let triple = TargetMachine::get_default_triple();
let target = Target::from_triple(&triple)
.map_err(|e| err(format!("host triple {:?}: {e}", triple.as_str())))?;
let cpu = TargetMachine::get_host_cpu_name();
let features = TargetMachine::get_host_cpu_features();
let tm = target
.create_target_machine(
&triple,
cpu.to_str().unwrap_or(""),
features.to_str().unwrap_or(""),
level,
RelocMode::PIC,
CodeModel::Small,
)
.ok_or_else(|| err(format!("no target machine for {:?}", triple.as_str())))?;
module.set_triple(&triple);
module.set_data_layout(&tm.get_target_data().get_data_layout());
let buf = tm
.write_to_memory_buffer(module, FileType::Object)
.map_err(|e| err(format!("object emission failed: {e}")))?;
Ok(buf.as_slice().to_vec())
}
fn jit_builder_from_env() -> Result<llvm_sys::orc2::lljit::LLVMOrcLLJITBuilderRef, LoweringError> {
use inkwell::targets::{CodeModel, RelocMode, TargetMachine};
let err = |message: String| LoweringError {
message,
at: "orc::jit_builder_from_env".to_string(),
};
let Some(level) = opt_level_from_env()? else {
return Ok(ptr::null_mut());
};
let triple = TargetMachine::get_default_triple();
let target = Target::from_triple(&triple)
.map_err(|e| err(format!("host triple {:?}: {e}", triple.as_str())))?;
let cpu = TargetMachine::get_host_cpu_name();
let features = TargetMachine::get_host_cpu_features();
let tm = target
.create_target_machine(
&triple,
cpu.to_str().unwrap_or(""),
features.to_str().unwrap_or(""),
level,
RelocMode::Default,
CodeModel::JITDefault,
)
.ok_or_else(|| {
err(format!(
"no target machine for host triple {:?}",
triple.as_str()
))
})?;
unsafe {
let jtmb =
llvm_sys::orc2::LLVMOrcJITTargetMachineBuilderCreateFromTargetMachine(tm.as_mut_ptr());
std::mem::forget(tm);
let b = llvm_sys::orc2::lljit::LLVMOrcCreateLLJITBuilder();
llvm_sys::orc2::lljit::LLVMOrcLLJITBuilderSetJITTargetMachineBuilder(b, jtmb);
Ok(b)
}
}
fn discarding_names_context() -> Context {
let ctx = Context::create();
unsafe { llvm_sys::core::LLVMContextSetDiscardValueNames(ctx.raw(), 1) };
ctx
}
#[cfg(test)]
mod permit_tests {
use super::*;
#[test]
fn parse_permits_clamps() {
assert_eq!(parse_permits(usize::MAX / 64, 8), 1);
assert!(parse_permits(1, 8) <= 8);
assert!(parse_permits(1, 8) >= 1);
}
#[test]
fn parse_permits_uvm_under_a_30g_cap() {
const GB: u64 = 1 << 30;
let uvm = 1_058_271_860;
assert_eq!(permits_for_budget(27 * GB, uvm, 8), 1);
assert_eq!(permits_for_budget(57 * GB, uvm, 8), 2);
assert_eq!(permits_for_budget(139 * GB, uvm, 8), 7);
}
}