Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions crates/qjs-cli/hot-functions.order
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# budget 0xa00 __RNvMs6_NtNtCs9nYd1Hk1rek_11qjs_runtime5value6objectNtB5_9ObjectRef32write_existing_own_data_property
# budget 0x300 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute16boxed_truthiness
# budget 0x300 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute14boxed_equality
# budget 0x500 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
# pinned: typed-loop executor at 0x200, interpreter's at 0xc40 mod 4 KiB (python3 -m tools.benchmark.layout_pin), head 49
# budget 0xe60 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
# budget 0x1800 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10compact_fn12numeric_plan3run
# pinned: typed-loop executor at 0x200, interpreter's at 0xc40 mod 4 KiB (python3 -m tools.benchmark.layout_pin), head 54
__RNvMsz_NtNtNtCs1OjIl8oxbrv_5alloc11collections5btree3mapINtB5_8IntoIteryNtNtNtCscUtGwbhD4WH_5gimli4read6abbrev12AbbreviationE10dying_nextCsg55jX0GwzBC_3std
__RNvXs7_NtNtCsg55jX0GwzBC_3std2io5errorNtB5_5ErrorNtNtCsl8K0bEFm1U0_4core3fmt7Display3fmt
__RINvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute3runNtNtB6_2vm2VmEB8_
Expand Down Expand Up @@ -55,10 +56,15 @@ __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute16boxed_truth
__RNvNtNtCsg55jX0GwzBC_3std3sys2fs8read_dir
__RNvMsn_NtCs1OjIl8oxbrv_5alloc4syncINtB5_3ArcNtNtNtCsg55jX0GwzBC_3std6thread6thread5InnerNtNtBM_5alloc6SystemE9drop_slowBM_
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute14boxed_equality
__RNvMsn_NtCs1OjIl8oxbrv_5alloc4syncINtB5_3ArcNtNtNtNtCsg55jX0GwzBC_3std3sys2fs4unix12InnerReadDirE9drop_slowBO_
__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterNtNtNtNtB9_3sys5stdio4unix6StderrENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
__RNvMs8_NtCsg55jX0GwzBC_3std4pathNtB5_10Components25parse_next_component_back
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
__RNvNtNtCsg55jX0GwzBC_3std3sys2fs12canonicalize
__RNvNvNtCsg55jX0GwzBC_3std2fs14read_to_string5inner
__RNvMNtCs2IzQ63mrjeP_9addr2line5frameINtB2_9FrameIterINtNtNtCscUtGwbhD4WH_5gimli4read12endian_slice11EndianSliceNtNtBV_9endianity12LittleEndianEE4nextCsg55jX0GwzBC_3std
__RNvNtNtCsg55jX0GwzBC_3std2io5stdio31print_to_buffer_if_capture_used
__RNvMsz_NtNtNtCs1OjIl8oxbrv_5alloc11collections5btree3mapINtB5_8IntoIteryINtNtCsl8K0bEFm1U0_4core6result6ResultINtNtBb_4sync3ArcNtNtNtCscUtGwbhD4WH_5gimli4read6abbrev13AbbreviationsENtB25_5ErrorEE10dying_nextCsg55jX0GwzBC_3std
__RNvNtNtNtNtCsg55jX0GwzBC_3std3sys2io5error4unix17decode_error_kind
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10compact_fn12numeric_plan3run
__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterNtNtNtNtB9_3sys5stdio4unix6StderrENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
__RNvMs4_NtCs1OjIl8oxbrv_5alloc7raw_vecINtB5_11RawVecInnerNtNtCsg55jX0GwzBC_3std5alloc6SystemE14grow_amortizedBW_
__RNvXs1i_NtCsl8K0bEFm1U0_4core3fmtRNtNtNtB8_5panic8location8LocationNtB6_7Display3fmtCsg55jX0GwzBC_3std
__RNvXs1_NtCs9nYd1Hk1rek_11qjs_runtime5valueNtB5_5ValueNtNtCsl8K0bEFm1U0_4core5clone5Clone5clone
Expand Down
35 changes: 35 additions & 0 deletions crates/qjs-runtime/src/bytecode/compact_fn/wide/activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ impl WideActivation<'_> {
{
return Ok(crate::string::js_string_eq(left, right) == (op == BinaryOp::Eq));
}
// The rest that runs no user code -- `node.left == null` -- without
// cloning the operands into the general path.
if let Some(Value::Boolean(result)) =
crate::operations::eval_binary_without_env(left, op, right)
{
return Ok(result);
}
if matches!(op, BinaryOp::Eq | BinaryOp::Ne)
&& let Some(equal) = loose_equality_without_conversion(left, right)
{
return Ok(equal == (op == BinaryOp::Eq));
}
let value = self.eval_binary(left.clone(), op, right.clone())?;
Ok(crate::is_truthy(&value))
}
Expand Down Expand Up @@ -1406,6 +1418,22 @@ fn run_frames(
}
}

/// `left == right` where no conversion can run: `null` or `undefined`
/// against anything -- nothing else equals them but the IsHTMLDDA host
/// object -- and two numbers. Kept out of the shared operator helpers, whose
/// growth re-rolled this tier's dispatch codegen (crypto-md5 +2.5% cycles at
/// identical instructions).
#[inline(never)]
fn loose_equality_without_conversion(left: &Value, right: &Value) -> Option<bool> {
match (left, right) {
(Value::Null | Value::Undefined, other) | (other, Value::Null | Value::Undefined) => Some(
matches!(other, Value::Null | Value::Undefined) || crate::html_dda::is_html_dda(other),
),
(Value::Number(left), Value::Number(right)) => Some(left == right),
_ => None,
}
}

/// A unary operator, with the interpreter's own fast paths: `!` and `void`
/// coerce nothing, a number answers inline, everything else runs the general
/// operator in an empty realm frame like the tier's binary path.
Expand Down Expand Up @@ -1499,6 +1527,13 @@ fn construct_from_activation(
if let Some(array) = construct_plain_array(env, &callee, arguments) {
return Ok(array);
}
if let Value::Function(function) = &callee
&& function.native.is_some()
&& let Some(result) =
crate::function::construct_intrinsic_directly(function, arguments, env)
{
return result;
}
let mut env = env.empty_frame();
crate::function::construct_function(callee.clone(), callee, arguments.to_vec(), &mut env)
}
Expand Down
4 changes: 3 additions & 1 deletion crates/qjs-runtime/src/bytecode/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ pub(super) fn compile_direct_eval_script(
direct_eval_source: true,
..Compiler::default()
};
compiler.compile_eval_into(script)
let mut bytecode = compiler.compile_eval_into(script)?;
bytecode.mark_direct_eval_code();
Ok(bytecode)
}

/// Compiles a module body. Module code is always strict mode, regardless of a
Expand Down
26 changes: 26 additions & 0 deletions crates/qjs-runtime/src/bytecode/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,8 @@ pub struct Bytecode {
/// name-keyed binding. Deciding this per local used to ask the environment
/// about every name on every call.
cached_authoritative_mask_clean: u128,
/// The top level of a direct eval's code (not a function inside it).
direct_eval_code: bool,
}

impl Bytecode {
Expand Down Expand Up @@ -940,6 +942,7 @@ impl Bytecode {
cached_uses_lexical_this: false,
cached_hoisted_slots: Vec::new(),
cached_authoritative_mask_clean: 0,
direct_eval_code: false,
};
// Order matters: closure/arguments metadata reads the simpler caches
// (written-binding names, creates-closures) computed just above. Nested
Expand Down Expand Up @@ -1421,6 +1424,29 @@ impl Bytecode {
self.cached_contains_direct_eval
}

pub(super) fn mark_direct_eval_code(&mut self) {
self.direct_eval_code = true;
}

/// Whether this is the top level of a direct eval's code.
pub(super) fn is_direct_eval_code(&self) -> bool {
self.direct_eval_code
}

/// Every name the body resolves through its environment at run time
/// rather than through a slot: its free-name reads, writes and `typeof`s,
/// and its `delete`s by name. Nested function bodies' names are included
/// only through `global_names`.
pub(super) fn names_resolved_by_name(&self) -> impl Iterator<Item = &str> {
self.global_names
.iter()
.map(String::as_str)
.chain(self.code.iter().filter_map(|op| match op {
Op::DeleteIdent(name) | Op::DeleteIdentWith { name, .. } => Some(name.as_str()),
_ => None,
}))
}

pub(crate) fn contains_with(&self) -> bool {
self.cached_contains_with
}
Expand Down
1 change: 1 addition & 0 deletions crates/qjs-runtime/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub(crate) use vm_numeric_leaf::try_eval_numeric_leaf;
pub(crate) use vm_private::apply_instance_private_element;
pub(crate) use vm_result::FunctionBytecodeResult;
pub(crate) use vm_set::set_property as set_object_property;
pub(crate) use vm_string_append::concat_primitives;
pub(crate) use vm_this_property_leaf::try_eval_this_property_leaf;

pub(crate) fn delete_object_property(
Expand Down
18 changes: 18 additions & 0 deletions crates/qjs-runtime/src/bytecode/typed_loop/branchy_nested_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,21 @@ fn an_inherited_accessor_is_not_answered_from_the_slot_cache() {
run(o, 5) + ':' + calls;";
assert_eq!(eval(source), Ok(Value::String("10:5".into())));
}

/// A relational comparison against a string literal in a typed loop
/// compares strings by code unit (and numbers as numbers) instead of
/// unboxing; a mixed or object operand leaves the loop exactly, `valueOf`
/// included. Expected values from V8.
#[test]
fn typed_loops_compare_against_string_literals() {
let source = r#"function scan(s) { var bad = 0, le = 0; for (var i = 0; i < s.length; i++) { var ch = s.charAt(i); if (ch < "0" || ch > "9") bad++; if (ch <= "5") le++; if (ch >= "x") le += 10; } return bad + ":" + le; }
function mixed(a) { var n = 0; for (var i = 0; i < a.length; i++) { if (a[i] < "5") n++; if (a[i] > 3) n += 100; } return n; }
var calls = 0; var o = { valueOf: function () { calls++; return 4; } };
function objs(a) { var n = 0; for (var i = 0; i < a.length; i++) { if (a[i] < "5") n++; } return n; }
var out = [scan("1234xyz"), scan("12éz9"), scan(""), mixed(["1", "9", 2, 7, "10", undefined, null]), objs(["1", o, "7", o]), calls];
out.join(',');"#;
assert_eq!(
eval(source),
Ok(Value::String("3:34,2:22,0:0,304,3,2".to_owned().into()))
);
}
104 changes: 25 additions & 79 deletions crates/qjs-runtime/src/bytecode/typed_loop/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ struct Builder<'a> {

mod admission;
mod element;
mod globals;

pub(super) use admission::{admitted_binary, admitted_unary};
use admission::{expression_has_control_flow, scalar_expression_may_write_or_branch};
Expand Down Expand Up @@ -661,6 +662,19 @@ impl<'a> Builder<'a> {
///
/// Two scalars stay on the scalar comparison: widening them would trade a
/// register compare for two `Box` operations and a value comparison.
fn top_operands_include_string_constant(&self) -> bool {
self.stack
.iter()
.rev()
.take(2)
.any(|&(register, class, _)| {
class == Class::Boxed
&& self.boxed_constants.iter().any(|(candidate, value)| {
*candidate == register && matches!(value, Value::String(_))
})
})
}

fn top_operands_include_boxed(&self) -> bool {
self.stack
.iter()
Expand Down Expand Up @@ -1022,83 +1036,6 @@ impl<'a> Builder<'a> {
Some(())
}

/// Boxed register seeded from the global binding `name`, allocating one if
/// new.
fn global_register(&mut self, name: &str) -> Option<u16> {
if let Some((register, _)) = self
.boxed_global_reads
.iter()
.find(|(_, candidate)| candidate == name)
{
return Some(*register);
}
let register = self.fresh_boxed()?;
self.boxed_global_reads.push((register, name.to_owned()));
Some(register)
}

/// Whether the region contains an instruction that could write `name`,
/// which would make a hoisted read of that same binding observably stale.
fn region_writes_global_named(&self, name: &str) -> bool {
self.bytecode.code[self.header..=self.backedge]
.iter()
.any(|op| match op {
Op::StoreGlobalStrict(candidate) | Op::DefineGlobalVar(candidate) => {
candidate == name
}
Op::StoreGlobalSloppy {
name: candidate, ..
}
| Op::StoreLocalOrGlobalSloppy {
name: candidate, ..
}
| Op::AppendStringLiteralGlobal {
name: candidate, ..
} => candidate == name,
_ => false,
})
}

/// A named-property write could target `globalThis` and mutate a hoisted
/// global binding. A region with a sloppy fallback sink therefore keeps
/// those writes out of this tier; dense array writes remain separately
/// guarded and cannot alter a global object's binding descriptors.
fn region_writes_a_sloppy_global(&self) -> bool {
self.bytecode.code[self.header..=self.backedge]
.iter()
.any(|op| matches!(op, Op::StoreLocalOrGlobalSloppy { .. }))
}

/// Returns the frame slot for an unresolved sloppy binding only when this
/// bytecode owns the compiler-emitted fallback slot for the same name.
fn sloppy_global_fallback_slot(&self, name: &str) -> Option<usize> {
let slot = self.bytecode.local_slot(name)?;
let local = self.bytecode.locals.get(slot)?;
(local.name == name && local.sloppy_global_fallback && !local.compiler_temporary)
.then_some(slot)
}

/// Index of the prepared sink for a sloppy fallback write, adding it once
/// per slot/name pair. The runtime validates the dynamic binding and
/// property identities before entering the program.
fn sloppy_global_write_index(&mut self, slot: usize, name: &str) -> Option<u16> {
if self.sloppy_global_fallback_slot(name) != Some(slot) {
return None;
}
let slot = u32::try_from(slot).ok()?;
if let Some(index) =
self.sloppy_global_writes
.iter()
.position(|(candidate_slot, candidate_name)| {
*candidate_slot == slot && candidate_name == name
})
{
return u16::try_from(index).ok();
}
self.sloppy_global_writes.push((slot, name.to_owned()));
u16::try_from(self.sloppy_global_writes.len() - 1).ok()
}

/// Diagnostic builds name the instruction a pass gave up on:
/// `QJS_TL_TRACE=1` on a `perf-counters` binary prints the region, the
/// site, the operation and what this pass discovered. Paired with the
Expand Down Expand Up @@ -1407,11 +1344,20 @@ impl<'a> Builder<'a> {
// deoptimized on its first iteration instead. This arm must come
// first, and must require a genuinely boxed operand, so ordinary
// numeric comparisons keep the scalar file.
// Relational operators likewise when one side is a string
// literal: `ch < "0"` over a `charAt` result compares strings,
// which unboxing deoptimized. Only then -- a boxed `i <
// list.length` must keep the pass that reads the length as a
// scalar.
Op::Binary(binary)
if matches!(
if (matches!(
*binary,
BinaryOp::Eq | BinaryOp::Ne | BinaryOp::StrictEq | BinaryOp::StrictNe
) && self.top_operands_include_boxed() =>
) && self.top_operands_include_boxed())
|| (matches!(
*binary,
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge
) && self.top_operands_include_string_constant()) =>
{
let (right, _) = self.pop_boxed()?;
let (left, _) = self.pop_boxed()?;
Expand Down
83 changes: 83 additions & 0 deletions crates/qjs-runtime/src/bytecode/typed_loop/compile/globals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! The region's global bindings: hoisted reads, and the sloppy fallback
//! globals it writes.

use super::{Builder, Op};

impl Builder<'_> {
/// Boxed register seeded from the global binding `name`, allocating one if
/// new.
pub(super) fn global_register(&mut self, name: &str) -> Option<u16> {
if let Some((register, _)) = self
.boxed_global_reads
.iter()
.find(|(_, candidate)| candidate == name)
{
return Some(*register);
}
let register = self.fresh_boxed()?;
self.boxed_global_reads.push((register, name.to_owned()));
Some(register)
}

/// Whether the region contains an instruction that could write `name`,
/// which would make a hoisted read of that same binding observably stale.
pub(super) fn region_writes_global_named(&self, name: &str) -> bool {
self.bytecode.code[self.header..=self.backedge]
.iter()
.any(|op| match op {
Op::StoreGlobalStrict(candidate) | Op::DefineGlobalVar(candidate) => {
candidate == name
}
Op::StoreGlobalSloppy {
name: candidate, ..
}
| Op::StoreLocalOrGlobalSloppy {
name: candidate, ..
}
| Op::AppendStringLiteralGlobal {
name: candidate, ..
} => candidate == name,
_ => false,
})
}

/// A named-property write could target `globalThis` and mutate a hoisted
/// global binding. A region with a sloppy fallback sink therefore keeps
/// those writes out of this tier; dense array writes remain separately
/// guarded and cannot alter a global object's binding descriptors.
pub(super) fn region_writes_a_sloppy_global(&self) -> bool {
self.bytecode.code[self.header..=self.backedge]
.iter()
.any(|op| matches!(op, Op::StoreLocalOrGlobalSloppy { .. }))
}

/// Returns the frame slot for an unresolved sloppy binding only when this
/// bytecode owns the compiler-emitted fallback slot for the same name.
pub(super) fn sloppy_global_fallback_slot(&self, name: &str) -> Option<usize> {
let slot = self.bytecode.local_slot(name)?;
let local = self.bytecode.locals.get(slot)?;
(local.name == name && local.sloppy_global_fallback && !local.compiler_temporary)
.then_some(slot)
}

/// Index of the prepared sink for a sloppy fallback write, adding it once
/// per slot/name pair. The runtime validates the dynamic binding and
/// property identities before entering the program.
pub(super) fn sloppy_global_write_index(&mut self, slot: usize, name: &str) -> Option<u16> {
if self.sloppy_global_fallback_slot(name) != Some(slot) {
return None;
}
let slot = u32::try_from(slot).ok()?;
if let Some(index) =
self.sloppy_global_writes
.iter()
.position(|(candidate_slot, candidate_name)| {
*candidate_slot == slot && candidate_name == name
})
{
return u16::try_from(index).ok();
}
self.sloppy_global_writes.push((slot, name.to_owned()));
u16::try_from(self.sloppy_global_writes.len() - 1).ok()
}
}
Loading
Loading