Skip to content

Commit 7c170d5

Browse files
committed
Avoid empty direct-call upvalue allocations
1 parent e38b3bf commit 7c170d5

5 files changed

Lines changed: 150 additions & 1 deletion

File tree

crates/qjs-runtime/src/bytecode/ir.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,10 @@ pub struct Bytecode {
695695
local_slots: HashMap<String, usize>,
696696
parameter_slots: Vec<usize>,
697697
received_upvalue_slots: Vec<usize>,
698+
/// Whether direct-call frame setup may need per-local upvalue storage for
699+
/// an outer capture or sloppy global fallback. Module imports are tracked
700+
/// by `CallEnv` and checked separately at call time.
701+
has_direct_local_upvalue_routes: bool,
698702
global_names: Vec<String>,
699703
global_lexical_names: Vec<String>,
700704
sloppy_global_assignment_names: Vec<String>,
@@ -783,11 +787,15 @@ impl Bytecode {
783787
.enumerate()
784788
.filter_map(|(slot, local)| local.is_received_upvalue().then_some(slot))
785789
.collect();
790+
let has_direct_local_upvalue_routes = locals
791+
.iter()
792+
.any(|local| local.is_received_upvalue() || local.sloppy_global_fallback);
786793
let mut bytecode = Self {
787794
constants,
788795
local_slots: collect_local_slots(&locals),
789796
parameter_slots,
790797
received_upvalue_slots,
798+
has_direct_local_upvalue_routes,
791799
locals,
792800
global_names: collect_global_names(&code),
793801
global_lexical_names,
@@ -971,6 +979,10 @@ impl Bytecode {
971979
&self.received_upvalue_slots
972980
}
973981

982+
pub(super) fn has_direct_local_upvalue_routes(&self) -> bool {
983+
self.has_direct_local_upvalue_routes
984+
}
985+
974986
pub(crate) fn local_name_at(&self, slot: usize) -> Option<&str> {
975987
self.locals.get(slot).map(|local| local.name.as_str())
976988
}

crates/qjs-runtime/src/bytecode/vm.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,14 @@ impl<'a> Vm<'a> {
320320
upvalues: &[Upvalue],
321321
env: &CallEnv,
322322
) -> Vec<Option<Upvalue>> {
323+
// Most direct leaf calls have no captured, module, or sloppy-global
324+
// cells. An empty vector represents the all-None state for those
325+
// frames and avoids allocating one pointer-sized entry per local on
326+
// every call. Direct-call eligibility excludes operations that can
327+
// create cells later (closures, eval, and with).
328+
if !bytecode.has_direct_local_upvalue_routes() && !env.has_module_imports() {
329+
return Vec::new();
330+
}
323331
let mut next_received = 0;
324332
bytecode
325333
.locals
@@ -1505,3 +1513,80 @@ impl<'a> Vm<'a> {
15051513
references_name.then(|| name.to_owned())
15061514
}
15071515
}
1516+
1517+
#[cfg(test)]
1518+
mod tests {
1519+
use super::*;
1520+
use crate::bytecode::ir::Local;
1521+
1522+
fn local(name: &str, from_env: bool) -> Local {
1523+
Local {
1524+
name: name.to_owned(),
1525+
hoisted: false,
1526+
hoisted_function: false,
1527+
parameter: false,
1528+
catch_binding: false,
1529+
mutable: true,
1530+
from_env,
1531+
sloppy_global_fallback: false,
1532+
}
1533+
}
1534+
1535+
fn empty_env() -> CallEnv {
1536+
CallEnv::new(new_realm(HashMap::new()))
1537+
}
1538+
1539+
#[test]
1540+
fn direct_cell_free_frame_uses_empty_upvalue_storage() {
1541+
let bytecode = Bytecode::new(Vec::new(), vec![local("value", false)], Vec::new());
1542+
let env = empty_env();
1543+
1544+
let local_upvalues = Vm::initial_direct_local_upvalues(&bytecode, &[], &env);
1545+
1546+
assert!(local_upvalues.is_empty());
1547+
assert_eq!(
1548+
Vm::initial_authoritative_slots(&bytecode, &local_upvalues, &env),
1549+
1
1550+
);
1551+
}
1552+
1553+
#[test]
1554+
fn direct_captured_frame_keeps_received_upvalue_storage() {
1555+
let bytecode = Bytecode::new(Vec::new(), vec![local("captured", true)], Vec::new());
1556+
let env = empty_env();
1557+
let captured = Upvalue::new(Value::Number(42.0));
1558+
1559+
let local_upvalues =
1560+
Vm::initial_direct_local_upvalues(&bytecode, std::slice::from_ref(&captured), &env);
1561+
1562+
assert_eq!(local_upvalues.len(), 1);
1563+
assert!(
1564+
local_upvalues[0]
1565+
.as_ref()
1566+
.is_some_and(|upvalue| upvalue.ptr_eq(&captured))
1567+
);
1568+
}
1569+
1570+
#[test]
1571+
fn direct_module_frame_keeps_import_cell_storage() {
1572+
let bytecode = Bytecode::new(Vec::new(), vec![local("imported", false)], Vec::new());
1573+
let mut env = empty_env();
1574+
let exports = DynamicBindings::new();
1575+
exports.insert("exported".to_owned(), Value::Number(7.0));
1576+
env.set_module_import(
1577+
"imported".to_owned(),
1578+
exports.clone(),
1579+
"exported".to_owned(),
1580+
);
1581+
1582+
let local_upvalues = Vm::initial_direct_local_upvalues(&bytecode, &[], &env);
1583+
1584+
assert_eq!(local_upvalues.len(), 1);
1585+
assert!(
1586+
local_upvalues[0]
1587+
.as_ref()
1588+
.zip(exports.cell("exported").as_ref())
1589+
.is_some_and(|(local, exported)| local.ptr_eq(exported))
1590+
);
1591+
}
1592+
}

crates/qjs-runtime/src/bytecode/vm_bindings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ impl Vm<'_> {
386386
.filter_map(|(slot, local)| {
387387
(!bytecode.global_scope
388388
&& !local.sloppy_global_fallback
389-
&& local_upvalues.get(slot).is_some_and(Option::is_none)
389+
&& local_upvalues.get(slot).is_none_or(Option::is_none)
390390
&& env.slot_is_authoritative(&local.name))
391391
.then_some(1_u128 << slot)
392392
})

crates/qjs-runtime/src/function/env.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,10 @@ impl CallEnv {
605605
self.module_imports.clone()
606606
}
607607

608+
pub(crate) fn has_module_imports(&self) -> bool {
609+
!self.module_imports.is_empty()
610+
}
611+
608612
pub(crate) fn set_module_imports(&mut self, imports: ModuleImports) {
609613
self.module_imports = imports;
610614
}

tasks/T018-broad-performance.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,54 @@ and `8c9b2f0ab3c8f98ec6009f8a01ba6a93e4c2238220fa00ddff3ce9f5e9046264`.
20032003
These gains satisfy the no-overfitting regression boundary for this unit, but
20042004
the external <=1.00x target and broad allocation <=1.00x target remain open.
20052005

2006+
The thirty-seventh v2 unit follows a hotspot shared by the external
2007+
`access-binary-trees` port and the broad call/allocation profiles. Direct leaf
2008+
frames previously allocated a `Vec<Option<Upvalue>>` with one `None` entry per
2009+
local on every call even when the compiled function had no received capture,
2010+
sloppy-global route, or module import. Bytecode now caches whether either
2011+
local route exists, `CallEnv` exposes whether module-import routes exist, and a
2012+
cell-free direct frame represents the all-`None` upvalue table with an empty
2013+
vector. Authoritative-slot setup treats a missing entry as `None`. The existing
2014+
direct-call admission contract excludes closures, direct eval, and `with`, so
2015+
no operation can create a cell later on this path. Captured and module-import
2016+
unit tests prove that their identity-bearing cells still use full storage. The
2017+
implementation has no workload ID, source path, iteration count, or checksum
2018+
input.
2019+
2020+
Against clean base binary SHA-256
2021+
`0fd0b797227d3e82086d49a81817d6178990b174a1f70f44ad21a736f42cec41`,
2022+
candidate binary SHA-256
2023+
`af59c4e55c36f2dd739b2698b285f6bda059a32fa7bb00383c090632251f1957`
2024+
reduced an eleven-block interleaved external-shaped binary-tree median from
2025+
8.653387 s to 8.391495 s (0.969735x). The complete local three-role broad
2026+
diagnostic preserved all 25 cases and 225 eligible exact-checksum samples at
2027+
0.998932x candidate/base. Binding was 0.987920x, call 0.997499x, and allocation
2028+
1.013692x; the latter remains below the unit's 2% material family threshold.
2029+
The raw SHA-256 is
2030+
`fd022895f0a7bc9197bc352274bea0cc0d9a28d352bbed11ea903b7f4cdb8ec8`.
2031+
The strict analyzer rejected the intentionally receipt-less local binaries, so
2032+
these remain regression diagnostics rather than a performance claim.
2033+
2034+
Independent one-block full external A/B previews preserved identical coverage
2035+
at 5/5 JetStream, 8/14 Kraken, and 23/26 SunSpider. Across all 36 common
2036+
comparable cases, candidate/base was 0.998782x; five cases improved by more
2037+
than 2%, 27 stayed within 2%, four moved between 2.05% and 5.12%, and none
2038+
regressed by 25%. Suite candidate/base ratios were 0.994606x, 1.000293x, and
2039+
0.999167x. Candidate/QuickJS-NG diagnostics remain far from completion at
2040+
9.599757x, 5.766279x, and 8.740135x. An eleven-block interleaved rerun of the
2041+
external source confirmed `access-binary-trees` at 0.969378x; three apparent
2042+
regressions resolved to 0.988744x (`math-cordic`), 0.994895x (`crypto-md5`),
2043+
and 1.000494x (`string-fasta`), while `date-format-tofte` retained a small
2044+
1.021284x movement. Candidate raw/report SHA-256 are
2045+
`ddda93bdf1a6953061728ab3112cc2215867aa8d4c39e5858d098d5563c307e9`
2046+
and `864339bfe2f648024a63be730dce401a2a2812709a28f1be41ea24f63ffb8c9a`;
2047+
base raw/report SHA-256 are
2048+
`5f38ae170e09a785fefc6fa053712e4c5da64c7b13f28d3a0eac096fa1d53893`
2049+
and `c3c049ecbccff7e082320ad9c88d38506e30edfa7c7dba51c807fbfec7cf0335`.
2050+
These local results satisfy the unit's no-overfitting regression boundary but
2051+
do not satisfy B4 or B5; the exact-SHA hosted broad and external artifacts
2052+
remain authoritative.
2053+
20062054
## Historical Broad V1 Baseline
20072055

20082056
The first complete baseline was recorded on 2026-07-15 at commit

0 commit comments

Comments
 (0)