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>
With --with-static-linked-ext, the archives, libraries and Init
functions of the extensions are known only to exts.mk, and the
top-level $(PROGRAM) rule linked ruby with a bare EXTOBJS whenever
its prerequisites changed. The sub-make from exts.mk relinked it
correctly afterwards only because it happened to come later, and
because configuring ext/-test- deletes ext/extinit.c. With `make -j`
under GNU make 3.81, or 4.4 with `DOT_WAIT=`, `programs` and
`test-precheck` ran that link and the exts sub-make at once, leaving
a ruby without any extension or encoding, or no ruby at all when the
two links collided in codesign. A bare `make ruby` gave the same
broken ruby even serially.
Now the top-level $(PROGRAM) depends on `exts` instead of linking
when PROGRAM_EXTS is set, and only the sub-make, which passes
PROGRAM_EXTS empty along with the real EXTOBJS, links. EXTOBJS at
the top level is always dmyext.o so that ext/extinit.c is generated
only with the full EXTINITS. `.WAIT` stays in the test targets; it
still keeps the outputs of the dynamic build from interleaving, but
the correctness of the static build no longer depends on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`update-src` truncated revision.h to force the regeneration, but the
blank file also defeated the guard in file2lastrev.rb that keeps the
existing content when no VCS is available, so `make up` on a released
tarball dropped `RUBY_REVISION` and `RUBY_FULL_REVISION`. Removing the
timestamp alone is enough to force the regeneration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.deps/depend` depends on `$(REVISION_H)`, so make makes it while
remaking the included makefiles, before `update-src` removes it. As
make does not make a target twice in a run, `after-update` kept using
the emptied `revision.h` and `make up` failed to compile version.c with
undefined `RUBY_RELEASE_MONTH`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
$(LIBS) pulls in $(EXTLIBS), whose library search paths reach the link
line through $(XLDFLAGS). With --with-static-linked-ext and
--with-openssl-dir this link failed with `ld: library 'ssl' not found`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nmake does not join a `{$(VPATH)}target` line and a bare `target` line
into a single node, so the prerequisites declared on the braced lines
never fired. An out-of-place mswin build kept using a probes.dmyh
generated before gc__xcalloc was added to probes.d and gc.c failed to
compile with RUBY_DTRACE_GC_XCALLOC undefined. parse.c had the same
problem with its id.h prerequisite. GNU make is unaffected since
tool/prereq.status strips the braces for uncommon.mk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simplecov 1.1 ships the HTML and JSON formatters and has no runtime
dependencies, so the globs for simplecov-html, simplecov_json_formatter
and docile never match anything. Pin `make update-coverage` to the same
`~> 1.1` the bundler dev gems now use, so a `.bundle` left over with 0.x,
which does need docile on the load path, gets replaced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up on dd93d13c8a (#18071).
Baking revision.h into dump_ast couples every builtin *.rbinc to the
current commit: whenever HEAD changes, e.g. when switching between two
branches that only differ under yjit/, revision.h is regenerated,
dump_ast is relinked, all $(BUILTIN_RB_INCS) are regenerated with
identical content but fresh timestamps, and every C file that includes
one of them is recompiled, followed by relinking miniruby and ruby.
The --version banner was the only use of revision.h in dump_ast, so
print only the Prism version. Since dump_ast no longer includes revision.h,
it is not relinked when HEAD changes, so $(BUILTIN_RB_INCS) and their
dependents are no longer invalidated by commit-only changes.
Replace `-s` globals with explicit command options and standard long
option names. Group source selection under `--scope` and represent
output behavior as a single mode so command-line precedence is clear.
Keep command-line parsing and option dispatch inside `Mkdepend`, and
return command success directly from `Mkdepend.main`.
Generate only core and encoding dependencies during configure.
Generate extension dependency files from `configure-ext.mk` after
`miniruby` is available, so `THREAD_MODEL` can be resolved before
rules enter an extension Makefile.
Select the extension scope when `-extensions` is passed. Keep quoted
project-internal headers ahead of similarly named public headers while
scanning extension sources. Limit snapshot expansion to files needed
before `miniruby`.
Replace `tool/update-deps` with a Ruby source scanner so dependency
updates no longer require GNU make, a configured build, or compiler
preprocessor output.
Keep compact source mappings and scanner declarations in dependency
files, and expand complete rules into the build directory. Avoid the
temporary fixture headers required by the compiler-driven prototype.
Use the generated rules from GNU make, BSD make, and NMake builds.
Support out-of-tree and read-only source trees, and retain expanded
dependencies shipped in release archives when baseruby is unavailable.
If we use out-of-source build and have `${BUILD_DIR}/run.gdb` (by
`make run.gdb`), `${SOURCE_DIR}/.gdbinit` is loaded twice because
`run.gdb` loads `${SOURCE_DIR}/.gdbinit`.
If we have `run.gdb`, we don't need `-x ${SOURCE_DIR}/.gdbinit`.
There is one more problem for `${BUILD_DIR}/run.gdb` and
`${SOURCE_DIR}/.gdbinit`. `source -s misc/gdb.py` in
`${SOURCE_DIR}/.gdbinit` is failed when we use out-of-source build and
`${BUILD_DIR}/run.gdb` because `${SOURCE_DIR}` isn't included in
search path of `source`. If we have `directory $(srcdir)` in
`${BUILD_DIR}/run.gdb`, `source -s misc/gdb.py` works in
`${SOURCE_DIR}/.gdbinit` that is loaded from `${BUILD_DIR}/run.gdb`.
net-imap's released test helper still calls the pre-1.0.0
`SimpleCov.formatters=` API, which raises NoMethodError with simplecov
1.0.0. Bundled gem coverage is not collected in CI, so opt out via
SIMPLECOV_DISABLE and drop simplecov from the test gem installation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `benchmark` recipe collected files with `$(find ... | sort)`,
which cmd.exe cannot interpret, so `nmake benchmark` failed with
`'sort)' is not recognized`. Select the files with Ruby instead so
the recipe works regardless of the shell, and pass the executables
with forward slashes for benchmark-driver's Shellwords splitting.
This commit implements support in ZJIT for inlining GC fast path. The GC
fast path is defined in gc_fastpath.rs for both the default GC and MMTk.
For the default GC, the fast path uses the bump pointer allocator that
was implemented in https://github.com/ruby/ruby/pull/17201. If the bump
pointer region is full, it will bail out and fall back to the slow path.
This GC fast path is used by the newarray instruction in ZJIT in the empty
array case. We can see significant improvements in the following microbenchmark,
which allocates 100 million empty arrays:
def run(max)
i = 0
while i < max
a = []
i += 1
end
end
30.times { run(2) }
run(100_000_000)
This microbenchmark runs 1.5x faster on my machine:
Benchmark 1: master
Time (mean ± σ): 583.3 ms ± 10.1 ms [User: 569.3 ms, System: 12.0 ms]
Range (min … max): 571.9 ms … 600.0 ms 10 runs
Benchmark 2: branch
Time (mean ± σ): 389.5 ms ± 8.2 ms [User: 375.0 ms, System: 12.1 ms]
Range (min … max): 380.2 ms … 405.3 ms 10 runs
Summary
branch ran
1.50 ± 0.04 times faster than master
* Group test-bundler-parallel workers by recorded runtime
turbo_tests balances workers by a recorded runtime log only when invoked
as a bare `spec`; this build passes an absolute spec/bundler path, so it
grouped by file size and let the single heaviest file dominate the
wall-clock. A tool/ patch loaded from the recipe records per-file
runtimes and reorders the size-based grouper to use the previous run's
data, leaving the synced spec/bin/parallel_rspec untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix stale reference in runtime recorder comment
The launcher script it pointed at was replaced by the recipe calling
BundlerRuntimeGrouping.install! directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rescue LoadError when installing runtime grouping
install! is meant to degrade to filesize grouping on any failure, but
LoadError from the parallel_tests require is a ScriptError and escaped
the StandardError rescue, killing the whole recipe instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The recipe moved into common.mk kept the literal `-Ispec/syntax_suggest:spec/lib`
from template/Makefile.in. On Windows Ruby treats `:` as an invalid path
separator (the drive-letter delimiter), so spec_helper fails to load and the
mswin run errors out instead of running the SyntaxSuggest spec. Use
$(PATH_SEPARATOR) (`;` on mswin, `:` on Unix), matching RAKER just below, so the
single recipe works for both builds. Verified 170 examples, 0 failures on the
VS 2022 mswin build and the msys2 UCRT64 GNU make build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
template/Makefile.in is used only by the GNU make build, so on mswin the
common.mk stub was an empty rule and nmake check silently skipped the
SyntaxSuggest spec. Move the recipe to common.mk so both builds share it,
matching how test-bundled-gems-spec is already shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Get rid of the CFUNC overhead in String#ascii_only? and #valid_encoding?. Define them as leaf Primitive.cexpr! builtins in a new string.rb, following the pattern of numeric.rb.
This makes them 1.2x-1.4x faster across the interpreter, YJIT, and
ZJIT.
Move ZJIT's cfunc annotation for String#ascii_only? to annotate_builtin!, and add one for String#valid_encoding?.
Fix CI for builtin String#ascii_only?
If `load-relative` is specified, the system uses `dladdr` to locate
the executable file path when initializing the load path; however, on
some platforms (at least OpenBSD), `dladdr` fails under certain
conditions (such as when debug information is missing). In that case,
the system falls back to using `argv[0]`; however, if this is a
relative path, the executable cannot be found after changing the
working directory with the `-C` option, resulting in an error.
When Ruby is built with `--with-modular-gc`, the default GC module is
built with `BUILDING_MODULAR_GC` defined.
The existing code stubbed out `probes.h` and `RUBY_DTRACE_GC_HOOK` in
this build path, so any DTrace script attached against the modular
default GC saw zero GC events.
This PR enables the GC probes in the modular GC library so they fire
when Ruby is built with `--enable-dtrace --with-modular-gc`
To test: build with `./configure --enable-dtrace
--with-modular-gc=/tmp/modgc`, then `make install-modular-gc
MODULAR_GC=default`.
Probes are present in the shared library:
```console
$ sudo bpftrace -l 'usdt:/tmp/modgc/librubygc.default.so:ruby:*'
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__begin
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__end
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__begin
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__end
```
They fire under a GC-heavy workload (`test.rb` contains `100000.times { Object.new }; GC.start`):
```console
$ sudo bpftrace -e '
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__begin { @mark_begin++ }
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__end { @mark_end++ }
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__begin { @sweep_begin++ }
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__end { @sweep_end++ }
' -c 'env RUBY_GC_LIBRARY=default ./miniruby --disable=gems test.rb'
@mark_begin: 2
@mark_end: 2
@sweep_begin: 18
@sweep_end: 18
```