516 Commits
Author SHA1 Message Date
Erik Berlin 224726a9f5 Skip rb_eql() for identical elements in Array#eql?
Array#== walks both element buffers and only calls rb_equal() for a
pair whose VALUEs differ. Array#eql? called rb_ary_elt() twice and
rb_eql() once for every element, even though rb_eql() answers true
for identical VALUEs before doing anything else. For arrays of
Fixnums, Symbols, and nil, which are identical whenever they are
equal, that made eql? several times slower than ==.

recursive_eql() now walks the buffers the way recursive_equal() does.
A pair that is not identical still goes through rb_eql(), which can
run user code that resizes either array, so the length and both
buffers are re-read after every such call. Arrays whose lengths came
apart compare unequal, as they do under ==. Before, the shorter one
read as nil past its end, so the result depended on the remaining
elements of the longer one.

The new tests cover an element whose eql? empties both arrays,
shortens one of them, or grows both past their original capacity
mid-compare.
2026-10-16 10:09:20 +02:00
Nobuyoshi Nakada 74856fbfb6 Use identity sets to track unspecified keyword arguments
Keyword tracking only needs membership, so replace identity hashes
with identity sets to avoid storing unused values.  Add `rb_set_p`
for the type assertion in `vm_check_keyword`.

Add benchmarks for 31, 32 and 128 keyword parameters with sparse and
dense dynamic defaults, using both direct keywords and keyword splats.
The 31-parameter cases provide a bitmask-only control.

Local arm64 macOS measurements show about 16-26% higher throughput
with many omitted dynamic defaults at 32 and 128 parameters.  Smaller
differences remain noisy, including in the bitmask-only controls.

The tracking container is created at call time, leaving IBF and
`ISeq#to_a` unchanged.  Verify IBF-loaded calls with the Integer
refinement and colliding keyword indices from [Bug #22335].
2026-09-23 21:36:41 +09:00
Guillaume Malette fc04a83092 YJIT: Inline Struct member writes in gen_struct_aset (GH-17477) 2026-09-11 12:41:32 -04:00
Jean Boussier 6acfd9c2b9 string.c: Speedup String#tr fastpath with SIMD
The fast path is now 4.4x faster, now making it ~2.6x times
faster than `CGI.escape_html`, and 26x faster than an equivalent
`gsub`.

```
compare-ruby: ruby 4.1.0dev (2026-09-06T11:22:48Z master 13fc4ffd32) +YJIT +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-09-06T14:39:43Z string-tr-simd e2526ca9ba) +YJIT +PRISM [arm64-darwin25]
``

|             |compare-ruby|built-ruby|
|:------------|-----------:|---------:|
|tr_escape    |     41.595k|  182.732k|
|             |           -|     4.39x|
|gsub_escape  |      7.135k|    6.977k|
|             |       1.02x|         -|
|cgi_escape   |     70.117k|   70.179k|
|             |           -|     1.00x|

However, contrary to a lookup table, here the performance is linear with
the number of characters to match, hence once we're looking for 16 distinct
bytes, the gain compared to the lookup table is down to ~2x.
2026-09-06 20:47:32 +02:00
Jean Boussier 522f6ea227 String#tr: take a Hash for multi-character replacements
[Feature #22238]
2026-09-03 13:11:59 +02:00
Roman SamoilovandCodex f20155d53c marshal.c: Make partial objects table conditional
Only allocate and use the partial objects table when a load proc is active or loaded objects must be frozen.

Co-authored-by: Codex <noreply@openai.com>
2026-08-30 08:50:45 +02:00
Jean Boussier 37325e9f7a Array#flatten: optimize the common *args.flatten pattern
A common Ruby idiom for variadic methods is to flatten the argument
list so that it can be called with either variadic arguments or an
Array.

e.g. from the sqlite3 gem:

```ruby
def bind_params(*bind_vars)
  bind_vars.flatten.each do |var|
  # ...
```

However, as soon as `flatten` encounter another array, it has to
protect against recursion, which requires allocating an expensive
identity Hash.
So this pattern has a relatively high cost when called with
a single array argument (e.g. `bind_params [1, 2, 3]`).

We can specialize for that common case without noticeably
impacting other usages of `Array#flatten`:

|                              |compare-ruby|built-ruby|
|:-----------------------------|-----------:|---------:|
|small_flat_ary.flatten        |      7.547M|    7.974M|
|                              |           -|     1.06x|
|small_flat_ary.flatten!       |      6.031M|    6.105M|
|                              |           -|     1.01x|
|large_flat_ary.flatten        |    465.896k|  485.861k|
|                              |           -|     1.04x|
|large_flat_ary.flatten!       |    455.872k|  478.103k|
|                              |           -|     1.05x|
|small_pairs_ary.flatten       |      1.327M|    1.407M|
|                              |           -|     1.06x|
|small_pairs_ary.flatten!      |      1.165M|    1.153M|
|                              |       1.01x|         -|
|large_pairs_ary.flatten       |     96.612k|   96.976k|
|                              |           -|     1.00x|
|large_pairs_ary.flatten!      |     94.108k|   97.289k|
|                              |           -|     1.03x|
|mostly_flat_ary.flatten       |    399.648k|  417.327k|
|                              |           -|     1.04x|
|mostly_flat_ary.flatten!      |    378.315k|  395.946k|
|                              |           -|     1.05x|
|small_nested_ary.flatten      |      2.513M|    7.788M|
|                              |           -|     3.10x|
|small_nested_ary.flatten!     |      2.024M|    5.914M|
|                              |           -|     2.92x|
|large_nested_ary.flatten      |    341.635k|  485.319k|
|                              |           -|     1.42x|
|large_nested_ary.flatten!     |    329.837k|  475.014k|
|                              |           -|     1.44x|
|small_nested_ary.flatten(1)   |      9.497M|   61.350M|
|                              |           -|     6.46x|
|small_nested_ary.flatten!(1)  |      5.136M|   17.668M|
|                              |           -|     3.44x|
|large_nested_ary.flatten(1)   |      1.575M|   60.241M|
|                              |           -|    38.26x|
|large_nested_ary.flatten!(1)  |      1.369M|   16.835M|
|                              |           -|    12.30x|
2026-08-22 22:13:33 +02:00
Jean Boussier 7da70f1773 hash.c: speedup ar_find_entry_hint with SWAR
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|
2026-08-14 11:45:14 +02:00
Hiroshi SHIBATA c2243efe69 Make nmake benchmark work on mswin
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.
2026-07-13 15:04:42 +09:00
Jean Boussier 3d350d6670 Use rb_obj_shape_slot_size over rb_gc_obj_slot_size
`rb_gc_obj_slot_size` has to check the page metadata to
get the slot size, which likely cause cache misses.

`rb_obj_shape_slot_size` does simple arithmetic using
only a few shape bits.

```
compare-ruby: ruby 4.1.0dev (2026-07-08T03:48:16Z master 01c5b2e890) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-07-08T10:17:41Z gc-changed-slot-size 08029b0f7c) +PRISM [arm64-darwin25]
warming up.
```

|                        |compare-ruby|built-ruby|
|:-----------------------|-----------:|---------:|
|binary_concat_embedded  |      5.262M|    5.707M|
|                        |           -|     1.08x|
2026-07-08 13:39:59 +02:00
Sampo Kuokkanen 439c06b479 Annotate Symbol#empty? as leaf (#17464)
Move `Symbol#empty?` from the C `sym_empty` into `symbol.rb`, implemented as `RBOOL(RSTRING_LEN(rb_sym2str(self)) == 0)`, mirroring the existing Symbol#to_s and #name leaf builtins.

Also added an `annotate_builtin! ` in `zjit/src/cruby_methods.rs` to tell ZJIT that the method does not need GC now. 

`Symbol#empty?` as a leaf builtin, via `benchmark-driver` (`GC.disable`, `loop_count: 20_000_000`, Apple M1). Speedup vs master (CFUNC `sym_empty`):

| benchmark | interpreter | `--yjit` | `--zjit` |
|---|---:|---:|---:|
| `:"".empty?`       | **1.24x** | **1.30x** | **1.19x** |
| `:abcdefgh.empty?` | **1.19x** | **1.19x** | **1.17x** |

Windows native and WSL2 also get faster:

| benchmark | interpreter |
|---|---:|
| `:"".empty?`       | **1.47x** |
| `:abcdefgh.empty?` | **1.36x** |

### WSL2 (Ubuntu) `x86_64-linux`

| benchmark | interpreter | `--yjit` | `--zjit` |
|---|---:|---:|---:|
| `:"".empty?`       | **1.39x** | **1.33x** | **1.32x** |
| `:abcdefgh.empty?` | **1.37x** | **1.39x** | **1.33x** |
2026-07-02 04:26:34 +00:00
Sampo Kuokkanen 93e977936f Define String#ascii_only? and #valid_encoding? in Ruby
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?
2026-06-24 17:17:33 -04:00
Sampo Kuokkanen 13fe77dd2b Add an ASCII fast path for String#casecmp?
String#casecmp? folds both operands with a Unicode case fold, allocating two new strings, and compares the results. For ASCII-only strings, Unicode case folding is equivalent to ASCII case-insensitive comparison, so the operands can be compared byte-by-byte without allocating anything.
2026-06-23 22:29:36 +09:00
Sampo Kuokkanen 217dd3d1fb Improve performance of String#capitalize with a single-byte ASCII fast path
#upcase/#downcase already case-map ASCII strings with a plain byte
loop, but #capitalize/#capitalize! always went through full Unicode
case-mapping, making them ~10x slower on the same input. Add
capitalize_single (mirroring upcase_single/downcase_single), taken
whenever case_option_single_p() holds: it titlecases the first byte and
downcases the rest, touching ASCII letters only.
2026-06-20 16:14:45 +09:00
Fletcher Dares 51d4ad5f9a Fast path ASCII-8BIT case mapping 2026-06-19 16:56:31 +09:00
Yaroslav Markin 35b7652058 Speed up Array#join with a memcpy fast path
When every element is a String sharing one ASCII-compatible fast-path
encoding (UTF-8/US-ASCII/ASCII-8BIT) and the separator is compatible, build
the result with a single memcpy pass instead of appending element by element
through rb_str_buf_append. Other cases fall back to the existing path.
2026-06-11 20:59:01 +02:00
Mari Imaizumi b7aa15f594 Improve performance of String#inspect with an ASCII bulk-skip fast path
Bulk-skip ASCII bytes that need no escaping via a 256-byte lookup
table, avoiding per-byte `rb_enc_precise_mbclen` and `rb_enc_mbc_to_codepoint`
calls. Eligible for well-formed strings (CR=7BIT, or UTF-8 VALID);
other strings fall through to the existing path unchanged.
2026-06-05 12:33:15 +09:00
FletcherDares 056ae1f926 Centralize ISASCII early exit for search_nonascii (#17166) 2026-06-02 13:45:55 +09:00
FletcherDares effac0a485 Improve performance of String#scrub by skipping ASCII runs with search_nonascii (#16359)
Improve performance of String#scrub by skipping ascii characters after finding a multi byte character using search_nonascii. Create benchmark for String#scrub to show performance difference made by including the new ascii skips.
2026-05-26 13:52:41 +09:00
FletcherDares a6c4e98cbd Improve performance of String#codepoints with ASCII hot path in multi-byte strings. (#16991) 2026-05-23 08:16:43 +09:00
Fletcher Dares 6a211f21e8 perf: Avoid eager gsub! copy on no match 2026-05-14 08:46:20 -07:00
John Hawthorn bea03284ec Stack-allocate onig region for match 2026-05-14 07:07:45 -07:00
Nobuyoshi Nakada 6f8ece0690 pathname: Add simple benchmarks 2026-05-13 17:37:48 +09:00
Chris Hasińskiandtomoya ishida 4a0072d5f2 Speed up Integer#to_s with a two digit lookup table (#16719)
* numeric: emit two decimal digits per iteration in rb_fix2str

Replace the digit-at-a-time loop in rb_fix2str with the standard
itoa 2-digit lookup table for base 10.  Each iteration now
writes two digits using a single (u % 100, u / 100) pair, so the
number of loop iterations is halved for multi-digit integers.
The classic per-digit loop is kept for non-base-10 conversion.

Benchmark (Apple M-series, 5M-10M ops, best of 3 runs):

  case            base       patch      delta
  ---------       -----      -----      -----
  1-digit   (5)   64 ns/op   64 ns/op    -0%
  2-digit   (42)  64 ns/op   65 ns/op    +2%  (noise)
  3-digit   (400) 66 ns/op   64 ns/op    -3%
  5-digit   (12345)          69 ns/op   67 ns/op    -3%
  10-digit  (1234567890)     77 ns/op   67 ns/op   -13%
  19-digit  (2^62-1)        111 ns/op   75 ns/op   -33%

The crossover is at ~3 digits: below that the constant setup
dominates and the benefit is within noise, above that the halved
iteration count shows up linearly.  Typical Rails payloads mix
short IDs (1-5 digits) and longer values (timestamps, nanos,
large counts), so the win is workload-dependent but strictly
non-negative for real code.

Correctness: 100k random fuzz across the full fixnum range plus
targeted edges (0, ±1, ±99, ±100, 2^30-1, 2^62-1, etc.) all pass.
make test-all shows 34694 tests, 7325860 assertions, 0 new
failures (same pre-existing TestArgf#test_puts flake as on
master) — test_integer.rb alone runs 38 tests / 421628 assertions
of which Integer#to_s exercises the bulk, all pass.

The 200-byte lookup table sits in .rodata and fits in a single
cache line of its own (3 lines for the whole table).  No change
to public API, no change to bignum conversion, no change to
non-base-10 conversion paths.

* bignum: emit two decimal digits per iteration in big2str_2bdigits

Extend the 2-digit lookup-table itoa optimisation from rb_fix2str to
the inner conversion loop used by Bignum#to_s.  big2str_2bdigits has
two code paths — a leading-chunk path that emits variable-length
digits, and a recursive-chunk path that emits a fixed-width zero-
padded block — and both gain from the halved division count.  The
classic per-digit loop is preserved for non-base-10 conversion.

Moves the ruby_decimal_digit_pairs table from a file-static in
numeric.c to bignum.c next to ruby_digitmap, and exposes it through
internal/bignum.h so both files share the same 200-byte .rodata
instance.

Benchmark (Apple M-series, best of 3 runs, measures bignum-only
speedup against the preceding fixnum commit):

  case            base      patch     delta
  ---------       -----     -----     -----
  big_20dig   10^19+...  146 ns/op 124 ns/op  -15%
  big_40dig   10^39+...  174 ns/op 152 ns/op  -13%
  big_100dig  10^99+42   236 ns/op 213 ns/op  -10%
  big_500dig  10^499+7  1119 ns/op 1086 ns/op  -3%
  big_1000dig 10^999    3490 ns/op 3459 ns/op  -1%
  fix_19dig   2^62-1      76 ns/op   76 ns/op   0% (unchanged path)

Wins concentrate in the 20-100 digit range where big2str_2bdigits
is the dominant cost.  Above ~500 digits the Karatsuba divmod
recursion dominates and the digit-emission saving shrinks to the
noise floor.  The 20-100 range is what actual Ruby code exercises
(financial high-precision sums, nanosecond timestamps, large
counters); crypto-size (1000+ digit) bignums are rare in to_s paths.

Correctness: 100k random fixnum fuzz unchanged, 500 random bignum
fuzz up to 2^256 with cross-check against sprintf("%d"), bases
2/8/16/36 round-trip, plus edge cases (0, just-above-fixnum, ±2^100,
20-digit strings near the fixnum boundary).  test/ruby/test_integer.rb
stays at 38 tests / 421628 assertions / 0 failures, test_bignum.rb
passes 74 / 607 / 0 failures, full make test-all reports 34694
tests / 0 new failures (same TestArgf#test_puts pre-existing flake
as master).

* benchmark: add int_to_s yaml for Integer#to_s

Reproducible benchmark for the two preceding commits.  Covers:

- 1/2/3/5/10/19-digit positive fixnums (spans the break-even point
  and the two large-number wins at the top)
- A negative fixnum (exercises the minus-sign prepend path)
- 20/40/100-digit bignums (spans the big2str_2bdigits win range)
- Two string-interpolation scenarios, so reviewers can see how much
  of the Integer#to_s speedup reaches real code that allocates the
  result string too

Intended to be consumed by benchmark-driver against master vs
int-to-s-twodigit for A/B comparison.  Matches the numbers in the
commit messages of 5bfb7e02a2 and c5df6de835.

---------

Co-authored-by: tomoya ishida <tomoyapenguin@gmail.com>
2026-05-09 02:10:12 +09:00
Chris Hasiński edb95b13a3 ZJIT: Add HIR tests and benchmarks for numeric predicate annotations
Add snapshot tests verifying correct HIR generation for each annotated
method:
- Float cfuncs (nan?, finite?, infinite?) emit CCall with BoolExact
  or Fixnum|NilClass return type
- Integer builtins (zero?, even?, odd?) emit InvokeBuiltin with
  BoolExact return type
- Float builtins (zero?, positive?, negative?) emit InvokeBuiltin
  with BoolExact return type

Add benchmark yml files for Float and Integer predicates to the
benchmark/ directory.
2026-04-14 14:04:52 -07:00
Jean Boussier cfec60d4be dir.c: cache and revalidate working directory
`rb_dir_getwd_ospath()` is called quite frequently, but
the overwhelming majority of the time, the current directory
didn't change.

We can also assume that most of the time, `PATH_MAX` is enough
for `getcwd`, hence we can first attempt to use a small stack
buffer rather than always allocate on the heap.

This way we can keep the last `pwd` and revalidate it with no
allocation.

On macOS syscalls are fairly slow, so the gain isn't very large.

macOS:

```
compare-ruby: ruby 4.1.0dev (2026-04-09T05:19:02Z master c091c186e4) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-04-09T06:37:20Z get-cwd-cache ea02126d79) +PRISM [arm64-darwin25]
```

|         |compare-ruby|built-ruby|
|:--------|-----------:|---------:|
|Dir.pwd  |    105.183k|  113.420k|
|         |           -|     1.08x|
```

Linux (inside virtualized Docker)

```
compare-ruby: ruby 4.1.0dev (2026-04-07T08:26:25Z master fcd210086c) +PRISM [aarch64-linux]
built-ruby: ruby 4.1.0dev (2026-04-09T06:38:09Z get-cwd-cache 6774af9ba7) +PRISM [aarch64-linux]
```

|         |compare-ruby|built-ruby|
|:--------|-----------:|---------:|
|Dir.pwd  |      4.157M|    5.541M|
|         |           -|     1.33x|
2026-04-10 11:49:58 +02:00
Jean Boussier 36b0ae025a Add single byte fast path to File.expand_path
Similar to previous changes to `File.join` & al.

```
compare-ruby: ruby 4.1.0dev (2026-04-09T12:24:09Z master c919778017) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-04-09T17:42:24Z expand-path-mbenc 4fd047f73c) +PRISM [arm64-darwin25]
```

|             |compare-ruby|built-ruby|
|:------------|-----------:|---------:|
|expand_path  |    719.828k|    1.922M|
|             |           -|     2.67x|
2026-04-10 08:37:12 +02:00
Kevin Newton c0e41097b0 Speed up memmem on Apple
Apple's libc implementation of memmem is super slow (it is a forked
version of freebsd's that never got vectorized). Instead, we should
fall back to the rolling hash on Apple. In the attached benchmark,
I'm seeing 1.07% slower to 30.34% slower, depending on the
haystack.

For reference, here are the various implementations I checked:

* musl: https://git.musl-libc.org/cgit/musl/tree/src/string/memmem.c
* freebsd: https://github.com/freebsd/freebsd-src/blob/main/lib/libc/string/memmem.c
* apple: https://github.com/apple-oss-distributions/Libc/blob/main/string/FreeBSD/memmem.c

You can see Apple just linearly searches through the string and
calls memcmp each time, whereas the other two do a window'd rolling
hash similar to the fallback Ruby already has.
2026-03-13 09:15:55 -04:00
Matt Valentine-House 208b173c25 Look up slot sizes for allocations in a table
Also remove BASE_SLOT_SIZE.
2026-03-09 15:09:53 +00:00
Jean Boussier 519a4bdbc1 Optimize File.basename
The actual algorithm is largely unchanged, just allowed to use
singlebyte checks for common encodings.

It could certainly be optimized much further, as here again it often
scans from the front of the string when we're interested in the back of
it. But the algorithm as many Windows only corner cases so I'd rather
ship a good improvement now and eventually come back to it later.

Most of improvement here is from the reduced setup cost (avodi double
null checks, avoid duping the argument, etc), and skipping the multi-byte
checks.

```
compare-ruby: ruby 4.1.0dev (2026-01-19T03:51:30Z master 631bf19b37) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-01-21T08:21:05Z opt-basename 7eb11745b2) +PRISM [arm64-darwin25]
```

|           |compare-ruby|built-ruby|
|:----------|-----------:|---------:|
|long       |      3.412M|   18.158M|
|           |           -|     5.32x|
|long_name  |      1.981M|    8.580M|
|           |           -|     4.33x|
|withext    |      3.200M|   12.986M|
|           |           -|     4.06x|
2026-01-21 11:23:01 +01:00
Jean Boussier 53fe9933fd Optimize File.extname for common encodings
Similar optimizations to the ones performed in GH-15907.

- Skip the expensive multi-byte encoding handling for the common
  encodings that are known to be safe.
- Use `CheckPath` to save on copying the argument and only scan it for
  NULL bytes once.
- Create the return string with rb_enc_str_new instead of rb_str_subseq
  as it's going to be a very small string anyway.

This could be optimized a little bit further by searching for both `.` and `dirsep`
in one pass,

```
compare-ruby: ruby 4.1.0dev (2026-01-19T03:51:30Z master 631bf19b37) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-01-20T07:33:42Z master 6fb50434e3) +PRISM [arm64-darwin25]
```

|           |compare-ruby|built-ruby|
|:----------|-----------:|---------:|
|long       |      3.606M|   22.229M|
|           |           -|     6.17x|
|long_name  |      2.254M|   13.416M|
|           |           -|     5.95x|
|short      |     16.488M|   29.969M|
|           |           -|     1.82x|
2026-01-20 09:58:51 +01:00
Jean Boussier 2d10f15118 file.c: dirname_n also use strrdirsep when n > 1
It's both simpler and faster.

|       |compare-ruby|built-ruby|
|:------|-----------:|---------:|
|long   |      3.960M|   24.072M|
|       |           -|     6.08x|
|short  |     15.417M|   29.841M|
|       |           -|     1.94x|
|n_4    |      3.858M|   18.415M|
|       |           -|     4.77x|
2026-01-20 08:33:42 +01:00
Jean Boussier 27bb1623cd file.c: Optimize rb_file_dirname_n fixed costs
- `str_null_check` was performed twice, once by `FilePathStringValue`
  and a second time by `StringValueCStr`.
- `StringValueCStr` was checking for the terminator presence, but we
  don't care about that.
- `FilePathStringValue` calls `rb_str_new_frozen` to ensure `fname`
  isn't mutated, but that's costly for such a check. Instead we
  can do it in debug mode only.
- `rb_enc_get` is slow because it accepts arbitrary objects, even immediates,
  so it has to do numerous type checks. Add a much faster `rb_str_enc_get`
  when we know we're dealing with a string.
- `rb_enc_copy` is slow for the same reasons, since we already have the
  encoding, we can use `rb_enc_str_new` instead.
2026-01-20 08:33:42 +01:00
Jean Boussier 6cd4549060 Optimize File.join common use case
`File.join` is a hotspot for common libraries such as Zeitwerk
and Bootsnap. It has a fairly flexible signature, but 99% of
the time it's called with just two (or a small number of) UTF-8 strings.

If we optimistically optimize for that use case we can cut down a large
number of type and encoding checks, significantly speeding up the method.

The one remaining expensive check we could try to optimize is `str_null_check`.
Given it's common to use the same base string for joining, we could memoize it.
Also we could precompute it for literal strings.

```
compare-ruby: ruby 4.1.0dev (2026-01-17T14:40:03Z master 00a3b71eaf) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-01-18T12:10:38Z spedup-file-join 069bab58d4) +PRISM [arm64-darwin25]
warming up....

|              |compare-ruby|built-ruby|
|:-------------|-----------:|---------:|
|two_strings   |      2.475M|    9.444M|
|              |           -|     3.82x|
|many_strings  |    551.975k|    2.346M|
|              |           -|     4.25x|
|array         |    514.946k|  522.034k|
|              |           -|     1.01x|
|mixed         |    621.236k|  633.189k|
|              |           -|     1.02x|
```
2026-01-18 16:31:31 +01:00
Jeremy Evans 38701a4de8 Remove deprecated support for to_set taking arguments 2025-12-29 14:54:41 +09:00
John Hawthorn bdeee7012c Avoid allocating intermediate string in zone_str 2025-11-16 15:23:28 -08:00
Jean Boussier fd0c772db7 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|
```
2025-08-30 14:14:10 +02:00
Jean BoussierandAaron Patterson b6d4882c05 YJIT: getinstancevariable cache indexes for types other than T_OBJECT
While accessing the ivars of other types is too complicated to
realistically generate the ASM for it, we can at least provide
the ivar index as to not have to lookup the shape tree every
time.

```
compare-ruby: ruby 3.5.0dev (2025-08-27T14:58:58Z merge-vm-setivar-d.. 5b749d8e53) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-28T17:58:32Z yjit-get-exivar efaa8c9b09) +YJIT +PRISM [arm64-darwin24]

|                           |compare-ruby|built-ruby|
|:--------------------------|-----------:|---------:|
|vm_ivar_get_on_obj         |     930.458|   936.865|
|                           |           -|     1.01x|
|vm_ivar_get_on_class       |     134.471|   431.622|
|                           |           -|     3.21x|
|vm_ivar_get_on_generic     |     146.679|   284.408|
|                           |           -|     1.94x|
```

Co-Authored-By: Aaron Patterson <tenderlove@ruby-lang.org>
2025-08-29 00:02:29 +02:00
Jean Boussier e7fb87ee3a Populate ivar caches for types other than T_OBJECT
`vm_setinstancevariable` had a codepath to try to match the inline
cache for types other than T_OBJECT, but the cache population path
in `vm_setivar_slowpath` was exclusive to T_OBJECT, so `vm_setivar_default`
would never match anything.

This commit improves `vm_setivar_slowpath` so that it is capable of
filling the cache for all types, and adds a `vm_setivar_class` codepath
for `T_CLASS` and `T_MODULE`.

`vm_setivar`, `vm_setivar_default` and `vm_setivar_class` could be unified,
but based on the very explicit `NOINLINE` I assume they were split to minimize
codesize.

```
compare-ruby: ruby 3.5.0dev (2025-08-27T14:58:58Z merge-vm-setivar-d.. 5b749d8e53) +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-27T16:30:31Z setivar-cache-gene.. 4fe78ff296) +PRISM [arm64-darwin24]

|                         |compare-ruby|built-ruby|
|:------------------------|-----------:|---------:|
|vm_ivar_set_on_instance  |     161.809|   164.688|
|                         |           -|     1.02x|
|vm_ivar_set_on_generic   |      58.769|   115.638|
|                         |           -|     1.97x|
|vm_ivar_set_on_class     |      70.034|   141.042|
|                         |           -|     2.01x|
```
2025-08-28 09:25:51 +02:00
Erim Icel c914389ae8 Update string_casecmp.yml 2025-08-11 22:22:38 +09:00
Erim Icel 5e324ac11c Optimize str_casecmp length check using pointer end 2025-08-11 22:22:38 +09:00
Jean BoussierandÉtienne Barrié f3206cc79b Struct: keep direct reference to IMEMO/fields when space allows
It's not rare for structs to have additional ivars, hence are one
of the most common, if not the most common type in the `gen_fields_tbl`.

This can cause Ractor contention, but even in single ractor mode
means having to do a hash lookup to access the ivars, and increase
GC work.

Instead, unless the struct is perfectly right sized, we can store
a reference to the associated IMEMO/fields object right after the
last struct member.

```
compare-ruby: ruby 3.5.0dev (2025-08-06T12:50:36Z struct-ivar-fields-2 9a30d141a1) +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-06T12:57:59Z struct-ivar-fields-2 2ff3ec237f) +PRISM [arm64-darwin24]
warming up.....

|                      |compare-ruby|built-ruby|
|:---------------------|-----------:|---------:|
|member_reader         |    590.317k|  579.246k|
|                      |       1.02x|         -|
|member_writer         |    543.963k|  527.104k|
|                      |       1.03x|         -|
|member_reader_method  |    213.540k|  213.004k|
|                      |       1.00x|         -|
|member_writer_method  |    192.657k|  191.491k|
|                      |       1.01x|         -|
|ivar_reader           |    403.993k|  569.915k|
|                      |           -|     1.41x|
```

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
2025-08-06 17:07:49 +02:00
Jean Boussier 95235fd528 benchmark_driver: Stop using Ractor#take 2025-07-04 08:23:20 +02:00
Tim Smith 3cfd71e7e4 Fix minor typos in comments, specs, and docs
Just a bit of minor cleanup

Signed-off-by: Tim Smith <tsmith84@gmail.com>
2025-06-18 07:51:16 +09:00
Hartley McGuire 8120971932 Move more NilClass methods to ruby
```
$ make benchmark ITEM=nilclass COMPARE_RUBY="/opt/rubies/ruby-master/bin/ruby"
/opt/rubies/3.4.2/bin/ruby --disable=gems -rrubygems -I../benchmark/lib ../benchmark/benchmark-driver/exe/benchmark-driver \
                    --executables="compare-ruby::/opt/rubies/ruby-master/bin/ruby -I.ext/common --disable-gem" \
                    --executables="built-ruby::./miniruby -I../lib -I. -I.ext/common  ../tool/runruby.rb --extout=.ext  -- --disable-gems --disable-gem" \
                    --output=markdown --output-compare -v $(find ../benchmark -maxdepth 1 -name 'nilclass' -o -name '*nilclass*.yml' -o -name '*nilclass*.rb' | sort)
compare-ruby: ruby 3.5.0dev (2025-06-02T13:52:25Z master cbd49ecbbe) +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-06-02T22:47:21Z hm-ruby-nilclass 3e7f1f0466) +PRISM [arm64-darwin24]

|             |compare-ruby|built-ruby|
|:------------|-----------:|---------:|
|rationalize  |     24.056M|   53.908M|
|             |           -|     2.24x|
|to_c         |     23.652M|   82.781M|
|             |           -|     3.50x|
|to_i         |     89.526M|   84.388M|
|             |       1.06x|         -|
|to_f         |     84.746M|   96.899M|
|             |           -|     1.14x|
|to_r         |     25.107M|   83.472M|
|             |           -|     3.32x|
|splat        |     42.772M|   42.717M|
|             |       1.00x|         -|
```

This makes them much faster
2025-06-12 09:30:09 +02:00
John Hawthorn e01e89f55c Avoid calling RCLASS_SUPER in rb_class_superclass 2025-05-23 10:22:24 -07:00
Samuel Williams 73c9d6ccaa Allow IO#close to interrupt IO operations on fibers using fiber_interrupt hook. (#12839) 2025-05-23 14:55:05 +09:00
John Hawthorn 9b8c846bdf Add an additional test to module_eqq 2025-05-12 19:05:19 -07:00
John Hawthorn b0502e8f90 Remove respond_to check from Class#bind_call 2025-05-12 14:10:29 -07:00
e4f85bfc31 Implement Set as a core class
Set has been an autoloaded standard library since Ruby 3.2.
The standard library Set is less efficient than it could be, as it
uses Hash for storage, which stores unnecessary values for each key.

Implementation details:

* Core Set uses a modified version of `st_table`, named `set_table`.
  than `s/st_/set_/`, the main difference is that the stored records
  do not have values, making them 1/3 smaller. `st_table_entry` stores
  `hash`, `key`, and `record` (value), while `set_table_entry` only
  stores `hash` and `key`.  This results in large sets using ~33% less
  memory compared to stdlib Set.  For small sets, core Set uses 12% more
  memory (160 byte object slot and 64 malloc bytes, while stdlib set
  uses 40 for Set and 160 for Hash).  More memory is used because
  the set_table is embedded and 72 bytes in the object slot are
  currently wasted. Hopefully we can make this more efficient and have
  it stored in an 80 byte object slot in the future.

* All methods are implemented as cfuncs, except the pretty_print
  methods, which were moved to `lib/pp.rb` (which is where the
  pretty_print methods for other core classes are defined).  As is
  typical for core classes, internal calls call C functions and
  not Ruby methods.  For example, to check if something is a Set,
  `rb_obj_is_kind_of` is used, instead of calling `is_a?(Set)` on the
  related object.

* Almost all methods use the same algorithm that the pure-Ruby
  implementation used.  The exception is when calling `Set#divide` with a
  block with 2-arity.  The pure-Ruby method used tsort to implement this.
  I developed an algorithm that only allocates a single intermediate
  hash and does not need tsort.

* The `flatten_merge` protected method is no longer necessary, so it
  is not implemented (it could be).

* Similar to Hash/Array, subclasses of Set are no longer reflected in
  `inspect` output.

* RDoc from stdlib Set was moved to core Set, with minor updates.

This includes a comprehensive benchmark suite for all public Set
methods.  As you would expect, the native version is faster in the
vast majority of cases, and multiple times faster in many cases.
There are a few cases where it is significantly slower:

* Set.new with no arguments (~1.6x)
* Set#compare_by_identity for small sets (~1.3x)
* Set#clone for small sets (~1.5x)
* Set#dup for small sets (~1.7x)

These are slower as Set does not currently use the AR table
optimization that Hash does, so a new set_table is initialized for
each call.  I'm not sure it's worth the complexity to have an AR
table-like optimization for small sets (for hashes it makes sense,
as small hashes are used everywhere in Ruby).

The rbs and repl_type_completor bundled gems will need updates to
support core Set.  The pull request marks them as allowed failures.

This passes all set tests with no changes.  The following specs
needed modification:

* Modifying frozen set error message (changed for the better)
* `Set#divide` when passed a 2-arity block no longer yields the same
  object as both the first and second argument (this seems like an issue
  with the previous implementation).
* Set-like objects that override `is_a?` such that `is_a?(Set)` return
  `true` are no longer treated as Set instances.
* `Set.allocate.hash` is no longer the same as `nil.hash`
* `Set#join` no longer calls `Set#to_a` (it calls the underlying C
   function).
* `Set#flatten_merge` protected method is not implemented.

Previously, `set.rb` added a `SortedSet` autoload, which loads
`set/sorted_set.rb`.  This replaces the `Set` autoload in `prelude.rb`
with a `SortedSet` autoload, but I recommend removing it and
`set/sorted_set.rb`.

This moves `test/set/test_set.rb` to `test/ruby/test_set.rb`,
reflecting that switch to a core class.  This does not move the spec
files, as I'm not sure how they should be handled.

Internally, this uses the st_* types and functions as much as
possible, and only adds set_* types and functions as needed.
The underlying set_table implementation is stored in st.c, but
there is no public C-API for it, nor is there one planned, in
order to keep the ability to change the internals going forward.

For internal uses of st_table with Qtrue values, those can
probably be replaced with set_table.  To do that, include
internal/set_table.h.  To handle symbol visibility (rb_ prefix),
internal/set_table.h uses the same macro approach that
include/ruby/st.h uses.

The Set class (rb_cSet) and all methods are defined in set.c.
There isn't currently a C-API for the Set class, though C-API
functions can be added as needed going forward.

Implements [Feature #21216]

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
Co-authored-by: Oliver Nutter <mrnoname1000@riseup.net>
2025-04-26 10:31:11 +09:00