From 8c615a0466d5e2b583aa851ee909af18a9a4d12c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sun, 13 Sep 2026 09:09:50 +0900 Subject: [PATCH 01/17] Bypass the broken autoconf wrapper on Cygwin The autoconf-20260320-1 package replaced /usr/bin/autoreconf with a wrapper that fails with a shell syntax error on every call, so run the versioned binary directly. Co-Authored-By: Claude Opus 5 --- .github/workflows/cygwin.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index aeb98b95eca222..fa00cba8c58104 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -58,7 +58,8 @@ jobs: - name: configure run: | - ./autogen.sh + # The autoreconf wrapper in autoconf-20260320-1 has a shell syntax error + AUTORECONF=autoreconf-2.73 ./autogen.sh ./configure --disable-install-doc shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0} From 0da3803fc4c1e36ccc93def2e71c40c34432eff0 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 12 Sep 2026 20:31:41 -0500 Subject: [PATCH 02/17] [DOC] Harmonize unlink methods --- file.c | 25 +++++++++++++++++-------- pathname_builtin.rb | 9 ++++++++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/file.c b/file.c index f7d0a46420d4f2..1d916ed609089a 100644 --- a/file.c +++ b/file.c @@ -3881,19 +3881,28 @@ unlink_internal(const char *path, void *arg) } /* + * :markup: markdown + * * call-seq: - * File.delete(*filepaths) -> integer - * File.unlink(*filepaths) -> integer + * File.delete(*paths) -> integer + * File.unlink(*paths) -> integer + * + * Removes the entry at each path in `paths`; + * returns the count of removed entries. * - * Removes the file entry at each path in +filepaths+; - * returns the number of removed files. + * Does not follow [symbolic links](rdoc-ref:file/symbolic_links.md); + * if an entry is a symlink, the link itself is removed. * - * File.write('t.tmp', 'foo') - * File.write('u.tmp', 'bar') - * File.delete('t.tmp', 'u.tmp') # => 2 + * ```ruby + * File.write('t.tmp', 'foo') + * File.write('u.tmp', 'bar') + * File.delete('t.tmp', 'u.tmp') # => 2 + * File.symlink('README.md', 'foo') + * File.unlink('foo') # => 1 + * ``` * * Raises an exception on any error; - * some files may have been deleted before the path causing the error. + * some entries may have been deleted before the path causing the error. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 8728c521c7f4db..dcc7797a7f92ce 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2975,11 +2975,18 @@ class Pathname # * mixed * # unlink -> 0 or 1 # # Removes the entry represented by `self`; - # returns `0` if a directory, `1` if a file: + # returns `0` if a directory, `1` otherwise. + # + # Does not follow [symbolic links](rdoc-ref:file/symbolic_links.md); + # if the entry is a symlink, the link itself is removed. # # ```ruby # Pathname(Pathname.mktmpdir).unlink # => 0 # Pathname(Tempfile.create).unlink # => 1 + # pn_target = Pathname('README.md') # => # + # pn_link = Pathname('foo') # => # + # pn_link.make_symlink(pn_target) + # pn_link.delete # ``` # def unlink() From 69d785afcf4c15812e10845cebff747e8c358f8a Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sat, 12 Sep 2026 09:06:25 -0500 Subject: [PATCH 03/17] [DOC] Harmonize lchmod methods --- file.c | 20 ++++---------------- pathname_builtin.rb | 20 ++++---------------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/file.c b/file.c index 1d916ed609089a..8d021238280073 100644 --- a/file.c +++ b/file.c @@ -3133,24 +3133,12 @@ lchmod_internal(const char *path, void *mode) * call-seq: * File.lchmod(mode, *paths) -> paths_count * - * Not supported on some platforms (raises NotImplementedError). + * Not supported on Linux or Windows (raises NotImplementedError). * - * When supported: like File::chmod, but does not follow symbolic links, + * When supported: like File::chmod, + * but does not follow [symbolic links](rdoc-ref:file/symbolic_links.md), * and therefore changes the mode of the entries given by `paths`; - * returns the number of paths given: - * - * ```ruby - * File.write('t.tmp', '') - * File.symlink('t.tmp', 'link') - * File.stat('t.tmp').mode.to_s(8) # => "100664" - * File.stat('link').mode.to_s(8) # => "100664" - * File.lchmod(0777, 'link') - * File.stat('t.tmp').mode.to_s(8) # => "100664" - * File.stat('link').mode.to_s(8) # => "100777" - * File.delete('t.tmp') - * File.delete('link') - * ``` - * + * returns the number of paths given. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index dcc7797a7f92ce..f01b770860f110 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1543,23 +1543,11 @@ def chmod(mode) File.chmod(mode, @path) end # call-seq: # lchmod(mode) -> 1 # - # Not supported on some platforms (raises NotImplementedError). - # - # When supported: like Pathname::chmod, but does not follow symbolic links, - # and therefore changes the mode of the entry specified by `self`: - # - # ```ruby - # File.write('t.tmp', '') - # File.symlink('t.tmp', 'link') - # File.stat('t.tmp').mode.to_s(8) # => "100664" - # File.stat('link').mode.to_s(8) # => "100664" - # Pathname('link').lchmod(0777) - # File.stat('t.tmp').mode.to_s(8) # => "100664" - # File.stat('link').mode.to_s(8) # => "100777" - # File.delete('t.tmp') - # File.delete('link') - # ``` + # Not supported on Linux or Windows (raises NotImplementedError). # + # When supported: like Pathname#chmod, + # but does not follow [symbolic links](rdoc-ref:file/symbolic_links.md), + # and therefore changes the mode of the entry specified by `self`. def lchmod(mode) File.lchmod(mode, @path) end # :markup: markdown From aaf83bcebe3d3612e7704fd3548575a3f2c29325 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 13 Sep 2026 14:48:19 +1200 Subject: [PATCH 04/17] Scope fiber_interrupt with an operation proxy. (#18793) [Bug #21918] --- doc/language/fiber.md | 10 +-- include/ruby/fiber/scheduler.h | 15 +++- internal/io.h | 3 + internal/scheduler.h | 7 ++ scheduler.c | 138 +++++++++++++++++++++++++++++++-- test/fiber/scheduler.rb | 19 +---- test/fiber/test_io_close.rb | 58 +++++++++++++- thread.c | 53 ++++++++++++- 8 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 internal/scheduler.h diff --git a/doc/language/fiber.md b/doc/language/fiber.md index 2107a7b9934f8c..ff029a7ec8c253 100644 --- a/doc/language/fiber.md +++ b/doc/language/fiber.md @@ -234,7 +234,7 @@ non-blocking*, the operation will invoke the scheduler. Closing an IO interrupts all blocking operations on that IO. When a thread calls `IO#close`, it first attempts to interrupt any threads or fibers that are blocked on that IO. The closing thread waits until all blocked threads and fibers have been properly interrupted and removed from the IO's blocking list. Each interrupted thread or fiber receives an `IOError` and is cleanly removed from the blocking operation. Only after all blocking operations have been interrupted and cleaned up will the actual file descriptor be closed, ensuring proper resource cleanup and preventing potential race conditions. -For fibers managed by a scheduler, the interruption process involves calling `rb_fiber_scheduler_fiber_interrupt` on the scheduler. The corresponding `Fiber::Scheduler#fiber_interrupt` hook is required. This allows the scheduler to handle the interruption in a way that's appropriate for its event loop implementation. The scheduler can then notify the fiber, which will receive an `IOError` and be removed from the blocking operation. This mechanism ensures that fiber-based concurrency works correctly with IO operations, even when those operations are interrupted by `IO#close`. +For fibers managed by a scheduler, the interruption process involves calling `rb_fiber_scheduler_fiber_interrupt` on the scheduler. The corresponding `Fiber::Scheduler#fiber_interrupt` hook is required. For IO operations, the first argument is an operation-scoped proxy which responds to `alive?`, `raise`, and `transfer`, matching the subset of the Fiber interface needed to deliver an interruption. The scheduler can raise the supplied exception on the target, or enqueue the target directly and later call `transfer` to raise its stored exception. If the operation completes before the queued interruption is processed, `alive?` returns false and both `raise` and `transfer` have no effect. This prevents a delayed exception from escaping the operation which caused it and interrupting a later operation on the same fiber. ```mermaid sequenceDiagram @@ -266,16 +266,16 @@ sequenceDiagram ThreadB->>IO: thread_io_close_notify_all Note over ThreadB: rb_mutex_sleep - IO->>Scheduler: rb_fiber_scheduler_fiber_interrupt(Fiber1) - Scheduler->>Fiber1: fiber_interrupt with IOError + IO->>Scheduler: rb_fiber_scheduler_fiber_interrupt(Target1) + Scheduler->>Target1: raise IOError if alive? activate Fiber1 Note over IO: fiber_interrupt causes removal from blocking list Fiber1->>IO: rb_io_blocking_operation_exit() IO-->>ThreadB: Wakeup thread deactivate Fiber1 - IO->>Scheduler: rb_fiber_scheduler_fiber_interrupt(Fiber2) - Scheduler->>Fiber2: fiber_interrupt with IOError + IO->>Scheduler: rb_fiber_scheduler_fiber_interrupt(Target2) + Scheduler->>Target2: raise IOError if alive? activate Fiber2 Note over IO: fiber_interrupt causes removal from blocking list Fiber2->>IO: rb_io_blocking_operation_exit() diff --git a/include/ruby/fiber/scheduler.h b/include/ruby/fiber/scheduler.h index 73a1ed40640be0..10eb1352777da8 100644 --- a/include/ruby/fiber/scheduler.h +++ b/include/ruby/fiber/scheduler.h @@ -25,7 +25,8 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() // Version 3: Adds support for `fiber_interrupt`. // Version 4: IO hooks use single-transfer `(offset, length)` semantics. -#define RUBY_FIBER_SCHEDULER_VERSION 4 +// Version 5: `fiber_interrupt` may receive an operation-scoped proxy. +#define RUBY_FIBER_SCHEDULER_VERSION 5 struct timeval; struct rb_thread_struct; @@ -475,16 +476,22 @@ int rb_fiber_scheduler_blocking_operation_cancel(rb_fiber_scheduler_blocking_ope VALUE rb_fiber_scheduler_blocking_operation_wait(VALUE scheduler, void* (*function)(void *), void *data, rb_unblock_function_t *unblock_function, void *data2, int flags, struct rb_fiber_scheduler_blocking_operation_state *state); /** - * Interrupt a fiber by raising an exception. You can construct an exception using `rb_make_exception`. + * Interrupt a target by raising an exception. You can construct an exception using `rb_make_exception`. + * + * The target is usually a Fiber. For an IO operation, it may instead be an + * operation-scoped proxy which responds to `alive?`, `raise`, and `transfer`. + * Calling `transfer` raises the stored exception in the target Fiber, allowing + * the proxy to be queued directly. Schedulers should restrict their use of the + * target to those methods. * * This hook may be invoked by a different thread. * * @param[in] scheduler Target scheduler. - * @param[in] fiber The fiber to interrupt. + * @param[in] target The Fiber or operation-scoped proxy to interrupt. * @param[in] exception The exception to raise in the fiber. * @return What `scheduler.fiber_interrupt` returns. */ -VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exception); +VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE target, VALUE exception); /** * Create and schedule a non-blocking fiber. diff --git a/internal/io.h b/internal/io.h index 2110f0b0876271..f47aba42d1e9af 100644 --- a/internal/io.h +++ b/internal/io.h @@ -27,6 +27,9 @@ struct rb_io_blocking_operation { // The execution context of the blocking operation. struct rb_execution_context_struct *ec; + + // An operation-scoped target passed to the Fiber scheduler if interrupted. + VALUE scheduler_interrupt_target; }; /** Ruby's IO, metadata and buffers. */ diff --git a/internal/scheduler.h b/internal/scheduler.h new file mode 100644 index 00000000000000..f79b5d02a6dbc9 --- /dev/null +++ b/internal/scheduler.h @@ -0,0 +1,7 @@ +#pragma once + +#include "ruby/ruby.h" + +VALUE rb_fiber_scheduler_interrupt_target_new(VALUE fiber, VALUE exception); +VALUE rb_fiber_scheduler_interrupt_target_exception(VALUE target); +void rb_fiber_scheduler_interrupt_target_invalidate(VALUE target); diff --git a/scheduler.c b/scheduler.c index d315b275fbad10..0c955a28747aaf 100644 --- a/scheduler.c +++ b/scheduler.c @@ -17,6 +17,7 @@ #include "ruby/thread.h" // For `ruby_thread_has_gvl_p`: +#include "internal/scheduler.h" #include "internal/thread.h" // For atomic operations: @@ -51,6 +52,118 @@ static ID id_fiber_schedule; // Our custom blocking operation class static VALUE rb_cFiberSchedulerBlockingOperation; +static VALUE rb_cFiberSchedulerInterruptTarget; + +struct rb_fiber_scheduler_interrupt_target { + VALUE fiber; + VALUE exception; + bool active; +}; + +static void +interrupt_target_mark(void *ptr) +{ + struct rb_fiber_scheduler_interrupt_target *target = ptr; + + rb_gc_mark(target->fiber); + rb_gc_mark(target->exception); +} + +static size_t +interrupt_target_memsize(const void *ptr) +{ + return sizeof(struct rb_fiber_scheduler_interrupt_target); +} + +static const rb_data_type_t interrupt_target_data_type = { + "Fiber::Scheduler::InterruptTarget", + { + interrupt_target_mark, + RUBY_DEFAULT_FREE, + interrupt_target_memsize, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY +}; + +static struct rb_fiber_scheduler_interrupt_target * +get_interrupt_target(VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target; + TypedData_Get_Struct(self, struct rb_fiber_scheduler_interrupt_target, &interrupt_target_data_type, target); + return target; +} + +static VALUE +interrupt_target_alive_p(VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target = get_interrupt_target(self); + + if (!target->active) { + return Qfalse; + } + + return rb_fiber_alive_p(target->fiber); +} + +static VALUE +interrupt_target_raise(int argc, VALUE *argv, VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target = get_interrupt_target(self); + + if (!target->active) { + return Qnil; + } + + return rb_fiber_raise(target->fiber, argc, argv); +} + +static VALUE +interrupt_target_transfer(VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target = get_interrupt_target(self); + + if (!target->active || !RTEST(rb_fiber_alive_p(target->fiber))) { + return Qnil; + } + + VALUE exception = target->exception; + return rb_fiber_raise(target->fiber, 1, &exception); +} + +VALUE +rb_fiber_scheduler_interrupt_target_new(VALUE fiber, VALUE exception) +{ + struct rb_fiber_scheduler_interrupt_target *target; + VALUE self = TypedData_Make_Struct(rb_cFiberSchedulerInterruptTarget, struct rb_fiber_scheduler_interrupt_target, &interrupt_target_data_type, target); + + target->fiber = fiber; + target->exception = exception; + target->active = true; + + return self; +} + +VALUE +rb_fiber_scheduler_interrupt_target_exception(VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target = get_interrupt_target(self); + + if (target->active) { + return target->exception; + } + + return Qnil; +} + +void +rb_fiber_scheduler_interrupt_target_invalidate(VALUE self) +{ + struct rb_fiber_scheduler_interrupt_target *target = get_interrupt_target(self); + + target->active = false; + target->fiber = Qnil; + target->exception = Qnil; +} /* * Custom blocking operation structure for blocking operations @@ -344,6 +457,15 @@ Init_Fiber_Scheduler(void) // Register the anonymous class as a GC root so it doesn't get collected rb_gc_register_mark_object(rb_cFiberSchedulerBlockingOperation); + // Define an anonymous interrupt target class. Instances are passed to + // fiber_interrupt and cannot be instantiated directly. + rb_cFiberSchedulerInterruptTarget = rb_class_new(rb_cObject); + rb_undef_alloc_func(rb_cFiberSchedulerInterruptTarget); + rb_define_method(rb_cFiberSchedulerInterruptTarget, "alive?", interrupt_target_alive_p, 0); + rb_define_method(rb_cFiberSchedulerInterruptTarget, "raise", interrupt_target_raise, -1); + rb_define_method(rb_cFiberSchedulerInterruptTarget, "transfer", interrupt_target_transfer, 0); + rb_gc_register_mark_object(rb_cFiberSchedulerInterruptTarget); + #if 0 /* for RDoc */ rb_cFiberScheduler = rb_define_class_under(rb_cFiber, "Scheduler", rb_cObject); rb_define_method(rb_cFiberScheduler, "close", rb_fiber_scheduler_close, 0); @@ -1154,17 +1276,21 @@ VALUE rb_fiber_scheduler_blocking_operation_wait(VALUE scheduler, void* (*functi /* * Document-method: Fiber::Scheduler#fiber_interrupt - * call-seq: fiber_interrupt(fiber, exception) + * call-seq: fiber_interrupt(target, exception) * - * Invoked by Ruby's core methods to notify the scheduler that the blocked fiber should be interrupted - * with an exception. For example, IO#close uses this method to interrupt fibers that are performing - * blocking IO operations. + * Invoked by Ruby's core methods to notify the scheduler that a blocked fiber + * should be interrupted with an exception. For IO operations, +target+ is an + * operation-scoped proxy which responds to #alive?, #raise, and #transfer. The + * scheduler should enqueue the target on its owning thread and use only those + * methods. #transfer raises the interruption's stored exception in the target. + * Once the operation completes, #alive? returns false and #raise has no effect, + * preventing a delayed exception from escaping into a later operation. * */ -VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exception) +VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE target, VALUE exception) { VALUE arguments[] = { - fiber, exception + target, exception }; VALUE result; diff --git a/test/fiber/scheduler.rb b/test/fiber/scheduler.rb index af66ba106002e1..2cb04812fa2d4f 100644 --- a/test/fiber/scheduler.rb +++ b/test/fiber/scheduler.rb @@ -313,24 +313,9 @@ def unblock(blocker, fiber) io.write_nonblock('.') end - class FiberInterrupt - def initialize(fiber, exception) - @fiber = fiber - @exception = exception - end - - def alive? - @fiber.alive? - end - - def transfer - @fiber.raise(@exception) - end - end - - def fiber_interrupt(fiber, exception) + def fiber_interrupt(target, _exception) @lock.synchronize do - @ready << FiberInterrupt.new(fiber, exception) + @ready << target end io = @urgent.last diff --git a/test/fiber/test_io_close.rb b/test/fiber/test_io_close.rb index 742b40841d90d6..14c6731acbb9e2 100644 --- a/test/fiber/test_io_close.rb +++ b/test/fiber/test_io_close.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require 'test/unit' +require 'io/wait' require_relative 'scheduler' class TestFiberIOClose < Test::Unit::TestCase @@ -44,6 +45,46 @@ def test_io_close_across_fibers end end + def test_io_close_interrupt_does_not_escape_blocking_operation + with_socket_pair do |source, source_peer| + with_socket_pair do |unrelated, unrelated_peer| + errors = [] + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + 5.times do + Fiber.schedule do + begin + source.wait_readable(0.01) + source.close + rescue => error + errors << [:source, error] + end + + begin + unrelated.wait_readable(0.01) + rescue => error + errors << [:unrelated, error] + end + end + end + end + + thread.join + + assert_equal 4, errors.size + assert_equal [:source], errors.map(&:first).uniq + errors.each do |location, error| + assert_equal :source, location + assert_instance_of IOError, error + assert_match(/closed/, error.message) + end + end + end + end + def test_io_close_blocking_thread omit "Interrupting a io_wait read is not supported!" if RUBY_PLATFORM =~ /mswin|mingw/ @@ -80,9 +121,19 @@ def test_io_close_blocking_fiber with_socket_pair do |i, o| error = nil + scheduler = nil + + scheduler_class = Class.new(Scheduler) do + attr_reader :interrupt_target + + def fiber_interrupt(target, exception) + @interrupt_target = target + super + end + end thread = Thread.new do - scheduler = Scheduler.new + scheduler = scheduler_class.new Fiber.set_scheduler scheduler Fiber.schedule do @@ -102,6 +153,11 @@ def test_io_close_blocking_fiber assert_instance_of IOError, error assert_match(/closed/, error.message) + + interrupt_target = scheduler.interrupt_target + assert_not_predicate interrupt_target, :alive? + assert_nil interrupt_target.transfer + assert_nil interrupt_target.raise(IOError.new) end end end diff --git a/thread.c b/thread.c index 5de075cf73ad9a..992f81190882ff 100644 --- a/thread.c +++ b/thread.c @@ -87,6 +87,7 @@ #include "internal/object.h" #include "internal/proc.h" #include "ruby/fiber/scheduler.h" +#include "internal/scheduler.h" #include "internal/signal.h" #include "internal/thread.h" #include "internal/time.h" @@ -2023,6 +2024,10 @@ rb_io_blocking_operation_exit(struct rb_io *io, struct rb_io_blocking_operation // Indicate that the blocking operation is no longer active: blocking_operation->ec = NULL; + if (!NIL_P(blocking_operation->scheduler_interrupt_target)) { + rb_fiber_scheduler_interrupt_target_invalidate(blocking_operation->scheduler_interrupt_target); + } + if (RB_TEST(wakeup_mutex)) { struct io_blocking_operation_arguments arguments = { .io = io, @@ -2047,6 +2052,30 @@ rb_thread_io_blocking_operation_ensure(VALUE _argument) return Qnil; } +struct thread_io_blocking_operation_arguments { + VALUE (*function)(VALUE); + VALUE argument; + struct rb_io_blocking_operation *blocking_operation; +}; + +static VALUE +rb_thread_io_blocking_operation_body(VALUE _arguments) +{ + struct thread_io_blocking_operation_arguments *arguments = (void *)_arguments; + VALUE result = arguments->function(arguments->argument); + VALUE target = arguments->blocking_operation->scheduler_interrupt_target; + + if (!NIL_P(target)) { + VALUE exception = rb_fiber_scheduler_interrupt_target_exception(target); + + if (!NIL_P(exception)) { + rb_exc_raise(exception); + } + } + + return result; +} + /* * Executes a function that performs a blocking IO operation, while properly tracking * the operation in the IO's blocking_operations list. This ensures proper cleanup @@ -2069,6 +2098,7 @@ rb_thread_io_blocking_operation(VALUE self, VALUE(*function)(VALUE), VALUE argum rb_execution_context_t *ec = GET_EC(); struct rb_io_blocking_operation blocking_operation = { .ec = ec, + .scheduler_interrupt_target = Qnil, }; rb_io_blocking_operation_enter(io, &blocking_operation); @@ -2077,7 +2107,13 @@ rb_thread_io_blocking_operation(VALUE self, VALUE(*function)(VALUE), VALUE argum .blocking_operation = &blocking_operation }; - return rb_ensure(function, argument, rb_thread_io_blocking_operation_ensure, (VALUE)&io_blocking_operation_arguments); + struct thread_io_blocking_operation_arguments arguments = { + .function = function, + .argument = argument, + .blocking_operation = &blocking_operation, + }; + + return rb_ensure(rb_thread_io_blocking_operation_body, (VALUE)&arguments, rb_thread_io_blocking_operation_ensure, (VALUE)&io_blocking_operation_arguments); } static bool @@ -2187,6 +2223,7 @@ rb_thread_io_blocking_call(struct rb_io* io, rb_blocking_function_t *func, void struct rb_io_blocking_operation blocking_operation = { .ec = ec, + .scheduler_interrupt_target = Qnil, }; rb_io_blocking_operation_enter(io, &blocking_operation); @@ -3086,7 +3123,16 @@ thread_io_close_notify_all(VALUE _io) rb_thread_t *thread = ec->thread_ptr; if (thread->scheduler != Qnil) { - rb_fiber_scheduler_fiber_interrupt(thread->scheduler, rb_fiberptr_self(ec->fiber_ptr), error); + VALUE target = blocking_operation->scheduler_interrupt_target; + + if (NIL_P(target)) { + VALUE fiber = rb_fiberptr_self(ec->fiber_ptr); + target = rb_fiber_scheduler_interrupt_target_new(fiber, error); + blocking_operation->scheduler_interrupt_target = target; + } + + rb_fiber_scheduler_fiber_interrupt(thread->scheduler, target, error); + RB_GC_GUARD(target); } else { // If the thread is not the current thread, we need to enqueue an error: @@ -4884,6 +4930,7 @@ thread_io_wait(rb_thread_t *th, struct rb_io *io, int fd, int events, struct tim if (io) { blocking_operation.ec = ec; + blocking_operation.scheduler_interrupt_target = Qnil; COMPILER_WARNING_PUSH #if RBIMPL_COMPILER_SINCE(GCC, 12, 0, 0) COMPILER_WARNING_IGNORED(-Wdangling-pointer) @@ -5054,12 +5101,14 @@ thread_io_wait(rb_thread_t *th, struct rb_io *io, int fd, int events, struct tim if (io) { args.io = io; blocking_operation.ec = th->ec; + blocking_operation.scheduler_interrupt_target = Qnil; rb_io_blocking_operation_enter(io, &blocking_operation); args.blocking_operation = &blocking_operation; } else { args.io = NULL; blocking_operation.ec = NULL; + blocking_operation.scheduler_interrupt_target = Qnil; args.blocking_operation = NULL; } From 078196c12d07c7c3bb4f9335b6b0254fd1dc8762 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 20:15:37 +0900 Subject: [PATCH 05/17] Use Set instead of Hash for Array#| MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#| to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a | b } Results: Benchmark 1: master Time (mean ± σ): 237.5 ms ± 2.0 ms [User: 223.4 ms, System: 10.9 ms] Range (min … max): 234.6 ms … 240.7 ms 12 runs Benchmark 2: branch Time (mean ± σ): 165.2 ms ± 3.8 ms [User: 151.2 ms, System: 10.8 ms] Range (min … max): 159.8 ms … 174.2 ms 18 runs Summary branch ran 1.44 ± 0.04 times faster than master --- array.c | 13 ++++++++----- internal/set.h | 2 ++ set.c | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/array.c b/array.c index abaf5184e92cad..8fabc8f9e71ac2 100644 --- a/array.c +++ b/array.c @@ -6017,8 +6017,6 @@ rb_ary_union_hash(VALUE hash, VALUE ary2) static VALUE rb_ary_or(VALUE ary1, VALUE ary2) { - VALUE hash; - ary2 = to_ary(ary2); if (RARRAY_LEN(ary1) + RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { VALUE ary3 = rb_ary_new(); @@ -6027,10 +6025,15 @@ rb_ary_or(VALUE ary1, VALUE ary2) return ary3; } - hash = ary_make_hash(ary1); - rb_ary_union_hash(hash, ary2); + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary1) + RARRAY_LEN(ary2))); + for (long i = 0; i < RARRAY_LEN(ary1); i++) { + rb_set_add_no_check(set, RARRAY_AREF(ary1, i)); + } + for (long i = 0; i < RARRAY_LEN(ary2); i++) { + rb_set_add_no_check(set, RARRAY_AREF(ary2, i)); + } - return rb_hash_values(hash); + return rb_set_to_a(set); } /* diff --git a/internal/set.h b/internal/set.h index 48c282c3d4e23d..0995d6a217dc6f 100644 --- a/internal/set.h +++ b/internal/set.h @@ -12,5 +12,7 @@ #include "ruby/ruby.h" VALUE rb_ident_set_new(void); +bool rb_set_add_no_check(VALUE set, VALUE element); +VALUE rb_set_to_a(VALUE set); #endif /* INTERNAL_SET_H */ diff --git a/set.c b/set.c index 1c19c2ca81f057..f22880d57b578e 100644 --- a/set.c +++ b/set.c @@ -2278,6 +2278,22 @@ rb_ident_set_new(void) return set_alloc_with_size_and_type(rb_cSet, 0, &identhash); } +bool +rb_set_add_no_check(VALUE set, VALUE element) +{ + if (set_insert(RSET_TABLE(set), (st_data_t)element) == 0) { + RB_OBJ_WRITTEN(set, Qundef, element); + return true; + } + return false; +} + +VALUE +rb_set_to_a(VALUE set) +{ + return set_i_to_a(set); +} + /* C-API functions */ void From 1a78389785004aedb8ab9081a15a3fe0e83c9af5 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 20:37:18 +0900 Subject: [PATCH 06/17] Use Set instead of Hash for Array#union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#union to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a.union(b) } Results: Benchmark 1: master Time (mean ± σ): 239.9 ms ± 3.5 ms [User: 224.9 ms, System: 11.7 ms] Range (min … max): 234.8 ms … 246.0 ms 12 runs Benchmark 2: branch Time (mean ± σ): 165.5 ms ± 4.1 ms [User: 150.9 ms, System: 11.5 ms] Range (min … max): 158.9 ms … 174.0 ms 18 runs Summary branch ran 1.45 ± 0.04 times faster than master --- array.c | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/array.c b/array.c index 8fabc8f9e71ac2..100439819634d0 100644 --- a/array.c +++ b/array.c @@ -5968,14 +5968,6 @@ rb_ary_intersection_multi(int argc, VALUE *argv, VALUE ary) return result; } -static int -ary_hash_orset(st_data_t *key, st_data_t *value, st_data_t arg, int existing) -{ - if (existing) return ST_STOP; - *key = *value = (VALUE)arg; - return ST_CONTINUE; -} - static void rb_ary_union(VALUE ary_union, VALUE ary) { @@ -5988,14 +5980,10 @@ rb_ary_union(VALUE ary_union, VALUE ary) } static void -rb_ary_union_hash(VALUE hash, VALUE ary2) +rb_ary_union_set(VALUE set, VALUE ary) { - long i; - for (i = 0; i < RARRAY_LEN(ary2); i++) { - VALUE elt = RARRAY_AREF(ary2, i); - if (!rb_hash_stlike_update(hash, (st_data_t)elt, ary_hash_orset, (st_data_t)elt)) { - RB_OBJ_WRITTEN(hash, Qundef, elt); - } + for (long i = 0; i < RARRAY_LEN(ary); i++) { + rb_set_add_no_check(set, RARRAY_AREF(ary, i)); } } @@ -6026,12 +6014,8 @@ rb_ary_or(VALUE ary1, VALUE ary2) } VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary1) + RARRAY_LEN(ary2))); - for (long i = 0; i < RARRAY_LEN(ary1); i++) { - rb_set_add_no_check(set, RARRAY_AREF(ary1, i)); - } - for (long i = 0; i < RARRAY_LEN(ary2); i++) { - rb_set_add_no_check(set, RARRAY_AREF(ary2, i)); - } + rb_ary_union_set(set, ary1); + rb_ary_union_set(set, ary2); return rb_set_to_a(set); } @@ -6062,12 +6046,8 @@ rb_ary_or(VALUE ary1, VALUE ary2) static VALUE rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) { - int i; - long sum; - VALUE hash; - - sum = RARRAY_LEN(ary); - for (i = 0; i < argc; i++) { + long sum = RARRAY_LEN(ary); + for (int i = 0; i < argc; i++) { argv[i] = to_ary(argv[i]); sum += RARRAY_LEN(argv[i]); } @@ -6076,15 +6056,16 @@ rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) VALUE ary_union = rb_ary_new(); rb_ary_union(ary_union, ary); - for (i = 0; i < argc; i++) rb_ary_union(ary_union, argv[i]); + for (int i = 0; i < argc; i++) rb_ary_union(ary_union, argv[i]); return ary_union; } - hash = ary_make_hash(ary); - for (i = 0; i < argc; i++) rb_ary_union_hash(hash, argv[i]); + VALUE set = rb_obj_hide(rb_set_new_capa(sum)); + rb_ary_union_set(set, ary); + for (int i = 0; i < argc; i++) rb_ary_union_set(set, argv[i]); - return rb_hash_values(hash); + return rb_set_to_a(set); } /* From f792a0681151eb7c335412d46096157d69483e1e Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 20:45:18 +0900 Subject: [PATCH 07/17] Use Set instead of Hash for Array#- MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#- to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a - b } Results: Benchmark 1: master Time (mean ± σ): 200.3 ms ± 3.8 ms [User: 188.0 ms, System: 9.4 ms] Range (min … max): 194.1 ms … 206.8 ms 14 runs Benchmark 2: branch Time (mean ± σ): 170.7 ms ± 6.6 ms [User: 158.7 ms, System: 9.3 ms] Range (min … max): 160.9 ms … 183.7 ms 17 runs Summary branch ran 1.17 ± 0.05 times faster than master --- array.c | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/array.c b/array.c index 100439819634d0..fb0cead6c7ece1 100644 --- a/array.c +++ b/array.c @@ -5754,6 +5754,14 @@ ary_make_hash(VALUE ary) return ary_add_hash(hash, ary); } +static void +rb_ary_union_set(VALUE set, VALUE ary) +{ + for (long i = 0; i < RARRAY_LEN(ary); i++) { + rb_set_add_no_check(set, RARRAY_AREF(ary, i)); + } +} + static VALUE ary_add_hash_by(VALUE hash, VALUE ary) { @@ -5794,16 +5802,12 @@ ary_make_hash_by(VALUE ary) VALUE rb_ary_diff(VALUE ary1, VALUE ary2) { - VALUE ary3; - VALUE hash; - long i; - ary2 = to_ary(ary2); if (RARRAY_LEN(ary2) == 0) { return ary_make_shared_copy(ary1); } - ary3 = rb_ary_new(); + VALUE ary3 = rb_ary_new(); if (RARRAY_LEN(ary1) <= SMALL_ARRAY_LEN || RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - for (i=0; i new_array From f79fdf303ce3029e55537f6cdf26e891b0cd7a23 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 21:52:44 +0900 Subject: [PATCH 08/17] Use Set instead of Hash for Array#difference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#difference to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a.difference(b) } Results: Benchmark 1: master Time (mean ± σ): 192.2 ms ± 7.1 ms [User: 179.7 ms, System: 9.0 ms] Range (min … max): 182.4 ms … 208.9 ms 15 runs Benchmark 2: branch Time (mean ± σ): 168.1 ms ± 4.7 ms [User: 156.5 ms, System: 8.5 ms] Range (min … max): 160.2 ms … 174.6 ms 17 runs Summary branch ran 1.14 ± 0.05 times faster than master --- array.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/array.c b/array.c index fb0cead6c7ece1..80f3e3f30d6473 100644 --- a/array.c +++ b/array.c @@ -5847,25 +5847,27 @@ rb_ary_diff(VALUE ary1, VALUE ary2) static VALUE rb_ary_difference_multi(int argc, VALUE *argv, VALUE ary) { - VALUE ary_diff; - long i, length; volatile VALUE t0; - bool *is_hash = ALLOCV_N(bool, t0, argc); - ary_diff = rb_ary_new(); - length = RARRAY_LEN(ary); + bool *is_set = ALLOCV_N(bool, t0, argc); + VALUE ary_diff = rb_ary_new(); + long length = RARRAY_LEN(ary); - for (i = 0; i < argc; i++) { + for (long i = 0; i < argc; i++) { argv[i] = to_ary(argv[i]); - is_hash[i] = (length > SMALL_ARRAY_LEN && RARRAY_LEN(argv[i]) > SMALL_ARRAY_LEN); - if (is_hash[i]) argv[i] = ary_make_hash(argv[i]); + is_set[i] = (length > SMALL_ARRAY_LEN && RARRAY_LEN(argv[i]) > SMALL_ARRAY_LEN); + if (is_set[i]) { + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(argv[i]))); + rb_ary_union_set(set, argv[i]); + argv[i] = set; + } } - for (i = 0; i < RARRAY_LEN(ary); i++) { + for (long i = 0; i < RARRAY_LEN(ary); i++) { int j; VALUE elt = rb_ary_elt(ary, i); for (j = 0; j < argc; j++) { - if (is_hash[j]) { - if (rb_hash_stlike_lookup(argv[j], elt, NULL)) + if (is_set[j]) { + if (rb_set_lookup(argv[j], elt)) break; } else { From 41cf0629560dd041b66505fdaa92fb1423b6a0dc Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 22:05:34 +0900 Subject: [PATCH 09/17] Use Set instead of Hash for Array#& MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#& to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a & b } Results: Benchmark 1: master Time (mean ± σ): 196.4 ms ± 9.1 ms [User: 182.9 ms, System: 9.5 ms] Range (min … max): 186.3 ms … 214.7 ms 13 runs Benchmark 2: branch Time (mean ± σ): 163.4 ms ± 3.3 ms [User: 152.7 ms, System: 7.8 ms] Range (min … max): 157.7 ms … 169.9 ms 17 runs Summary branch ran 1.20 ± 0.06 times faster than master --- array.c | 20 ++++++++------------ internal/set.h | 1 + set.c | 6 ++++++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/array.c b/array.c index 80f3e3f30d6473..4150106707bc94 100644 --- a/array.c +++ b/array.c @@ -5910,17 +5910,13 @@ rb_ary_difference_multi(int argc, VALUE *argv, VALUE ary) static VALUE rb_ary_and(VALUE ary1, VALUE ary2) { - VALUE hash, ary3, v; - st_data_t vv; - long i; - ary2 = to_ary(ary2); - ary3 = rb_ary_new(); + VALUE ary3 = rb_ary_new(); if (RARRAY_LEN(ary1) == 0 || RARRAY_LEN(ary2) == 0) return ary3; if (RARRAY_LEN(ary1) <= SMALL_ARRAY_LEN && RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - for (i=0; i Date: Sat, 12 Sep 2026 22:12:55 +0900 Subject: [PATCH 10/17] Use Set instead of Hash for Array#intersect? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#intersect? to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a b = (5_000...15_000).to_a 1_000.times { a.intersect?(b) } Results: Benchmark 1: master Time (mean ± σ): 126.8 ms ± 3.8 ms [User: 115.5 ms, System: 8.9 ms] Range (min … max): 120.9 ms … 134.2 ms 22 runs Benchmark 2: branch Time (mean ± σ): 99.3 ms ± 3.5 ms [User: 89.4 ms, System: 7.6 ms] Range (min … max): 94.1 ms … 109.7 ms 29 runs Summary branch ran 1.28 ± 0.06 times faster than master --- array.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/array.c b/array.c index 4150106707bc94..8ddfcccf2f0d5b 100644 --- a/array.c +++ b/array.c @@ -6080,35 +6080,31 @@ rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) static VALUE rb_ary_intersect_p(VALUE ary1, VALUE ary2) { - VALUE hash, v, result, shorter, longer; - st_data_t vv; - long i; - ary2 = to_ary(ary2); if (RARRAY_LEN(ary1) == 0 || RARRAY_LEN(ary2) == 0) return Qfalse; if (RARRAY_LEN(ary1) <= SMALL_ARRAY_LEN && RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - for (i=0; i RARRAY_LEN(ary2)) { longer = ary1; shorter = ary2; } - hash = ary_make_hash(shorter); - result = Qfalse; + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(shorter))); + rb_ary_union_set(set, shorter); + VALUE result = Qfalse; - for (i=0; i Date: Sat, 12 Sep 2026 22:38:53 +0900 Subject: [PATCH 11/17] Use Set instead of Hash for Array#uniq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#uniq to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a + (0...10_000).to_a 1_000.times { a.uniq } Results: Benchmark 1: master Time (mean ± σ): 194.6 ms ± 4.9 ms [User: 183.2 ms, System: 8.7 ms] Range (min … max): 187.5 ms … 205.1 ms 15 runs Benchmark 2: branch Time (mean ± σ): 147.2 ms ± 4.0 ms [User: 134.9 ms, System: 9.5 ms] Range (min … max): 139.2 ms … 153.4 ms 20 runs Summary branch ran 1.32 ± 0.05 times faster than master --- array.c | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/array.c b/array.c index 8ddfcccf2f0d5b..d52fe16136226e 100644 --- a/array.c +++ b/array.c @@ -6591,22 +6591,25 @@ rb_ary_uniq_bang(VALUE ary) static VALUE rb_ary_uniq(VALUE ary) { - VALUE hash, uniq; - if (RARRAY_LEN(ary) <= 1) { - hash = 0; - uniq = rb_ary_dup(ary); + return rb_ary_dup(ary); } - else if (rb_block_given_p()) { - hash = ary_make_hash_by(ary); - uniq = rb_hash_values(hash); + + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary))); + + if (rb_block_given_p()) { + VALUE uniq = rb_ary_new_capa(RARRAY_LEN(ary)); + for (long i = 0; i < RARRAY_LEN(ary); i++) { + VALUE elt = rb_ary_elt(ary, i); + if (rb_set_add_no_check(set, rb_yield(elt))) + rb_ary_push(uniq, elt); + } + return uniq; } else { - hash = ary_make_hash(ary); - uniq = rb_hash_values(hash); + rb_ary_union_set(set, ary); + return rb_set_to_a(set); } - - return uniq; } /* From a4613a13fdb9515c70b385118d8c2d800a8557c4 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 22:48:15 +0900 Subject: [PATCH 12/17] Use Set instead of Hash for Array#uniq! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Array#uniq! to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = (0...10_000).to_a + (0...10_000).to_a 1_000.times { a.dup.uniq! } Results: Benchmark 1: master Time (mean ± σ): 242.1 ms ± 3.9 ms [User: 229.6 ms, System: 9.6 ms] Range (min … max): 235.7 ms … 248.3 ms 12 runs Benchmark 2: branch Time (mean ± σ): 197.4 ms ± 6.1 ms [User: 184.2 ms, System: 10.4 ms] Range (min … max): 185.9 ms … 204.5 ms 14 runs Summary branch ran 1.23 ± 0.04 times faster than master --- array.c | 83 +++++++++++++++------------------------------------------ 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/array.c b/array.c index d52fe16136226e..278410528a3379 100644 --- a/array.c +++ b/array.c @@ -5725,35 +5725,6 @@ rb_ary_cmp(VALUE ary1, VALUE ary2) return INT2FIX(-1); } -static VALUE -ary_add_hash(VALUE hash, VALUE ary) -{ - long i; - - for (i=0; i new_array @@ -6499,9 +6451,9 @@ rb_ary_minmax(VALUE ary) } static int -push_value(st_data_t key, st_data_t val, st_data_t ary) +push_value_i(VALUE elt, VALUE ary) { - rb_ary_push((VALUE)ary, (VALUE)val); + rb_ary_push(ary, elt); return ST_CONTINUE; } @@ -6535,19 +6487,28 @@ push_value(st_data_t key, st_data_t val, st_data_t ary) static VALUE rb_ary_uniq_bang(VALUE ary) { - VALUE hash; - long hash_size; - rb_ary_modify_check(ary); if (RARRAY_LEN(ary) <= 1) return Qnil; - if (rb_block_given_p()) - hash = ary_make_hash_by(ary); - else - hash = ary_make_hash(ary); - hash_size = RHASH_SIZE(hash); - if (RARRAY_LEN(ary) == hash_size) { + if (rb_block_given_p()) { + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary))); + VALUE uniq = rb_ary_new_capa(RARRAY_LEN(ary)); + for (long i = 0; i < RARRAY_LEN(ary); i++) { + VALUE elt = rb_ary_elt(ary, i); + if (rb_set_add_no_check(set, rb_yield(elt))) + rb_ary_push(uniq, elt); + } + if (RARRAY_LEN(ary) == RARRAY_LEN(uniq)) + return Qnil; + rb_ary_replace(ary, uniq); + return ary; + } + + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary))); + rb_ary_union_set(set, ary); + long size = (long)rb_set_size(set); + if (RARRAY_LEN(ary) == size) { return Qnil; } rb_ary_modify_check(ary); @@ -6556,8 +6517,8 @@ rb_ary_uniq_bang(VALUE ary) rb_ary_unshare(ary); FL_SET_EMBED(ary); } - ary_resize_capa(ary, hash_size); - rb_hash_foreach(hash, push_value, ary); + ary_resize_capa(ary, size); + rb_set_foreach(set, push_value_i, ary); return ary; } From 73b7ffeb85fec56bb28be0545cb7063603247a29 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 13 Sep 2026 08:51:48 +0900 Subject: [PATCH 13/17] Add ary_to_set --- array.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/array.c b/array.c index 278410528a3379..806ce4d231d7c6 100644 --- a/array.c +++ b/array.c @@ -5733,6 +5733,14 @@ rb_ary_union_set(VALUE set, VALUE ary) } } +static VALUE +ary_to_set(VALUE ary) +{ + VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary))); + rb_ary_union_set(set, ary); + return set; +} + /* * call-seq: * self - other_array -> new_array @@ -5767,8 +5775,7 @@ rb_ary_diff(VALUE ary1, VALUE ary2) return ary3; } - VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary2))); - rb_ary_union_set(set, ary2); + VALUE set = ary_to_set(ary2); for (long i = 0; i < RARRAY_LEN(ary1); i++) { if (rb_set_lookup(set, RARRAY_AREF(ary1, i))) continue; rb_ary_push(ary3, rb_ary_elt(ary1, i)); @@ -5808,9 +5815,7 @@ rb_ary_difference_multi(int argc, VALUE *argv, VALUE ary) argv[i] = to_ary(argv[i]); is_set[i] = (length > SMALL_ARRAY_LEN && RARRAY_LEN(argv[i]) > SMALL_ARRAY_LEN); if (is_set[i]) { - VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(argv[i]))); - rb_ary_union_set(set, argv[i]); - argv[i] = set; + argv[i] = ary_to_set(argv[i]); } } @@ -5876,8 +5881,7 @@ rb_ary_and(VALUE ary1, VALUE ary2) return ary3; } - VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary2))); - rb_ary_union_set(set, ary2); + VALUE set = ary_to_set(ary2); for (long i = 0; i < RARRAY_LEN(ary1); i++) { VALUE v = RARRAY_AREF(ary1, i); @@ -6050,8 +6054,7 @@ rb_ary_intersect_p(VALUE ary1, VALUE ary2) shorter = ary2; } - VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(shorter))); - rb_ary_union_set(set, shorter); + VALUE set = ary_to_set(shorter); VALUE result = Qfalse; for (long i = 0; i < RARRAY_LEN(longer); i++) { @@ -6505,8 +6508,7 @@ rb_ary_uniq_bang(VALUE ary) return ary; } - VALUE set = rb_obj_hide(rb_set_new_capa(RARRAY_LEN(ary))); - rb_ary_union_set(set, ary); + VALUE set = ary_to_set(ary); long size = (long)rb_set_size(set); if (RARRAY_LEN(ary) == size) { return Qnil; From 3b64ab47d0d57fb3121aa3d4bc54e291e6f28d41 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sun, 13 Sep 2026 05:44:40 +0900 Subject: [PATCH 14/17] Convert the fast fallback test delay with NUM2LONG The delay in `test_mode_settings` was stored as a raw VALUE, so a delay of n milliseconds slept for 2n+1. Co-Authored-By: Claude Opus 5 --- ext/socket/ipsocket.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/socket/ipsocket.c b/ext/socket/ipsocket.c index be8ee53a427802..506a56a1865bc4 100644 --- a/ext/socket/ipsocket.c +++ b/ext/socket/ipsocket.c @@ -713,7 +713,7 @@ init_fast_fallback_inetsock_internal(VALUE v) VALUE test_delay_setting = rb_hash_aref(test_mode_settings, ID2SYM(rb_intern("delay"))); if (!NIL_P(test_delay_setting)) { VALUE rb_test_delay_ms = rb_hash_aref(test_delay_setting, ID2SYM(rb_intern(family_sym))); - long test_delay_ms = NIL_P(rb_test_delay_ms) ? 0 : rb_test_delay_ms; + long test_delay_ms = NIL_P(rb_test_delay_ms) ? 0 : NUM2LONG(rb_test_delay_ms); arg->getaddrinfo_entries[i]->test_sleep_ms = test_delay_ms; } From bd52c74ce188d48093943a62e7276ce739cb10ad Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 13 Sep 2026 10:40:20 +0900 Subject: [PATCH 15/17] [DOC] Add File.lchmod and Pathname#lchmod examples --- file.c | 14 +++++++++++++- pathname_builtin.rb | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index 8d021238280073..e3e12052e9e2de 100644 --- a/file.c +++ b/file.c @@ -3138,7 +3138,19 @@ lchmod_internal(const char *path, void *mode) * When supported: like File::chmod, * but does not follow [symbolic links](rdoc-ref:file/symbolic_links.md), * and therefore changes the mode of the entries given by `paths`; - * returns the number of paths given. + * returns the number of paths given: + * + * ```ruby + * File.write('t.tmp', '') + * File.symlink('t.tmp', 'link') + * File.lstat('t.tmp').mode.to_s(8) # => "100664" + * File.lstat('link').mode.to_s(8) # => "120755" + * File.lchmod(0777, 'link') + * File.lstat('t.tmp').mode.to_s(8) # => "100664" + * File.lstat('link').mode.to_s(8) # => "120777" + * File.delete('t.tmp') + * File.delete('link') + * ``` */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index f01b770860f110..2191beccd1137a 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1547,7 +1547,21 @@ def chmod(mode) File.chmod(mode, @path) end # # When supported: like Pathname#chmod, # but does not follow [symbolic links](rdoc-ref:file/symbolic_links.md), - # and therefore changes the mode of the entry specified by `self`. + # and therefore changes the mode of the entry specified by `self`: + # + # ```ruby + # file = Pathname('t.tmp') + # file.write('') + # File.symlink('t.tmp', 'link') + # symlink = Pathname('link') + # file.lstat.mode.to_s(8) # => "100644" + # symlink.lstat.mode.to_s(8) # => "120755" + # symlink.lchmod(0777) + # file.lstat.mode.to_s(8) # => "100644" + # symlink.lstat.mode.to_s(8) # => "120777" + # file.delete + # symlink.delete + # ```` def lchmod(mode) File.lchmod(mode, @path) end # :markup: markdown From 7ebf7492a9e9453eb31deb3a11e2d2a713ea29d9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 13 Sep 2026 12:57:05 +0900 Subject: [PATCH 16/17] Preserve the blocking timeout across EC_EXEC_TAG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save `timeout` in a volatile pointer before `EC_EXEC_TAG` to avoid GCC 15.3.0's `-Wclobbered` warning on aarch64-linux. ``` ../src/thread.c: In function ‘thread_io_wait’: ../src/thread.c:4915:87: warning: argument ‘timeout’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered] 4915 | thread_io_wait(rb_thread_t *th, struct rb_io *io, int fd, int events, struct timeval *timeout) | ~~~~~~~~~~~~~~~~^~~~~~~ ``` --- thread.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thread.c b/thread.c index 992f81190882ff..4b64e8ed92d49c 100644 --- a/thread.c +++ b/thread.c @@ -4972,10 +4972,11 @@ COMPILER_WARNING_POP break; case io_wait_unhandled: EC_PUSH_TAG(ec); + struct timeval *volatile blocking_timeout = timeout; if ((state = EC_EXEC_TAG()) == TAG_NONE) { rb_hrtime_t *to, rel, end = 0; RUBY_VM_CHECK_INTS_BLOCKING(ec); - timeout_prepare(&to, &rel, &end, timeout); + timeout_prepare(&to, &rel, &end, blocking_timeout); do { nfds = numberof(fds); result = wait_for_single_fd_blocking_region(th, fds, nfds, to, &lerrno); From 29c669bb5689955546ef0531b3bddd7d20cdec9c Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 13 Sep 2026 12:27:34 +0900 Subject: [PATCH 17/17] Use Set instead of Hash for Enumerable#uniq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes Enumerable#uniq to use a temporary Set instead of Hash for performance and memory savings. We can see in the following benchmark: a = ((0...10_000).to_a + (0...10_000).to_a).each 1_000.times { a.uniq } Results: Benchmark 1: master Time (mean ± σ): 222.4 ms ± 3.7 ms [User: 212.4 ms, System: 7.6 ms] Range (min … max): 216.9 ms … 227.6 ms 13 runs Benchmark 2: branch Time (mean ± σ): 189.7 ms ± 4.0 ms [User: 177.7 ms, System: 9.2 ms] Range (min … max): 182.8 ms … 198.6 ms 15 runs Summary branch ran 1.17 ± 0.03 times faster than master --- enum.c | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/enum.c b/enum.c index a2941dd7dd5b38..8e6c40639a10e5 100644 --- a/enum.c +++ b/enum.c @@ -20,6 +20,7 @@ #include "internal/proc.h" #include "internal/rational.h" #include "internal/re.h" +#include "internal/set.h" #include "ruby/util.h" #include "ruby_assert.h" #include "symbol.h" @@ -4870,18 +4871,26 @@ enum_sum(int argc, VALUE* argv, VALUE obj) } static VALUE -uniq_func(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash)) +uniq_func(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) { ENUM_WANT_SVALUE(); - rb_hash_add_new_element(hash, i, i); + rb_set_add_no_check(set, i); return Qnil; } +struct uniq_iter_memo { + VALUE set; + VALUE ary; +}; + static VALUE -uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash)) +uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo_)) { + struct uniq_iter_memo *memo = (struct uniq_iter_memo *)memo_; ENUM_WANT_SVALUE(); - rb_hash_add_new_element(hash, rb_yield_values2(argc, argv), i); + if (rb_set_add_no_check(memo->set, rb_yield_values2(argc, argv))) { + rb_ary_push(memo->ary, i); + } return Qnil; } @@ -4909,15 +4918,18 @@ uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash)) static VALUE enum_uniq(VALUE obj) { - VALUE hash, ret; - rb_block_call_func *const func = - rb_block_given_p() ? uniq_iter : uniq_func; - - hash = rb_obj_hide(rb_hash_new()); - rb_block_call(obj, id_each, 0, 0, func, hash); - ret = rb_hash_values(hash); - rb_hash_clear(hash); - return ret; + if (rb_block_given_p()) { + struct uniq_iter_memo memo; + memo.set = rb_obj_hide(rb_set_new()); + memo.ary = rb_ary_new(); + rb_block_call(obj, id_each, 0, 0, uniq_iter, (VALUE)&memo); + return memo.ary; + } + else { + VALUE set = rb_obj_hide(rb_set_new()); + rb_block_call(obj, id_each, 0, 0, uniq_func, set); + return rb_set_to_a(set); + } } static VALUE