Skip to content

Commit e346649

Browse files
authored
perf(stable-api): inline dbl2num flonum encode for MRI stable versions (#734)
When the double is representable as a flonum tagged immediate, encode it in pure Rust (rotate-left-3 + tag) without calling rb_float_new. Falls back to rb_float_new for out-of-range doubles and on non-flonum builds. Mirrors the existing flonum decode fast path in num2dbl. Rust: 3 instructions C (DBL2NUM): 3 instructions
1 parent 27271f8 commit e346649

9 files changed

Lines changed: 166 additions & 12 deletions

File tree

crates/rb-sys-tests/src/stable_api_test.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,60 @@ fn test_dbl2num_and_num2dbl_roundtrip() {
16081608
}
16091609
}
16101610

1611+
// Parity tests for dbl2num: flonum-range values encode as tagged immediates,
1612+
// so both Rust and C produce identical VALUE representations (no heap pointer).
1613+
parity_test!(
1614+
name: test_dbl2num_in_flonum_range,
1615+
func: dbl2num,
1616+
data_factory: { 1.5f64 }
1617+
);
1618+
1619+
parity_test!(
1620+
name: test_dbl2num_positive_zero,
1621+
func: dbl2num,
1622+
data_factory: { 0.0f64 }
1623+
);
1624+
1625+
// Out-of-flonum-range values heap-allocate; verify via roundtrip not pointer equality.
1626+
#[rb_sys_test_helpers::ruby_test]
1627+
fn test_dbl2num_out_of_flonum_range() {
1628+
unsafe {
1629+
let val = 1e300f64;
1630+
let rust_obj = stable_api::get_default().dbl2num(val);
1631+
let recovered = stable_api::get_default().num2dbl(rust_obj);
1632+
assert!(
1633+
(val - recovered).abs() < f64::EPSILON,
1634+
"roundtrip failed: {} != {}",
1635+
val,
1636+
recovered
1637+
);
1638+
1639+
// Also confirm parity with compiled C by decoding both
1640+
let c_obj = stable_api::get_compiled().dbl2num(val);
1641+
let c_recovered = stable_api::get_default().num2dbl(c_obj);
1642+
assert!(
1643+
(val - c_recovered).abs() < f64::EPSILON,
1644+
"C roundtrip failed: {} != {}",
1645+
val,
1646+
c_recovered
1647+
);
1648+
}
1649+
}
1650+
1651+
#[rb_sys_test_helpers::ruby_test]
1652+
fn test_dbl2num_infinity() {
1653+
unsafe {
1654+
let inf_obj = ruby_eval!("Float::INFINITY");
1655+
let recovered = stable_api::get_default().num2dbl(inf_obj);
1656+
assert!(recovered.is_infinite() && recovered > 0.0);
1657+
1658+
// Verify our dbl2num produces a value that num2dbl can round-trip
1659+
let encoded = stable_api::get_default().dbl2num(f64::INFINITY);
1660+
let decoded = stable_api::get_default().num2dbl(encoded);
1661+
assert!(decoded.is_infinite() && decoded > 0.0);
1662+
}
1663+
}
1664+
16111665
parity_test!(
16121666
name: test_rhash_size_empty,
16131667
func: rhash_size,

crates/rb-sys/src/stable_api/ruby_2_7.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,22 @@ impl StableApiDefinition for Definition {
517517
}
518518
}
519519

520-
#[inline]
520+
#[inline(always)]
521521
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
522-
// Call the C function rb_float_new to create a Float VALUE
522+
#[cfg(ruby_use_flonum = "true")]
523+
{
524+
let bits = val.to_bits() as VALUE;
525+
let exp_bits = (bits >> 60) & 0x7;
526+
// Flonum-representable: exponent top-3 bits are 011 or 100
527+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
528+
return (bits.rotate_left(3) & !0x01) | 0x02;
529+
}
530+
// +0.0 special case
531+
if bits == 0 {
532+
return 0x8000_0000_0000_0002;
533+
}
534+
}
535+
// Out-of-flonum-range or flonum disabled: heap allocate
523536
unsafe { crate::rb_float_new(val) }
524537
}
525538

crates/rb-sys/src/stable_api/ruby_3_0.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,22 @@ impl StableApiDefinition for Definition {
520520
}
521521
}
522522

523-
#[inline]
523+
#[inline(always)]
524524
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
525-
// Call the C function rb_float_new to create a Float VALUE
525+
#[cfg(ruby_use_flonum = "true")]
526+
{
527+
let bits = val.to_bits() as VALUE;
528+
let exp_bits = (bits >> 60) & 0x7;
529+
// Flonum-representable: exponent top-3 bits are 011 or 100
530+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
531+
return (bits.rotate_left(3) & !0x01) | 0x02;
532+
}
533+
// +0.0 special case
534+
if bits == 0 {
535+
return 0x8000_0000_0000_0002;
536+
}
537+
}
538+
// Out-of-flonum-range or flonum disabled: heap allocate
526539
unsafe { crate::rb_float_new(val) }
527540
}
528541

crates/rb-sys/src/stable_api/ruby_3_1.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,22 @@ impl StableApiDefinition for Definition {
513513
}
514514
}
515515

516-
#[inline]
516+
#[inline(always)]
517517
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
518-
// Call the C function rb_float_new to create a Float VALUE
518+
#[cfg(ruby_use_flonum = "true")]
519+
{
520+
let bits = val.to_bits() as VALUE;
521+
let exp_bits = (bits >> 60) & 0x7;
522+
// Flonum-representable: exponent top-3 bits are 011 or 100
523+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
524+
return (bits.rotate_left(3) & !0x01) | 0x02;
525+
}
526+
// +0.0 special case
527+
if bits == 0 {
528+
return 0x8000_0000_0000_0002;
529+
}
530+
}
531+
// Out-of-flonum-range or flonum disabled: heap allocate
519532
unsafe { crate::rb_float_new(val) }
520533
}
521534

crates/rb-sys/src/stable_api/ruby_3_2.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,22 @@ impl StableApiDefinition for Definition {
511511
}
512512
}
513513

514-
#[inline]
514+
#[inline(always)]
515515
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
516-
// Call the C function rb_float_new to create a Float VALUE
516+
#[cfg(ruby_use_flonum = "true")]
517+
{
518+
let bits = val.to_bits() as VALUE;
519+
let exp_bits = (bits >> 60) & 0x7;
520+
// Flonum-representable: exponent top-3 bits are 011 or 100
521+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
522+
return (bits.rotate_left(3) & !0x01) | 0x02;
523+
}
524+
// +0.0 special case
525+
if bits == 0 {
526+
return 0x8000_0000_0000_0002;
527+
}
528+
}
529+
// Out-of-flonum-range or flonum disabled: heap allocate
517530
unsafe { crate::rb_float_new(val) }
518531
}
519532

crates/rb-sys/src/stable_api/ruby_3_3.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,22 @@ impl StableApiDefinition for Definition {
523523
}
524524
}
525525

526-
#[inline]
526+
#[inline(always)]
527527
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
528-
// Call the C function rb_float_new to create a Float VALUE
528+
#[cfg(ruby_use_flonum = "true")]
529+
{
530+
let bits = val.to_bits() as VALUE;
531+
let exp_bits = (bits >> 60) & 0x7;
532+
// Flonum-representable: exponent top-3 bits are 011 or 100
533+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
534+
return (bits.rotate_left(3) & !0x01) | 0x02;
535+
}
536+
// +0.0 special case
537+
if bits == 0 {
538+
return 0x8000_0000_0000_0002;
539+
}
540+
}
541+
// Out-of-flonum-range or flonum disabled: heap allocate
529542
unsafe { crate::rb_float_new(val) }
530543
}
531544

crates/rb-sys/src/stable_api/ruby_3_4.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,20 @@ impl StableApiDefinition for Definition {
537537

538538
#[inline(always)]
539539
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
540-
// Call the C function rb_float_new to create a Float VALUE
540+
#[cfg(ruby_use_flonum = "true")]
541+
{
542+
let bits = val.to_bits() as VALUE;
543+
let exp_bits = (bits >> 60) & 0x7;
544+
// Flonum-representable: exponent top-3 bits are 011 or 100
545+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
546+
return (bits.rotate_left(3) & !0x01) | 0x02;
547+
}
548+
// +0.0 special case
549+
if bits == 0 {
550+
return 0x8000_0000_0000_0002;
551+
}
552+
}
553+
// Out-of-flonum-range or flonum disabled: heap allocate
541554
unsafe { crate::rb_float_new(val) }
542555
}
543556

crates/rb-sys/src/stable_api/ruby_4_0.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,20 @@ impl StableApiDefinition for Definition {
533533

534534
#[inline(always)]
535535
fn dbl2num(&self, val: std::os::raw::c_double) -> VALUE {
536-
// Call the C function rb_float_new to create a Float VALUE
536+
#[cfg(ruby_use_flonum = "true")]
537+
{
538+
let bits = val.to_bits() as VALUE;
539+
let exp_bits = (bits >> 60) & 0x7;
540+
// Flonum-representable: exponent top-3 bits are 011 or 100
541+
if bits != 0x3000_0000_0000_0000 && (exp_bits == 3 || exp_bits == 4) {
542+
return (bits.rotate_left(3) & !0x01) | 0x02;
543+
}
544+
// +0.0 special case
545+
if bits == 0 {
546+
return 0x8000_0000_0000_0002;
547+
}
548+
}
549+
// Out-of-flonum-range or flonum disabled: heap allocate
537550
unsafe { crate::rb_float_new(val) }
538551
}
539552

script/show-asm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ FUNCTIONS = [
302302
ruby_source: "include/ruby/internal/arithmetic/long.h:RB_ULONG2NUM"
303303
},
304304

305+
# Float conversion operations
306+
{
307+
name: "dbl2num",
308+
rust: {unsafe: false, ret: "VALUE",
309+
expr: "{ let api = rb_sys::stable_api::get_default(); api.dbl2num(1.5f64) }"},
310+
c: {expr: "DBL2NUM(1.5)", ret: "VALUE"},
311+
ruby_source: "include/ruby/internal/arithmetic/double.h:DBL2NUM"
312+
},
313+
305314
# GC guard (macro, not StableApiDefinition method)
306315
# Uses a realistic pattern: extract pointer, call function that might GC, use guard
307316
{

0 commit comments

Comments
 (0)