Skip to content

Commit c910678

Browse files
committed
perf(jit): expose singleton lists to MIR cons optimization
Lower List(0) to nil and List(1) to an explicit cons with nil cdr, so temporary singleton lists use existing escape analysis and precise reconstruction. Keep wider lists on the adapter and retain real objects when identity, mutation, callbacks or outgoing edges require them. Cover zero-allocation loops, heap-valued deopt frames and AOT sidecars. Core suite: 10,331 passed, 54 skipped; all 46 MIR tests pass under GC stress and partition verification. The singleton loop improves 4.67x against the preceding build; whole-workload and compilation controls show no material regression. Benchmark artifacts are in tmp.
1 parent 9896e64 commit c910678

4 files changed

Lines changed: 112 additions & 35 deletions

File tree

‎crates/neovm-core/src/emacs_core/runtime/jit/aot.rs‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,12 @@ pub fn is_d0_aot_candidate(
18761876
#[cfg(target_os = "linux")]
18771877
#[doc(hidden)]
18781878
pub fn testkit_mir_cons_reconstruction_selftest(dir: &std::path::Path) {
1879+
testkit_mir_cons_reconstruction_case(dir, false);
1880+
testkit_mir_cons_reconstruction_case(dir, true);
1881+
}
1882+
1883+
#[cfg(target_os = "linux")]
1884+
fn testkit_mir_cons_reconstruction_case(dir: &std::path::Path, singleton: bool) {
18791885
use crate::emacs_core::bytecode::{ByteCodeFunction, Vm};
18801886
use crate::emacs_core::eval::Context;
18811887
use crate::emacs_core::jit::compile::{DeoptResume, LoadedUnit, NativeRun};
@@ -1911,6 +1917,9 @@ pub fn testkit_mir_cons_reconstruction_selftest(dir: &std::path::Path) {
19111917
Op::Car,
19121918
Op::Return,
19131919
];
1920+
if singleton {
1921+
f.ops.splice(5..7, [Op::List(1)]);
1922+
}
19141923
f.seal_hand_assembled_ops();
19151924
let m = mir::build_mir(&f.ops, &f.constants, 2).unwrap();
19161925
assert_eq!(
@@ -1924,7 +1933,11 @@ pub fn testkit_mir_cons_reconstruction_selftest(dir: &std::path::Path) {
19241933
let (obj, hash) = compile_leaf_to_object(&f.ops, &f.constants, 2, None)
19251934
.unwrap()
19261935
.unwrap();
1927-
let path = dir.join("cons-reconstruction.so");
1936+
let path = dir.join(if singleton {
1937+
"singleton-reconstruction.so"
1938+
} else {
1939+
"cons-reconstruction.so"
1940+
});
19281941
link_object_to_so(&obj, &path).unwrap();
19291942
// SAFETY: this is the object just emitted by our compiler; its runtime
19301943
// imports are the same exported shims used by the JIT.
@@ -1939,10 +1952,13 @@ pub fn testkit_mir_cons_reconstruction_selftest(dir: &std::path::Path) {
19391952
let NativeRun::DeoptAt(resume) = result else {
19401953
panic!("precise AOT deopt expected: {result:?}")
19411954
};
1942-
assert_eq!(resume.pc, 9);
1955+
assert_eq!(resume.pc, if singleton { 8 } else { 9 });
19431956
assert_eq!(resume.stack[2], resume.stack[3]);
19441957
assert_eq!(resume.stack[2].cons_car(), Value::make_int(8));
1945-
assert_eq!(resume.stack[2].cons_cdr(), float);
1958+
assert_eq!(
1959+
resume.stack[2].cons_cdr(),
1960+
if singleton { Value::NIL } else { float }
1961+
);
19461962
let DeoptResume {
19471963
pc,
19481964
stack,

‎crates/neovm-core/src/emacs_core/runtime/jit/compile/lowering.rs‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3172,14 +3172,11 @@ pub(crate) fn build_mir_leaf_fn<M: Module>(
31723172
// leave cval[r]=None — every use is a CarCdr that forwards to the
31733173
// operands.
31743174
MirOp::Cons(..) if cons_repl[r].is_some() => {}
3175-
// An ESCAPING cons is heap-allocated via the neovm_jit_cons shim —
3176-
// a GC SAFEPOINT, but NOT an observable side effect (a fresh
3177-
// unshared object), so it needs NO precise deopt: rerun-from-start
3178-
// (pure body) re-allocates a fresh cons the caller never saw, and a
3179-
// call-bearing body spills the allocated cons (a real Value) into
3180-
// its precise framestate normally. Force-tag car+cdr (no raw fixnum
3181-
// into the heap pair / across the safepoint; the shim self-roots
3182-
// them) + gc-root the live-across-allocation residual, like a call.
3175+
// An escaping cons uses the non-collecting neovm_jit_cons shim.
3176+
// A pure body may rerun from the start: a fresh unshared object
3177+
// is not observable yet. A precise body spills the real Value
3178+
// normally. Tag both fields before storing them in the heap;
3179+
// no root publication is needed across this allocation.
31833180
MirOp::Cons(car, cdr) => {
31843181
let rt = rt
31853182
.as_ref()

‎crates/neovm-core/src/emacs_core/runtime/jit/mir.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ fn lower_value_op(
606606
let r = emit(b, MirOp::Const(v), LispType::of_value(v), Effect::PURE);
607607
stack.push(r);
608608
}
609-
Op::Nil => {
609+
Op::Nil | Op::List(0) => {
610610
let r = emit(b, MirOp::Const(Value::NIL), LispType::Nil, Effect::PURE);
611611
stack.push(r);
612612
}
@@ -726,6 +726,16 @@ fn lower_value_op(
726726
let r = emit(b, MirOp::Cons(car, cdr), LispType::Cons, Effect::ALLOCATES);
727727
stack.push(r);
728728
}
729+
Op::List(1) => {
730+
// The byte compiler also uses list1 for (cons x nil). Expose the
731+
// pair to ordinary cons escape analysis and cold reconstruction.
732+
// Both allocation shims are non-collecting; neither can run Lisp.
733+
// Wider lists remain opaque until nested virtual objects qualify.
734+
let car = pop!();
735+
let cdr = emit(b, MirOp::Const(Value::NIL), LispType::Nil, Effect::PURE);
736+
let r = emit(b, MirOp::Cons(car, cdr), LispType::Cons, Effect::ALLOCATES);
737+
stack.push(r);
738+
}
729739
// Pure operand-stack shuffles — modelled as stack manipulation, no inst.
730740
Op::Pop => {
731741
pop!();

‎crates/neovm-core/src/emacs_core/runtime/jit/tests/mir_cons_deopt.rs‎

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -107,30 +107,40 @@ fn mir_cons_precise_frames_preserve_aliases_and_completed_effects() {
107107

108108
#[test]
109109
fn mir_cons_block_local_loop_allocation_count() {
110+
assert_block_local_loop_allocation_count(false);
111+
}
112+
113+
#[test]
114+
fn mir_singleton_block_local_loop_allocation_count() {
115+
assert_block_local_loop_allocation_count(true);
116+
}
117+
118+
fn assert_block_local_loop_allocation_count(singleton: bool) {
110119
let mut ev = Context::new();
111-
let f = function(
112-
vec![
113-
Op::Constant(0),
114-
Op::StackRef(1),
115-
Op::Constant(0),
116-
Op::Gtr,
117-
Op::GotoIfNil(16),
118-
Op::StackRef(0),
119-
Op::StackRef(2),
120-
Op::Nil,
121-
Op::Cons,
122-
Op::Car,
123-
Op::Add,
124-
Op::StackSet(1),
125-
Op::StackRef(1),
126-
Op::Sub1,
127-
Op::StackSet(2),
128-
Op::Goto(1),
129-
Op::Return,
130-
],
131-
vec![Value::make_int(0)],
132-
1,
133-
);
120+
let mut ops = vec![
121+
Op::Constant(0),
122+
Op::StackRef(1),
123+
Op::Constant(0),
124+
Op::Gtr,
125+
Op::GotoIfNil(16),
126+
Op::StackRef(0),
127+
Op::StackRef(2),
128+
Op::Nil,
129+
Op::Cons,
130+
Op::Car,
131+
Op::Add,
132+
Op::StackSet(1),
133+
Op::StackRef(1),
134+
Op::Sub1,
135+
Op::StackSet(2),
136+
Op::Goto(1),
137+
Op::Return,
138+
];
139+
if singleton {
140+
ops.splice(7..9, [Op::List(1)]);
141+
ops[4] = Op::GotoIfNil(15);
142+
}
143+
let f = function(ops, vec![Value::make_int(0)], 1);
134144
let leaf = compile_bytecode_function_with(&f, Some(&ev.obarray)).unwrap();
135145
assert_eq!(leaf.tier, leaf::LeafTier::Mir);
136146
// Fewer than 255 back edges avoids a service poll, so the allocation
@@ -170,6 +180,11 @@ fn mir_singleton_precise_frames_preserve_aliases_and_completed_effects() {
170180
2,
171181
);
172182
let m = mir::build_mir(&f.ops, &f.constants, 2).unwrap();
183+
assert_eq!(
184+
plan_mir_leaf(&m).cons_repl.iter().flatten().count(),
185+
2,
186+
"both singleton lists are reconstructed only on a cold exit"
187+
);
173188
let leaf = lower_mir_pure(&m).unwrap();
174189
let float = ev.eval_str("1.5").unwrap();
175190
let result = leaf.call(
@@ -253,10 +268,49 @@ fn mir_empty_list_preserves_the_residual_stack() {
253268
(vec![Op::List(0), Op::Pop, Op::Return], element),
254269
] {
255270
let m = mir::build_mir(&ops, &[], 1).unwrap();
271+
assert!(!plan_mir_leaf(&m).needs_rt);
256272
let leaf = lower_mir_pure(&m).unwrap();
257273
assert_eq!(
258274
leaf.call(&mut ev as *mut Context as *mut u8, &[element]),
259275
NativeRun::Ok(expected.bits())
260276
);
261277
}
262278
}
279+
280+
#[test]
281+
fn mir_singleton_precise_frame_preserves_a_heap_element() {
282+
let mut ev = Context::new();
283+
ev.eval_str("(setq mir-list-heap-v 0)").unwrap();
284+
let f = function(
285+
vec![
286+
Op::Constant(0),
287+
Op::VarSet(1),
288+
Op::StackRef(0),
289+
Op::List(1),
290+
Op::StackRef(1),
291+
Op::Add1,
292+
Op::Pop,
293+
Op::Car,
294+
Op::Return,
295+
],
296+
vec![Value::make_int(7), Value::symbol("mir-list-heap-v")],
297+
1,
298+
);
299+
let m = mir::build_mir(&f.ops, &f.constants, 1).unwrap();
300+
assert_eq!(plan_mir_leaf(&m).cons_repl.iter().flatten().count(), 1);
301+
let leaf = lower_mir_pure(&m).unwrap();
302+
let float = ev.eval_str("1.5").unwrap();
303+
let result = leaf.call(&mut ev as *mut Context as *mut u8, &[float]);
304+
let NativeRun::DeoptAt(ref resume) = result else {
305+
panic!("expected precise float deopt: {result:?}")
306+
};
307+
assert_eq!(resume.pc, 5);
308+
assert_eq!(resume.stack.len(), 3);
309+
assert_eq!(resume.stack[1].cons_car().bits(), float.bits());
310+
assert!(resume.stack[1].cons_cdr().is_nil());
311+
assert_eq!(
312+
mir_inline_guards::resume(&mut ev, &f, result).bits(),
313+
float.bits()
314+
);
315+
assert_eq!(ev.jit_root_stack_top, 0);
316+
}

0 commit comments

Comments
 (0)