Skip to content

Commit 727b118

Browse files
author
Ralph Küpper
committed
perf(runtime): give the relational operators the numeric fast path + and === already have
1 parent d1d9929 commit 727b118

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

crates/perry-runtime/src/builtins/arithmetic.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,28 +433,64 @@ unsafe fn string_content_for_bigint(value: f64) -> String {
433433
String::from_utf8_lossy(bytes).into_owned()
434434
}
435435

436+
/// Both operands already numeric (plain IEEE double or int32-tagged)?
437+
///
438+
/// `abstract_relational` opens a `RuntimeHandleScope`, roots both operands and
439+
/// runs `ToPrimitive` on each — necessary only because a *heap* operand can run
440+
/// user `valueOf`/`toString`. For a number `ToPrimitive` is the identity and
441+
/// there is no pointer to root, so the whole apparatus is dead weight. This is
442+
/// the same predicate and the same reasoning `dynamic_arith`'s binary operators
443+
/// already use; the relational operators were simply never given it.
444+
///
445+
/// NaN must stay `false` for all four operators, which Rust's `<`/`>`/`<=`/`>=`
446+
/// on `f64` already deliver.
447+
#[inline(always)]
448+
fn rel_numeric_operand(v: f64) -> Option<f64> {
449+
const TAG_BAND_FLOOR: u64 = 0x7FF9_0000_0000_0000;
450+
if (v.to_bits() & 0x7FFF_0000_0000_0000) < TAG_BAND_FLOOR {
451+
return Some(v);
452+
}
453+
let jv = crate::value::JSValue::from_bits(v.to_bits());
454+
if jv.is_int32() {
455+
return Some(jv.as_int32() as f64);
456+
}
457+
None
458+
}
459+
436460
/// `x < y` — codegen routes here for any relational `<` whose operands are not
437461
/// both statically numeric. Returns a NaN-boxed boolean (`f64`).
438462
#[no_mangle]
439463
pub extern "C" fn js_rel_lt(x: f64, y: f64) -> f64 {
464+
if let (Some(a), Some(b)) = (rel_numeric_operand(x), rel_numeric_operand(y)) {
465+
return rel_bool_f64(a < b);
466+
}
440467
rel_bool_f64(unsafe { abstract_relational(x, y, true) } == REL_TRUE)
441468
}
442469

443470
/// `x > y` ⇔ `IsLessThan(y, x, false)` is true (right operand `ToPrimitive`'d first).
444471
#[no_mangle]
445472
pub extern "C" fn js_rel_gt(x: f64, y: f64) -> f64 {
473+
if let (Some(a), Some(b)) = (rel_numeric_operand(x), rel_numeric_operand(y)) {
474+
return rel_bool_f64(a > b);
475+
}
446476
rel_bool_f64(unsafe { abstract_relational(y, x, false) } == REL_TRUE)
447477
}
448478

449479
/// `x <= y` ⇔ `IsLessThan(y, x, false)` is `false` (not `true`, not `undefined`).
450480
#[no_mangle]
451481
pub extern "C" fn js_rel_le(x: f64, y: f64) -> f64 {
482+
if let (Some(a), Some(b)) = (rel_numeric_operand(x), rel_numeric_operand(y)) {
483+
return rel_bool_f64(a <= b);
484+
}
452485
rel_bool_f64(unsafe { abstract_relational(y, x, false) } == REL_FALSE)
453486
}
454487

455488
/// `x >= y` ⇔ `IsLessThan(x, y, true)` is `false` (not `true`, not `undefined`).
456489
#[no_mangle]
457490
pub extern "C" fn js_rel_ge(x: f64, y: f64) -> f64 {
491+
if let (Some(a), Some(b)) = (rel_numeric_operand(x), rel_numeric_operand(y)) {
492+
return rel_bool_f64(a >= b);
493+
}
458494
rel_bool_f64(unsafe { abstract_relational(x, y, true) } == REL_FALSE)
459495
}
460496

@@ -773,3 +809,85 @@ pub extern "C" fn js_value_typeof(value: f64) -> *mut StringHeader {
773809
get_cached(&TYPEOF_NUMBER, "number")
774810
}
775811
}
812+
813+
814+
#[cfg(test)]
815+
mod rel_numeric_fastpath_tests {
816+
use super::*;
817+
818+
const INT32: u64 = 0x7FFE_0000_0000_0000;
819+
const UNDEF: u64 = 0x7FFC_0000_0000_0001;
820+
const NULLV: u64 = 0x7FFC_0000_0000_0002;
821+
const FALSEV: u64 = 0x7FFC_0000_0000_0003;
822+
const TRUEV: u64 = 0x7FFC_0000_0000_0004;
823+
824+
fn i32v(n: i32) -> f64 {
825+
f64::from_bits(INT32 | (n as u32 as u64))
826+
}
827+
fn is_true(v: f64) -> bool {
828+
v.to_bits() == TAG_TRUE_BITS
829+
}
830+
831+
/// The early-out accepts exactly the operands for which `ToPrimitive` is
832+
/// the identity and there is nothing to root; everything else must fall
833+
/// through to the full abstract relational comparison.
834+
#[test]
835+
fn fast_path_accepts_only_numbers() {
836+
assert!(rel_numeric_operand(1.5).is_some());
837+
assert!(rel_numeric_operand(-0.0).is_some());
838+
assert!(rel_numeric_operand(f64::INFINITY).is_some());
839+
assert!(rel_numeric_operand(f64::NEG_INFINITY).is_some());
840+
assert!(rel_numeric_operand(f64::NAN).is_some());
841+
assert_eq!(rel_numeric_operand(i32v(7)), Some(7.0));
842+
assert_eq!(rel_numeric_operand(i32v(-7)), Some(-7.0));
843+
for tag in [UNDEF, NULLV, FALSEV, TRUEV] {
844+
assert!(
845+
rel_numeric_operand(f64::from_bits(tag)).is_none(),
846+
"tag {tag:#x} must not take the numeric fast path"
847+
);
848+
}
849+
}
850+
851+
/// NaN makes all four operators false. This is the one way a naive `fcmp`
852+
/// early-out silently diverges from the spec, so pin it in both operand
853+
/// positions.
854+
#[test]
855+
fn nan_is_false_for_every_operator() {
856+
let n = f64::NAN;
857+
for (name, got) in [
858+
("NaN < x", js_rel_lt(n, 1.0)),
859+
("NaN > x", js_rel_gt(n, 1.0)),
860+
("NaN <= x", js_rel_le(n, 1.0)),
861+
("NaN >= x", js_rel_ge(n, 1.0)),
862+
("x < NaN", js_rel_lt(1.0, n)),
863+
("x > NaN", js_rel_gt(1.0, n)),
864+
("x <= NaN", js_rel_le(1.0, n)),
865+
("x >= NaN", js_rel_ge(1.0, n)),
866+
] {
867+
assert!(!is_true(got), "{name} must be false");
868+
}
869+
}
870+
871+
/// `-0 < 0` is false while `-0 <= 0` is true.
872+
#[test]
873+
fn signed_zero_matches_the_spec() {
874+
assert!(!is_true(js_rel_lt(-0.0, 0.0)));
875+
assert!(!is_true(js_rel_gt(-0.0, 0.0)));
876+
assert!(is_true(js_rel_le(-0.0, 0.0)));
877+
assert!(is_true(js_rel_ge(-0.0, 0.0)));
878+
}
879+
880+
#[test]
881+
fn ordinary_numeric_comparisons_are_unchanged() {
882+
assert!(is_true(js_rel_lt(1.0, 2.0)));
883+
assert!(!is_true(js_rel_lt(2.0, 1.0)));
884+
assert!(is_true(js_rel_ge(2.0, 2.0)));
885+
assert!(is_true(js_rel_le(2.0, 2.0)));
886+
assert!(is_true(js_rel_gt(f64::INFINITY, 1e308)));
887+
assert!(is_true(js_rel_lt(f64::NEG_INFINITY, 0.0)));
888+
assert!(is_true(js_rel_lt(i32v(3), i32v(9))));
889+
assert!(!is_true(js_rel_gt(i32v(3), i32v(9))));
890+
assert!(is_true(js_rel_lt(i32v(3), 3.5)));
891+
assert!(is_true(js_rel_gt(3.5, i32v(3))));
892+
}
893+
}

0 commit comments

Comments
 (0)