From fd0c772db7e5098c2b8e03559317a3592074dfe7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 30 Aug 2025 10:51:03 +0200 Subject: [PATCH 1/5] Micro-optimize Object#class Since `BUILTIN_TYPE` and `RCLASS_SINGLETON_P` are both stored in `RBasic.flags`, we can combine these two checks in a single bitmask. This rely on `T_ICLASS` and `T_CLASS` not overlapping, and assume `klass` is always either of these types. Just combining the masks brings a small but consistent 1.08x speedup on the simple case benchmark. ``` compare-ruby: ruby 3.5.0dev (2025-08-30T01:45:42Z obj-class 01a57bd6cd) +YJIT +PRISM [arm64-darwin24] built-ruby: ruby 3.5.0dev (2025-08-30T09:56:24Z obj-class 2685f8dbb4) +YJIT +PRISM [arm64-darwin24] | |compare-ruby|built-ruby| |:----------|-----------:|---------:| |obj | 444.410| 478.895| | | -| 1.08x| |extended | 135.139| 140.206| | | -| 1.04x| |singleton | 165.155| 155.832| | | 1.06x| -| |immediate | 380.103| 432.090| | | -| 1.14x| ``` But with the RB_UNLIKELY compiler hint, it's much more significant, however the singleton and enxtended cases are slowed down. However we can assume the simple case is way more common than the other two. ``` compare-ruby: ruby 3.5.0dev (2025-08-30T01:45:42Z obj-class 01a57bd6cd) +YJIT +PRISM [arm64-darwin24] built-ruby: ruby 3.5.0dev (2025-08-30T09:51:01Z obj-class 12d01a1b02) +YJIT +PRISM [arm64-darwin24] | |compare-ruby|built-ruby| |:----------|-----------:|---------:| |obj | 444.951| 556.191| | | -| 1.25x| |extended | 136.836| 113.871| | | 1.20x| -| |singleton | 166.335| 167.747| | | -| 1.01x| |immediate | 379.642| 509.515| | | -| 1.34x| ``` --- benchmark/object_class.yml | 40 ++++++++++++++++++++++++++++++++++++++ object.c | 15 +++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 benchmark/object_class.yml diff --git a/benchmark/object_class.yml b/benchmark/object_class.yml new file mode 100644 index 00000000000000..1e5409d1e2dfab --- /dev/null +++ b/benchmark/object_class.yml @@ -0,0 +1,40 @@ +prelude: | + def get_class(obj) + i = 10_000 + while i > 0 + i -= 1 + # 100 times per loop + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + end + end + + class Obj + end + obj = Obj.new + + singleton = Obj.new + def singleton.bar + end + + extended = Obj.new + 2.times do + extended.extend Module.new + end + + immediate = 1.4 +benchmark: + obj: get_class(obj) + extended: get_class(extended) + singleton: get_class(singleton) + immediate: get_class(immediate) +loop_count: 1000 diff --git a/object.c b/object.c index a7def50cd7a068..9a770e64f9425a 100644 --- a/object.c +++ b/object.c @@ -288,10 +288,23 @@ rb_class_real(VALUE cl) return cl; } +static inline VALUE +fake_class_p(VALUE klass) +{ + RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_ICLASS)); + STATIC_ASSERT(t_iclass_overlap, !(T_CLASS & T_ICLASS)); + + return FL_TEST_RAW(klass, T_ICLASS | FL_SINGLETON); +} + VALUE rb_obj_class(VALUE obj) { - return rb_class_real(CLASS_OF(obj)); + VALUE cl = CLASS_OF(obj); + while (RB_UNLIKELY(cl && fake_class_p(cl))) { + cl = RCLASS_SUPER(cl); + } + return cl; } /* From 01b89528cb663e1e533375d4282d906e49ca1496 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 30 Aug 2025 12:15:11 +0200 Subject: [PATCH 2/5] object.c: improve fake_class_p to also handle T_MODULE This requires ensuring T_MODULE never has FL_SINGLETON set, so RMODULE_IS_REFINEMENT had to be moved. --- class.c | 6 ++++-- object.c | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/class.c b/class.c index 24f61fd023e5fd..472095617c1e8a 100644 --- a/class.c +++ b/class.c @@ -62,8 +62,8 @@ * 0: RCLASS_IS_ROOT * The class has been added to the VM roots. Will always be marked and pinned. * This is done for classes defined from C to allow storing them in global variables. - * 1: RMODULE_IS_REFINEMENT - * Module is used for refinements. + * 1: + * Ensures that RUBY_FL_SINGLETON is never set on a T_MODULE. See `rb_class_real`. * 2: RCLASS_PRIME_CLASSEXT_PRIME_WRITABLE * This module's prime classext is the only classext and writable from any namespaces. * If unset, the prime classext is writable only from the root namespace. @@ -71,6 +71,8 @@ * Module has been initialized. * 4: RCLASS_NAMESPACEABLE * Is a builtin class that may be namespaced. It larger than a normal class. + * 5: RMODULE_IS_REFINEMENT + * Module is used for refinements. */ #define METACLASS_OF(k) RBASIC(k)->klass diff --git a/object.c b/object.c index 9a770e64f9425a..dc5ab2d934beb3 100644 --- a/object.c +++ b/object.c @@ -278,25 +278,25 @@ rb_obj_not_equal(VALUE obj1, VALUE obj2) return rb_obj_not(result); } +static inline VALUE +fake_class_p(VALUE klass) +{ + RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_MODULE) || RB_TYPE_P(klass, T_ICLASS)); + STATIC_ASSERT(t_iclass_overlap_t_class, !(T_CLASS & T_ICLASS)); + STATIC_ASSERT(t_iclass_overlap_t_module, !(T_MODULE & T_ICLASS)); + + return FL_TEST_RAW(klass, T_ICLASS | FL_SINGLETON); +} + VALUE rb_class_real(VALUE cl) { - while (cl && - (RCLASS_SINGLETON_P(cl) || BUILTIN_TYPE(cl) == T_ICLASS)) { + while (RB_UNLIKELY(cl && fake_class_p(cl))) { cl = RCLASS_SUPER(cl); } return cl; } -static inline VALUE -fake_class_p(VALUE klass) -{ - RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_ICLASS)); - STATIC_ASSERT(t_iclass_overlap, !(T_CLASS & T_ICLASS)); - - return FL_TEST_RAW(klass, T_ICLASS | FL_SINGLETON); -} - VALUE rb_obj_class(VALUE obj) { From f5da6395bd3fdf4b76a480f174cd304823ddc57f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 30 Aug 2025 13:04:11 +0200 Subject: [PATCH 3/5] Kernel#class skip null check `Kernel#class` can't possibly be called on an hidden object, hence we don't need to check for `klass == 0`. ``` compare-ruby: ruby 3.5.0dev (2025-08-30T01:45:42Z obj-class 01a57bd6cd) +YJIT +PRISM [arm64-darwin24] built-ruby: ruby 3.5.0dev (2025-08-30T10:21:10Z obj-class b67c16c477) +YJIT +PRISM [arm64-darwin24] | |compare-ruby|built-ruby| |:----------|-----------:|---------:| |obj | 445.217| 642.446| | | -| 1.44x| |extended | 136.826| 117.974| | | 1.16x| -| |singleton | 166.269| 166.695| | | -| 1.00x| |immediate | 380.243| 515.775| | | -| 1.36x| ``` --- kernel.rb | 2 +- object.c | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/kernel.rb b/kernel.rb index 888ef0c531d7e6..5d596c889258c9 100644 --- a/kernel.rb +++ b/kernel.rb @@ -17,7 +17,7 @@ module Kernel # def class Primitive.attr! :leaf - Primitive.cexpr! 'rb_obj_class(self)' + Primitive.cexpr! 'rb_obj_class_must(self)' end # diff --git a/object.c b/object.c index dc5ab2d934beb3..1ac14ec634612e 100644 --- a/object.c +++ b/object.c @@ -281,6 +281,7 @@ rb_obj_not_equal(VALUE obj1, VALUE obj2) static inline VALUE fake_class_p(VALUE klass) { + RUBY_ASSERT(klass); RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_MODULE) || RB_TYPE_P(klass, T_ICLASS)); STATIC_ASSERT(t_iclass_overlap_t_class, !(T_CLASS & T_ICLASS)); STATIC_ASSERT(t_iclass_overlap_t_module, !(T_MODULE & T_ICLASS)); @@ -307,6 +308,17 @@ rb_obj_class(VALUE obj) return cl; } +VALUE +rb_obj_class_must(VALUE obj) +{ + VALUE cl = CLASS_OF(obj); + RUBY_ASSERT(cl); + while (RB_UNLIKELY(fake_class_p(cl))) { + cl = RCLASS_SUPER(cl); + } + return cl; +} + /* * call-seq: * obj.singleton_class -> class From d89e73471db85f60ce45021f85487194124fe594 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 30 Aug 2025 13:32:44 +0200 Subject: [PATCH 4/5] object.c: refactor rb_obj_class and rb_class_real --- object.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/object.c b/object.c index 1ac14ec634612e..f2e307c5479737 100644 --- a/object.c +++ b/object.c @@ -289,11 +289,21 @@ fake_class_p(VALUE klass) return FL_TEST_RAW(klass, T_ICLASS | FL_SINGLETON); } +static inline VALUE +class_real(VALUE cl) +{ + RUBY_ASSERT(cl); + while (RB_UNLIKELY(fake_class_p(cl))) { + cl = RCLASS_SUPER(cl); + } + return cl; +} + VALUE rb_class_real(VALUE cl) { - while (RB_UNLIKELY(cl && fake_class_p(cl))) { - cl = RCLASS_SUPER(cl); + if (cl) { + cl = class_real(cl); } return cl; } @@ -302,8 +312,8 @@ VALUE rb_obj_class(VALUE obj) { VALUE cl = CLASS_OF(obj); - while (RB_UNLIKELY(cl && fake_class_p(cl))) { - cl = RCLASS_SUPER(cl); + if (cl) { + cl = class_real(cl); } return cl; } @@ -311,12 +321,7 @@ rb_obj_class(VALUE obj) VALUE rb_obj_class_must(VALUE obj) { - VALUE cl = CLASS_OF(obj); - RUBY_ASSERT(cl); - while (RB_UNLIKELY(fake_class_p(cl))) { - cl = RCLASS_SUPER(cl); - } - return cl; + return class_real(CLASS_OF(obj)); } /* From 395bda2fa1807fd28c9929d2059e404236e0a259 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 30 Aug 2025 14:17:14 +0200 Subject: [PATCH 5/5] object.c: make rb_obj_class_must static inline --- object.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object.c b/object.c index f2e307c5479737..6aa9c27f9dd876 100644 --- a/object.c +++ b/object.c @@ -318,7 +318,7 @@ rb_obj_class(VALUE obj) return cl; } -VALUE +static inline VALUE rb_obj_class_must(VALUE obj) { return class_real(CLASS_OF(obj));