ar_add_direct_i, added in 60a4894bdb (Dynamic ar_table size), inserts the
entries with ar_insert_direct and no write barrier. Unlike the other
hash_copy paths it can run Ruby code (ar_insert_direct may call #eql?) and
so trigger a GC part way through the copy. The new hash is then already
known to the GC with only the entries added so far, and the rest are never
barriered.
Found with wbcheck (WBCHECK_VERIFY_AFTER_WB=1) as an intermittent failure of
TestCoverage#test_branch_coverage_for_safe_method_invocation.
These still held String and Array lengths and indices in long, or cast them to long. rb_len_t is still long everywhere, so the generated code does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once rb_len_t widens this changes the `long *` output parameters of rb_range_beg_len(), rb_str_subpos(), rb_arithmetic_sequence_beg_len_step(), ruby_enc_find_basename() and ruby_enc_find_extname(), so extensions passing `long *` to them will need to be updated. The extension guides list these signatures verbatim and follow along.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extended Support for Solaris 9 ended in 2014. Solaris 10 and later have setenv and unsetenv, and libumem is always available, so the version specific fallbacks are no longer needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POSIX.1-2001 made unsetenv return int. The platforms that still declared it void, such as Mac OS X 10.4, FreeBSD 6 and glibc before 2.2.2, are no longer supported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Feature #22232]
Guaranteeing that any hash can be converted into an st_table is
limiting the evolution of RHash.
None of the public usages of `RHASH_TBL` were justified, they all
had equivalent `rb_hash_*` public APIs to perform the same thing
more efficiently without disabling write barriers nor coercing the
hash into an st_table.
include/ruby/win32.h already maps stat, lstat, access, truncate, utimensat, unlink, rename and getenv to the same rb_w32_* functions when compiling the core with RUBY_EXPORT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Previously we had a check that objects which were shareable, but not
frozen on a non-main Ractor would raise an exception if instance
variables were read. This prevented a non-main Ractor reading
non-shareable objects.
This changes the behaviour to forbid all writes to shareable objects
(even when unfrozen). This is done by re-purposing the frozen flag on
the object shape to reflect (FL_FROZEN || FL_SHAREABLE).
With IV writes forbidden on all shareable objects whether or not they
are frozen (with the exception of classes and modules), we can also
remove the check against reading instance variables, since no unsafe
instance variables can be set.
[Bug #22291]
`assert.h` always defines `RUBY_DEBUG`, as either 0 or 1, so `#ifdef` was always true and release builds paid for the two stores as well. They exist only so that the `RUBY_ASSERT` in `st_replace` and in `hash_copy` can tell an uninitialized table apart, and those compile away when `RUBY_DEBUG` is 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ar_entry hint may be initialized when it is used. It is caught by
Valgrind:
==91514== Conditional jump or move depends on uninitialised value(s)
==91514== at 0x1C5369: ar_find_entry_hint (hash.c:674)
==91514== by 0x1C85A5: ar_find_entry (hash.c:718)
==91514== by 0x1C85A5: ar_insert_direct.isra.0 (hash.c:1086)
==91514== by 0x1D24BE: ar_insert (hash.c:1114)
==91514== by 0x1D24BE: ar_bulk_insert (hash.c:5377)
==91514== by 0x1D24BE: rb_hash_bulk_insert.part.0 (hash.c:5392)
==91514== by 0x694891: pm_compile_hash_elements (prism_compile.c:1569)
==91514== by 0x69BBAB: pm_setup_args_core.isra.0 (prism_compile.c:1755)
==91514== by 0x69C682: pm_setup_args.constprop.0 (prism_compile.c:2146)
==91514== by 0x69F707: pm_compile_call (prism_compile.c:3821)
==91514== by 0x68906D: pm_compile_call_node (prism_compile.c:7527)
==91514== by 0x68AB4B: pm_compile_node (prism_compile.c:8893)
==91514== by 0x68CF71: pm_compile_node (prism_compile.c:10445)
==91514== by 0x6A8462: pm_compile_scope_node (prism_compile.c:7114)
==91514== by 0x68BB32: pm_compile_node (prism_compile.c:10306)
==91514== by 0x6A99F4: pm_iseq_compile_node (prism_compile.c:10627)
==91514== by 0x20C1DA: pm_iseq_build (iseq.c:1236)
==91514== by 0x689DCB: pm_new_child_iseq (prism_compile.c:1367)
==91514== by 0x689DCB: pm_compile_node (prism_compile.c:9173)
==91514== by 0x68CF49: pm_compile_node (prism_compile.c:10443)
==91514== by 0x6A7D2D: pm_compile_scope_node (prism_compile.c:7152)
==91514== by 0x68BB32: pm_compile_node (prism_compile.c:10306)
==91514== by 0x6A99F4: pm_iseq_compile_node (prism_compile.c:10627)
==91514== by 0x20C1DA: pm_iseq_build (iseq.c:1236)
==91514== by 0x68CB0C: pm_new_child_iseq (prism_compile.c:1367)
==91514== by 0x68CB0C: pm_compile_node (prism_compile.c:9972)
==91514== by 0x68CF49: pm_compile_node (prism_compile.c:10443)
==91514== by 0x6A7D2D: pm_compile_scope_node (prism_compile.c:7152)
==91514== by 0x68BB32: pm_compile_node (prism_compile.c:10306)
==91514== by 0x6A9A77: pm_iseq_compile_node (prism_compile.c:10617)
==91514== by 0x20C1DA: pm_iseq_build (iseq.c:1236)
==91514== by 0x20C3B3: pm_iseq_new_with_opt_try (iseq.c:1256)
==91514== by 0x18B336: rb_protect (eval.c:1154)
==91514== by 0x20A9D4: pm_iseq_new_with_opt (iseq.c:1284)
==91514== by 0x20A9D4: pm_iseq_new_top (iseq.c:1076)
==91514== by 0x212227: load_prism_parse (load.c:756)
==91514== by 0x18B513: rb_ec_ensure (eval.c:1176)
==91514== by 0x21524D: load_iseq_eval (load.c:799)
==91514== by 0x21524D: require_internal (load.c:1403)
==91514== by 0x2153C8: rb_require_string_internal (load.c:1513)
==91514== by 0x2153C8: rb_require_string (load.c:1499)
==91514== by 0x2153C8: rb_f_require (load.c:1089)
==91514== by 0x38E196: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3904)
==91514== by 0x395851: vm_call_symbol (vm_insnhelper.c:4403)
==91514== by 0x3A2C68: vm_sendish (vm_insnhelper.c:6283)
==91514== by 0x3A2C68: vm_exec_core (insns.def:912)
==91514== by 0x3929C9: rb_vm_exec (vm.c:2885)
==91514== by 0x214B88: load_iseq_eval (load.c:827)
==91514== by 0x214B88: require_internal (load.c:1403)
==91514== by 0x2153C8: rb_require_string_internal (load.c:1513)
==91514== by 0x2153C8: rb_require_string (load.c:1499)
==91514== by 0x2153C8: rb_f_require (load.c:1089)
==91514== by 0x38E196: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3904)
==91514== by 0x395851: vm_call_symbol (vm_insnhelper.c:4403)
==91514== by 0x3A2C68: vm_sendish (vm_insnhelper.c:6283)
==91514== by 0x3A2C68: vm_exec_core (insns.def:912)
==91514== by 0x3929C9: rb_vm_exec (vm.c:2885)
==91514== by 0x214B88: load_iseq_eval (load.c:827)
==91514== by 0x214B88: require_internal (load.c:1403)
==91514== by 0x2153C8: rb_require_string_internal (load.c:1513)
==91514== by 0x2153C8: rb_require_string (load.c:1499)
==91514== by 0x2153C8: rb_f_require (load.c:1089)
==91514== by 0x38E196: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3904)
==91514== by 0x395500: vm_call_alias (vm_insnhelper.c:4297)
==91514== by 0x3A2C68: vm_sendish (vm_insnhelper.c:6283)
==91514== by 0x3A2C68: vm_exec_core (insns.def:912)
==91514== by 0x3929C9: rb_vm_exec (vm.c:2885)
==91514== by 0x397967: vm_yield_with_cref (vm.c:1950)
==91514== by 0x397967: vm_yield (vm.c:1958)
==91514== by 0x397967: rb_ec_yield (vm_eval.c:1395)
==91514== by 0x60D03E: rb_ary_each (array.c:2792)
==91514== by 0x38E196: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3904)
==91514== by 0x3A3178: vm_sendish (vm_insnhelper.c:6283)
==91514== by 0x3A3178: vm_exec_core (insns.def:864)
==91514== by 0x392F22: vm_exec_loop (vm.c:2912)
==91514== by 0x392F22: rb_vm_exec (vm.c:2891)
==91514== by 0x397967: vm_yield_with_cref (vm.c:1950)
==91514== by 0x397967: vm_yield (vm.c:1958)
==91514== by 0x397967: rb_ec_yield (vm_eval.c:1395)
==91514== by 0x60D03E: rb_ary_each (array.c:2792)
==91514== by 0x38E196: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3904)
==91514== by 0x3A3178: vm_sendish (vm_insnhelper.c:6283)
==91514== by 0x3A3178: vm_exec_core (insns.def:864)
==91514== by 0x392F22: vm_exec_loop (vm.c:2912)
==91514== by 0x392F22: rb_vm_exec (vm.c:2891)
==91514== by 0x1874F2: rb_ec_exec_node (eval.c:299)
==91514== by 0x18A208: ruby_run_node (eval.c:337)
==91514== by 0x184525: rb_main (main.c:42)
==91514== by 0x184525: main (main.c:62)
This was previously flagged by ubsan because the AR_HINT_BASE_MASK
constant is implicitly typed as long:
hash.c:664:43: runtime error: signed integer overflow: 72340172838076673
* 131 cannot be represented in type 'long'
Notably, in an unsigned context, this line can't overflow
(AR_HINT_BASE_MASK is 0x0101..0101). VALUE is always an unsigned type.
This commit adds support for using AR tables for identity hashes (hashes
created by method Hash#compare_by_identity). Before this change, identity
hashes always used ST tables. However, this used a lot more memory because
ST tables are quite large. This commit adds a new flag RHASH_COMPARE_BY_IDENTITY
for AR tables that are compare-by-identity.
For example, for an empty identity hash, the size goes from 256 bytes to
160 bytes:
require "objspace"
h = {}.compare_by_identity
puts ObjectSpace.memsize_of(h)
`hash_copy` will call `st_init_existing_table_with_size` which
allocates and may trigger GC causing the current hash to be marked.
So we need to zero-out the `st_table` to avoid a segfault.
Instead of always allocating `ar_table` backed hashes in 160B
slots for them to have capacity of `RHASH_AR_TABLE_MAX_SIZE` (`8`),
they can now be allocated in any slot size between `64B` and `160B`.
`64B` is the minimum slot size that still allows transitioning to
an `st_table` if necessary.
The assumption is that there is a large number of literal hashes
and keyword argument hashes being built, and the overwhelming majority
of them aren't appended to.
When this assumption hold, this patch saves memory and reduce GC
pressure by allocating in a smaller slot than before.
However when the assumption doesn't hold, it has the oppositve effect
as the Hash will transition to an `st_table` when before it may have
stayed an `ar_table`.
Currently the main blocked for this change is that the compiler
has a tendency to generate code that make the final Hash size unknown.
For instance: `{a: 4, b: 4 + 1}` is compiled as:
```
0000 duphash {a: 4} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 putobject :b
0007 putobject 4
0009 putobject_INT2FIX_1_
0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>
```
Here `duphash` has no information about how large of a hash it needs to allocate.
For this patch to be more efficient the compiler should generate something like:
```
0000 putobject {a: 4} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 putobject :b
0007 putobject 4
0009 putobject_INT2FIX_1_
0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>
```
So that it's `hash_merge_ptr` that is responsible for allocating the final hash,
given it does have a much better clue of how large it would need to be.
Demo:
```ruby
require 'objspace'
def size(hash)
puts "size: #{hash.size}\tmemsize: #{ObjectSpace.memsize_of(hash)}"
end
size({})
size(a:1)
size(a:1, b:2)
size(a:1, b:2, c:3)
size(a:1, b:2, c:3, d:4)
size(a:1, b:2, c:3, d:4, e:5)
size(a:1, b:2, c:3, d:4, e:5, f:6)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9)
```
Before:
```
size: 0 memsize: 160
size: 1 memsize: 160
size: 2 memsize: 160
size: 3 memsize: 160
size: 4 memsize: 160
size: 5 memsize: 160
size: 6 memsize: 160
size: 7 memsize: 160
size: 8 memsize: 160
size: 9 memsize: 464
```
After:
```
size: 0 memsize: 64
size: 1 memsize: 64
size: 2 memsize: 64
size: 3 memsize: 80
size: 4 memsize: 96
size: 5 memsize: 128
size: 6 memsize: 128
size: 7 memsize: 160
size: 8 memsize: 160
size: 9 memsize: 448
```
With gcc 15.2.0 on aarch64 Ubuntu 26.4:
```console
hash.c: In function ‘rb_hash_s_create’:
hash.c:1982:20: warning: ‘hash’ may be used uninitialized [-Wmaybe-uninitialized]
1982 | return hash;
| ^~~~
hash.c:1920:11: note: ‘hash’ was declared here
1920 | VALUE hash, tmp;
| ^~~~
```
Rather than start from a mutable hash created with `newhash` or
`duphash`, and then iteratively insert into it, it's preferable to
start from a constant hidden hash, so that we can directly allocate
the final hash with the right size.
For instance:
```ruby
{a: 1, B => 2, c: 3}
```
Before:
```
0000 duphash {a: 1} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 opt_getconstant_path <ic:0 B>
0007 putobject 2
0009 putobject :c
0011 putobject 3
0013 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:5, ARGS_SIMPLE>
```
After:
```
0000 putobject {a: 1} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 opt_getconstant_path <ic:0 B>
0007 putobject 2
0009 putobject :c
0011 putobject 3
0013 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:5, ARGS_SIMPLE>
```
Since, `duphash` has no idea how large the final hash might be,
it can't right-size the hash it allocates.
Hence it's preferable to use a `putobject` and let `hash_merge_ptr`
compute the final hash right size by assuming all keys are unique.
`st_replace` assumes that the `st_table` hasn't been
initialized, so when duping a hash, we shouldn't pre-allocate
the `st_table` otherwise we'll leak memory.
The pattern of allocating an empty hash with `rb_hash_new()` and
then appending to it is relatively wasteful, because every time
we go over the hash capacity, we'll have to `realloc`, rehash etc.
It may even get worse if https://github.com/ruby/ruby/pull/18345
lands, as right now an empty hash starts with a capacity of 8,
but it may very well start with a capacity of 2.
Hence in cases where the final hash size is easy to predict, we
should use `rb_hash_new_capa()`.
Instead of iterating over the bytes of the ar_table hints
one by one, we can use a little bit of SWAR code to match them
all at once and return the first matching byte index.
In theory we could even re-use the mask to extract all matching
position, however we can't do that because since `ar_equal` calls
back into Ruby, the hints may change after every iteration.
```
compare-ruby: ruby 4.1.0dev (2026-08-06T14:05:57Z master 9658933451) +YJIT +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-08-06T16:42:50Z ar-find-entry-swar-2 723ff01514) +YJIT +PRISM [arm64-darwin25]
```
| |compare-ruby|built-ruby|
|:-----------|-----------:|---------:|
|hit_first | 13.891M| 15.085M|
| | -| 1.09x|
|hit_third | 13.322M| 15.065M|
| | -| 1.13x|
|hit_fourth | 12.759M| 14.410M|
| | -| 1.13x|
|hit_last | 11.545M| 15.275M|
| | -| 1.32x|
|miss | 10.872M| 16.286M|
| | -| 1.50x|
Implements GC fastpath for HashDup instruction for AR hashes (<= 8 elements).
This instruction is used for non-empty hashes that have only compile-time
constants.
Benchmark:
def run(times)
i = 0
while i < times
a = {a: 1, b: 2, c: 3}
i += 1
end
end
30.times { run(1) }
run(10_000_000)
Results:
Benchmark 1: master
Time (mean ± σ): 145.2 ms ± 2.8 ms [User: 136.9 ms, System: 6.6 ms]
Range (min … max): 141.1 ms … 152.3 ms 20 runs
Benchmark 2: branch
Time (mean ± σ): 82.1 ms ± 1.4 ms [User: 74.6 ms, System: 6.0 ms]
Range (min … max): 80.1 ms … 86.9 ms 35 runs
Summary
branch ran
1.77 ± 0.05 times faster than master
This commit centralizes shape setting for objects in the ZJIT GC fastpath
into the rb_zjit_new_obj_shape function to avoid each type having to deal
with shapes individually.
Add documentation-only deprecation notices to Module#ruby2_keywords,
main.ruby2_keywords, Proc#ruby2_keywords, Hash.ruby2_keywords_hash?,
and Hash.ruby2_keywords_hash, as the first phase of the schedule
proposed at https://bugs.ruby-lang.org/issues/22205.
If the shape capacity isn't set on an object, one thing that fails is
moving it across Ractors. The new object created in the dst Ractor needs
to be big enough to embed the contents if the src was embedded.
The Hash#[] and Array#product examples referenced undefined variables,
the StringIO doc named a nonexistent File::RDRW constant, and
Pathname#mkpath's call-seq advertised a positional permissions
argument that the keyword-only implementation does not accept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This extends the ZJIT GC fast path to the newhash instruction for empty
hashes.
This was verified in a stripped down benchmarking environment, using the
following micro-benchmark in ruby-bench:
```
def run(max)
i = 0
while i < max
h = {}
i += 1
end
end
require_relative '../harness/loader.rb'
run_benchmark(50) do
50.times do
run(500_000)
end
end
```
This patch resulted in a ~30% speedup.
```
master: ruby 4.1.0dev (2026-07-07T14:04:07Z master c51b1596c0) +ZJIT +PRISM [x86_64-linux]
experiment: ruby 4.1.0dev (2026-07-07T19:27:04Z mvh-zjit-inline-fa.. 60730f1aa1) +ZJIT +PRISM [x86_64-linux]
----- ------------ --------------- ------------------ -----------------
bench master (ms) experiment (ms) experiment 1st itr master/experiment
hash 682.3 ± 0.7% 518.0 ± 1.0% 1.082 1.317
----- ------------ --------------- ------------------ -----------------
Legend:
- experiment 1st itr: ratio of master/experiment time for the first benchmarking iteration.
- master/experiment: ratio of master/experiment time. Higher is better for experiment. Above 1 represents a speedup.
```