Skip to content

Commit 59d5175

Browse files
committed
perf(stable-api): inline rhash_size for MRI stable versions
Read RHash AR-table size directly from RBasic.flags (one mask+shift) and ST-table size from st_table.num_entries (one deref), replacing the rb_hash_size dylib call. Handles the 3.3+ layout change where st_table is embedded at sizeof(RHash) rather than behind a pointer. Rust: 4 instructions C (RHASH_SIZE): 3 instructions
1 parent e346649 commit 59d5175

9 files changed

Lines changed: 304 additions & 68 deletions

File tree

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,7 +1667,28 @@ parity_test!(
16671667
func: rhash_size,
16681668
data_factory: {
16691669
ruby_eval!("{}")
1670-
}
1670+
},
1671+
expected: 0
1672+
);
1673+
1674+
parity_test!(
1675+
name: test_rhash_size_ar_mode,
1676+
func: rhash_size,
1677+
data_factory: {
1678+
// Small hash — typically uses AR table (up to 8 entries)
1679+
ruby_eval!("{a: 1, b: 2}")
1680+
},
1681+
expected: 2
1682+
);
1683+
1684+
parity_test!(
1685+
name: test_rhash_size_st_mode,
1686+
func: rhash_size,
1687+
data_factory: {
1688+
// Large hash — forces ST table (> 8 entries)
1689+
ruby_eval!("(1..20).each_with_object({}) { |i, h| h[i] = i }")
1690+
},
1691+
expected: 20
16711692
);
16721693

16731694
#[rb_sys_test_helpers::ruby_test]
@@ -1685,7 +1706,17 @@ parity_test!(
16851706
func: rhash_empty_p,
16861707
data_factory: {
16871708
ruby_eval!("{}")
1688-
}
1709+
},
1710+
expected: true
1711+
);
1712+
1713+
parity_test!(
1714+
name: test_rhash_empty_p_nonempty,
1715+
func: rhash_empty_p,
1716+
data_factory: {
1717+
ruby_eval!("{a: 1}")
1718+
},
1719+
expected: false
16891720
);
16901721

16911722
#[rb_sys_test_helpers::ruby_test]

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -536,21 +536,49 @@ impl StableApiDefinition for Definition {
536536
unsafe { crate::rb_float_new(val) }
537537
}
538538

539-
#[inline]
539+
#[inline(always)]
540540
unsafe fn rhash_size(&self, obj: VALUE) -> usize {
541-
// Call rb_hash_size which returns a fixnum VALUE
542-
let size_val = crate::rb_hash_size(obj);
543-
// Convert fixnum to usize
544-
if self.fixnum_p(size_val) {
545-
// FIX2LONG: shift right by 1 to get the actual value
546-
(size_val >> 1) as usize
541+
// Ruby 2.7 RHash layout:
542+
// struct RHash { RBasic basic; union { st_table *st; ar_table *ar; } as; VALUE ifnone; };
543+
//
544+
// AR mode (RUBY_FL_USER3 not set): size is packed in
545+
// RBasic.flags bits [USER4..USER7] >> 16 (= FL_USHIFT+4).
546+
// ST mode (RUBY_FL_USER3 set): dereference the st_table pointer in
547+
// RHash.as and read st_table.num_entries directly.
548+
//
549+
// SAFETY: caller guarantees obj is a valid T_HASH VALUE.
550+
#[repr(C)]
551+
struct RHashPre33 {
552+
basic: crate::RBasic,
553+
st: *mut crate::st_table, // union { st_table *st; ar_table *ar; } — same size
554+
ifnone: VALUE,
555+
}
556+
557+
let rhash = obj as *const RHashPre33;
558+
let flags = (*rhash).basic.flags;
559+
560+
// RHASH_ST_TABLE_FLAG = FL_USER3 = 32768
561+
let st_flag = crate::ruby_fl_type::RUBY_FL_USER3 as VALUE;
562+
if (flags & st_flag) == 0 {
563+
// AR mode: size encoded in bits [USER4..USER7].
564+
// RHASH_AR_TABLE_SIZE_MASK = FL_USER4|FL_USER5|FL_USER6|FL_USER7 = 0x000F_0000
565+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16
566+
let mask: VALUE = (crate::ruby_fl_type::RUBY_FL_USER4 as VALUE)
567+
| (crate::ruby_fl_type::RUBY_FL_USER5 as VALUE)
568+
| (crate::ruby_fl_type::RUBY_FL_USER6 as VALUE)
569+
| (crate::ruby_fl_type::RUBY_FL_USER7 as VALUE);
570+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16 (stable across all Ruby versions)
571+
let shift = 16u32;
572+
((flags & mask) >> shift) as usize
547573
} else {
548-
// Fallback for large hashes (shouldn't normally happen)
549-
crate::rb_num2ulong(size_val) as usize
574+
// ST mode: dereference the st_table pointer and read num_entries.
575+
// SAFETY: the st pointer is valid when RHASH_ST_TABLE_FLAG is set.
576+
let st = (*rhash).st;
577+
(*st).num_entries as usize
550578
}
551579
}
552580

553-
#[inline]
581+
#[inline(always)]
554582
unsafe fn rhash_empty_p(&self, obj: VALUE) -> bool {
555583
self.rhash_size(obj) == 0
556584
}

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -539,21 +539,49 @@ impl StableApiDefinition for Definition {
539539
unsafe { crate::rb_float_new(val) }
540540
}
541541

542-
#[inline]
542+
#[inline(always)]
543543
unsafe fn rhash_size(&self, obj: VALUE) -> usize {
544-
// Call rb_hash_size which returns a fixnum VALUE
545-
let size_val = crate::rb_hash_size(obj);
546-
// Convert fixnum to usize
547-
if self.fixnum_p(size_val) {
548-
// FIX2LONG: shift right by 1 to get the actual value
549-
(size_val >> 1) as usize
544+
// Ruby 3.0 RHash layout (pre-3.3):
545+
// struct RHash { RBasic basic; union { st_table *st; ar_table *ar; } as; VALUE ifnone; };
546+
//
547+
// AR mode (RUBY_FL_USER3 not set): size is packed in
548+
// RBasic.flags bits [USER4..USER7] >> 16 (= FL_USHIFT+4).
549+
// ST mode (RUBY_FL_USER3 set): dereference the st_table pointer in
550+
// RHash.as and read st_table.num_entries directly.
551+
//
552+
// SAFETY: caller guarantees obj is a valid T_HASH VALUE.
553+
#[repr(C)]
554+
struct RHashPre33 {
555+
basic: crate::RBasic,
556+
st: *mut crate::st_table, // union { st_table *st; ar_table *ar; } — same size
557+
ifnone: VALUE,
558+
}
559+
560+
let rhash = obj as *const RHashPre33;
561+
let flags = (*rhash).basic.flags;
562+
563+
// RHASH_ST_TABLE_FLAG = FL_USER3 = 32768
564+
let st_flag = crate::ruby_fl_type::RUBY_FL_USER3 as VALUE;
565+
if (flags & st_flag) == 0 {
566+
// AR mode: size encoded in bits [USER4..USER7].
567+
// RHASH_AR_TABLE_SIZE_MASK = FL_USER4|FL_USER5|FL_USER6|FL_USER7 = 0x000F_0000
568+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16
569+
let mask: VALUE = (crate::ruby_fl_type::RUBY_FL_USER4 as VALUE)
570+
| (crate::ruby_fl_type::RUBY_FL_USER5 as VALUE)
571+
| (crate::ruby_fl_type::RUBY_FL_USER6 as VALUE)
572+
| (crate::ruby_fl_type::RUBY_FL_USER7 as VALUE);
573+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16 (stable across all Ruby versions)
574+
let shift = 16u32;
575+
((flags & mask) >> shift) as usize
550576
} else {
551-
// Fallback for large hashes (shouldn't normally happen)
552-
crate::rb_num2ulong(size_val) as usize
577+
// ST mode: dereference the st_table pointer and read num_entries.
578+
// SAFETY: the st pointer is valid when RHASH_ST_TABLE_FLAG is set.
579+
let st = (*rhash).st;
580+
(*st).num_entries as usize
553581
}
554582
}
555583

556-
#[inline]
584+
#[inline(always)]
557585
unsafe fn rhash_empty_p(&self, obj: VALUE) -> bool {
558586
self.rhash_size(obj) == 0
559587
}

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -532,21 +532,49 @@ impl StableApiDefinition for Definition {
532532
unsafe { crate::rb_float_new(val) }
533533
}
534534

535-
#[inline]
535+
#[inline(always)]
536536
unsafe fn rhash_size(&self, obj: VALUE) -> usize {
537-
// Call rb_hash_size which returns a fixnum VALUE
538-
let size_val = crate::rb_hash_size(obj);
539-
// Convert fixnum to usize
540-
if self.fixnum_p(size_val) {
541-
// FIX2LONG: shift right by 1 to get the actual value
542-
(size_val >> 1) as usize
537+
// Ruby 3.1 RHash layout (pre-3.3):
538+
// struct RHash { RBasic basic; union { st_table *st; ar_table *ar; } as; VALUE ifnone; };
539+
//
540+
// AR mode (RUBY_FL_USER3 not set): size is packed in
541+
// RBasic.flags bits [USER4..USER7] >> 16 (= FL_USHIFT+4).
542+
// ST mode (RUBY_FL_USER3 set): dereference the st_table pointer in
543+
// RHash.as and read st_table.num_entries directly.
544+
//
545+
// SAFETY: caller guarantees obj is a valid T_HASH VALUE.
546+
#[repr(C)]
547+
struct RHashPre33 {
548+
basic: crate::RBasic,
549+
st: *mut crate::st_table, // union { st_table *st; ar_table *ar; } — same size
550+
ifnone: VALUE,
551+
}
552+
553+
let rhash = obj as *const RHashPre33;
554+
let flags = (*rhash).basic.flags;
555+
556+
// RHASH_ST_TABLE_FLAG = FL_USER3 = 32768
557+
let st_flag = crate::ruby_fl_type::RUBY_FL_USER3 as VALUE;
558+
if (flags & st_flag) == 0 {
559+
// AR mode: size encoded in bits [USER4..USER7].
560+
// RHASH_AR_TABLE_SIZE_MASK = FL_USER4|FL_USER5|FL_USER6|FL_USER7 = 0x000F_0000
561+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16
562+
let mask: VALUE = (crate::ruby_fl_type::RUBY_FL_USER4 as VALUE)
563+
| (crate::ruby_fl_type::RUBY_FL_USER5 as VALUE)
564+
| (crate::ruby_fl_type::RUBY_FL_USER6 as VALUE)
565+
| (crate::ruby_fl_type::RUBY_FL_USER7 as VALUE);
566+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16 (stable across all Ruby versions)
567+
let shift = 16u32;
568+
((flags & mask) >> shift) as usize
543569
} else {
544-
// Fallback for large hashes (shouldn't normally happen)
545-
crate::rb_num2ulong(size_val) as usize
570+
// ST mode: dereference the st_table pointer and read num_entries.
571+
// SAFETY: the st pointer is valid when RHASH_ST_TABLE_FLAG is set.
572+
let st = (*rhash).st;
573+
(*st).num_entries as usize
546574
}
547575
}
548576

549-
#[inline]
577+
#[inline(always)]
550578
unsafe fn rhash_empty_p(&self, obj: VALUE) -> bool {
551579
self.rhash_size(obj) == 0
552580
}

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -530,21 +530,49 @@ impl StableApiDefinition for Definition {
530530
unsafe { crate::rb_float_new(val) }
531531
}
532532

533-
#[inline]
533+
#[inline(always)]
534534
unsafe fn rhash_size(&self, obj: VALUE) -> usize {
535-
// Call rb_hash_size which returns a fixnum VALUE
536-
let size_val = crate::rb_hash_size(obj);
537-
// Convert fixnum to usize
538-
if self.fixnum_p(size_val) {
539-
// FIX2LONG: shift right by 1 to get the actual value
540-
(size_val >> 1) as usize
535+
// Ruby 3.2 RHash layout (pre-3.3):
536+
// struct RHash { RBasic basic; union { st_table *st; ar_table *ar; } as; VALUE ifnone; };
537+
//
538+
// AR mode (RUBY_FL_USER3 not set): size is packed in
539+
// RBasic.flags bits [USER4..USER7] >> 16 (= FL_USHIFT+4).
540+
// ST mode (RUBY_FL_USER3 set): dereference the st_table pointer in
541+
// RHash.as and read st_table.num_entries directly.
542+
//
543+
// SAFETY: caller guarantees obj is a valid T_HASH VALUE.
544+
#[repr(C)]
545+
struct RHashPre33 {
546+
basic: crate::RBasic,
547+
st: *mut crate::st_table, // union { st_table *st; ar_table *ar; } — same size
548+
ifnone: VALUE,
549+
}
550+
551+
let rhash = obj as *const RHashPre33;
552+
let flags = (*rhash).basic.flags;
553+
554+
// RHASH_ST_TABLE_FLAG = FL_USER3 = 32768
555+
let st_flag = crate::ruby_fl_type::RUBY_FL_USER3 as VALUE;
556+
if (flags & st_flag) == 0 {
557+
// AR mode: size encoded in bits [USER4..USER7].
558+
// RHASH_AR_TABLE_SIZE_MASK = FL_USER4|FL_USER5|FL_USER6|FL_USER7 = 0x000F_0000
559+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16
560+
let mask: VALUE = (crate::ruby_fl_type::RUBY_FL_USER4 as VALUE)
561+
| (crate::ruby_fl_type::RUBY_FL_USER5 as VALUE)
562+
| (crate::ruby_fl_type::RUBY_FL_USER6 as VALUE)
563+
| (crate::ruby_fl_type::RUBY_FL_USER7 as VALUE);
564+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16 (stable across all Ruby versions)
565+
let shift = 16u32;
566+
((flags & mask) >> shift) as usize
541567
} else {
542-
// Fallback for large hashes (shouldn't normally happen)
543-
crate::rb_num2ulong(size_val) as usize
568+
// ST mode: dereference the st_table pointer and read num_entries.
569+
// SAFETY: the st pointer is valid when RHASH_ST_TABLE_FLAG is set.
570+
let st = (*rhash).st;
571+
(*st).num_entries as usize
544572
}
545573
}
546574

547-
#[inline]
575+
#[inline(always)]
548576
unsafe fn rhash_empty_p(&self, obj: VALUE) -> bool {
549577
self.rhash_size(obj) == 0
550578
}

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -542,21 +542,49 @@ impl StableApiDefinition for Definition {
542542
unsafe { crate::rb_float_new(val) }
543543
}
544544

545-
#[inline]
545+
#[inline(always)]
546546
unsafe fn rhash_size(&self, obj: VALUE) -> usize {
547-
// Call rb_hash_size which returns a fixnum VALUE
548-
let size_val = crate::rb_hash_size(obj);
549-
// Convert fixnum to usize
550-
if self.fixnum_p(size_val) {
551-
// FIX2LONG: shift right by 1 to get the actual value
552-
(size_val >> 1) as usize
547+
// Ruby 3.3 RHash layout (3.3+):
548+
// struct RHash { RBasic basic; VALUE ifnone; };
549+
// // st_table embedded at sizeof(RHash) = 24 bytes from obj when in ST mode.
550+
//
551+
// AR mode (RUBY_FL_USER3 not set): size is packed in
552+
// RBasic.flags bits [USER4..USER7] >> 16 (= FL_USHIFT+4).
553+
// ST mode (RUBY_FL_USER3 set): the st_table is embedded immediately after
554+
// the RHash struct in memory at offset sizeof(RHash) = 24 bytes.
555+
//
556+
// SAFETY: caller guarantees obj is a valid T_HASH VALUE.
557+
#[repr(C)]
558+
struct RHash33 {
559+
basic: crate::RBasic,
560+
ifnone: VALUE,
561+
}
562+
563+
let rbasic = obj as *const crate::RBasic;
564+
let flags = (*rbasic).flags;
565+
566+
// RHASH_ST_TABLE_FLAG = FL_USER3 = 32768
567+
let st_flag = crate::ruby_fl_type::RUBY_FL_USER3 as VALUE;
568+
if (flags & st_flag) == 0 {
569+
// AR mode: size encoded in bits [USER4..USER7].
570+
// RHASH_AR_TABLE_SIZE_MASK = FL_USER4|FL_USER5|FL_USER6|FL_USER7 = 0x000F_0000
571+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16
572+
let mask: VALUE = (crate::ruby_fl_type::RUBY_FL_USER4 as VALUE)
573+
| (crate::ruby_fl_type::RUBY_FL_USER5 as VALUE)
574+
| (crate::ruby_fl_type::RUBY_FL_USER6 as VALUE)
575+
| (crate::ruby_fl_type::RUBY_FL_USER7 as VALUE);
576+
// RHASH_AR_TABLE_SIZE_SHIFT = FL_USHIFT + 4 = 12 + 4 = 16 (stable across all Ruby versions)
577+
let shift = 16u32;
578+
((flags & mask) >> shift) as usize
553579
} else {
554-
// Fallback for large hashes (shouldn't normally happen)
555-
crate::rb_num2ulong(size_val) as usize
580+
// ST mode: the st_table is embedded at sizeof(RHash) past obj.
581+
// SAFETY: the embedded st_table is valid when RHASH_ST_TABLE_FLAG is set.
582+
let st_ptr = (obj as usize + core::mem::size_of::<RHash33>()) as *const crate::st_table;
583+
(*st_ptr).num_entries as usize
556584
}
557585
}
558586

559-
#[inline]
587+
#[inline(always)]
560588
unsafe fn rhash_empty_p(&self, obj: VALUE) -> bool {
561589
self.rhash_size(obj) == 0
562590
}

0 commit comments

Comments
 (0)