mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Closes: https://github.com/Shopify/ruby/issues/884
Inline `BasicObject#!=` by using a ZJIT hook. This expands specialization coverage to additional cases beyond the existing String and Integer specializations.
Although this might add some overhead in the interpreter when ZJIT is enabled, as the `opt_neq` fast path requires the C definition, the benchmarks below show speedups in several cases (e.g. lobsters, railsbench). I ran the benchmarks several times. The speedups varied between runs, but I consistently observed improvements over the previous implementation.
```
zjit-master: ruby 4.1.0dev (2026-10-10T08:21:06Z master 05917fd1e6) +ZJIT stats +PRISM [arm64-darwin25]
zjit-basicobject-hook: ruby 4.1.0dev (2026-10-10T13:30:56Z zjit-basicobject-n.. a6f95cadaa) +ZJIT stats +PRISM [arm64-darwin25]
ruby-bench: 3cc371202311fe2c821a6adb5adfced364f029de
------------- ---------------- -------------------------- ----------------------------- ---------------------------------
bench zjit-master (ms) zjit-basicobject-hook (ms) zjit-basicobject-hook 1st itr zjit-master/zjit-basicobject-hook
liquid-render 44.6 ± 10.2% 43.9 ± 6.3% 0.933 1.014
lobsters 415.1 ± 6.7% 384.1 ± 4.7% 1.070 1.081
railsbench 592.1 ± 5.8% 545.8 ± 2.0% 1.117 1.085
------------- ---------------- -------------------------- ----------------------------- ---------------------------------
```
---
<details>
<summary>Resolved: specialization with statically known receiver types</summary>
Note: Currently, the `==` call inside `BasicObject#!=` uses a profile shared across callers. When that profile is polymorphic, `==` can fall back to a dynamic send even when the receiver's exact type is known in the caller.
As a follow-up, we could improve specialization of `==` inside the inlined `!=` by using receiver types known at the call site, even when the shared profile for `==` is polymorphic.
```rb
2.times do
"x" != nil
1 != nil
end
NULL = Object.new
def test(value) = NULL != value
4.times { test(nil) }
```
```sh
# ./miniruby --zjit-dump-hir --zjit-call-threshold=4 --zjit-num-profiles=4 ../test.rb
```
On master:
```
v30:ObjectExact[VALUE(...)] = Const Value(VALUE(...))
PatchPoint MethodRedefined(Object@..., !=@..., cme:...)
PatchPoint MethodRedefined(Object@..., ==@..., cme:...)
v51:CBool = IsBitNotEqual v30, v11
v52:BoolExact = BoxBool v51
```
On current branch:
```
v30:ObjectExact[VALUE(...)] = Const Value(VALUE(...))
PatchPoint NoSingletonClass(Object@...)
PatchPoint MethodRedefined(Object@..., !=@..., cme:...)
PushInlineFrame :!=, v30 (...), num_args=1
v68:BasicObject = Send v30, :==, v11 # SendFallbackReason: Send: polymorphic fallback
v71:CBool = Test v68
CondBranch v71, bb14(), bb8()
// Removing the initial 2.times block lets the current branch specialize the comparison to IsBitEqual.
```
</details>
18 lines
558 B
Ruby
18 lines
558 B
Ruby
# ZJIT uses a Ruby definition of BasicObject#!= so it can inline the call to #==.
|
|
# Register it only for ZJIT, leaving the C definition in place otherwise.
|
|
class BasicObject
|
|
with_zjit do
|
|
if Primitive.rb_builtin_basic_definition_p(:!=)
|
|
undef :!=
|
|
|
|
def !=(other) # :nodoc:
|
|
# Keep the replacement classified as a basic method like the C definition.
|
|
Primitive.attr! :c_trace
|
|
|
|
# Negate truthiness without calling a possibly redefined #!.
|
|
Primitive.rb_builtin_bool_negate(self == other)
|
|
end
|
|
end
|
|
end
|
|
end
|